Housing: Refactor fixture resolution to direct (type, wmoDataID) lookup

Replace the broken GroupXHook→Group→XGroup chain with a direct
_defaultFixtureByTypeWmo index that maps (componentType, wmoDataID)
to the default fixture component ID. This correctly resolves fixtures
for all 4 racial house styles (Human/NightElf/BloodElf/Orc).

Key changes:
- BuildExteriorComponentIndexes: filter structural roots by
  ExteriorComponentType.ParentComponentType==0 with hardcoded
  fallback for broken DB2 store iteration
- New GetDefaultFixtureForType() replaces GetComponentAtHook()
- SpawnExtCompTree uses type+WMO lookup for hook children
- Door GO spawning fully data-driven from DB2 (entry + position
  from hook offset + ExitPoint offset)
- Root selection: rootOverrides → coreExtCompID → default → first
- Fix missing fixture overrides on late-spawn path
- Add GetRootComponentOverrides() for player-selected root variants
This commit is contained in:
luis
2026-03-15 17:52:14 -03:00
parent 29d7e7689b
commit 06a4b3f844
7 changed files with 294 additions and 262 deletions
+11 -5
View File
@@ -1792,12 +1792,18 @@ void WorldSession::HandleHousingFixtureDeleteFixture(WorldPackets::Housing::Hous
housingMap->DespawnSingleMeshObject(plotIndex, oldMesh->GetGUID());
}
// Spawn default component back at this hook (DB2 default)
ExteriorComponentEntry const* defaultComp = sHousingMgr.GetComponentAtHook(static_cast<int32>(removedHookID), housing->GetCoreExteriorComponentID());
if (defaultComp)
// Spawn default component back at this hook (DB2 default by type + wmoDataID)
ExteriorComponentHookEntry const* hookEntry = sExteriorComponentHookStore.LookupEntry(removedHookID);
if (hookEntry)
{
housingMap->SpawnFixtureAtHook(plotIndex, removedHookID, defaultComp->ID,
housing->GetHouseGuid(), static_cast<int32>(housing->GetHouseType()), player);
uint32 defaultCompID = sHousingMgr.GetDefaultFixtureForType(
static_cast<uint8>(hookEntry->ExteriorComponentTypeID),
static_cast<uint32>(housing->GetHouseType()));
if (defaultCompID)
{
housingMap->SpawnFixtureAtHook(plotIndex, removedHookID, defaultCompID,
housing->GetHouseGuid(), static_cast<int32>(housing->GetHouseType()), player);
}
}
}
+25 -38
View File
@@ -1771,51 +1771,38 @@ std::vector<Housing::Fixture const*> Housing::GetFixtures() const
std::unordered_map<uint32, uint32> Housing::GetFixtureOverrideMap() const
{
// Build override map from player's fixture selections.
// Two kinds of overrides:
// 1. Hook-based (CreateFixture): OptionId != 0 ? hookID ? componentID
// 2. Root-based (SetCoreFixture non-base): OptionId == 0, Type != 9 (Base)
// ? maps defaultRootComponentID ? newComponentID by matching Slot in the group
// Build override map from player's hook-based fixture selections.
// These are fixtures at hooks (doors, windows, etc.) where OptionId != 0.
// Root overrides (base, roof variants) are handled separately via GetRootComponentOverrides().
std::unordered_map<uint32, uint32> result;
uint32 baseCompID = GetCoreExteriorComponentID();
int32 groupID = sHousingMgr.GetGroupForComponent(baseCompID);
for (auto const& [pointId, fixture] : _fixtures)
{
if (fixture.OptionId != 0)
{
// Hook-based override (CreateFixture): hookID ? componentID
result[fixture.FixturePointId] = fixture.OptionId;
}
else
{
// Core fixture (SetCoreFixture): OptionId == 0
ExteriorComponentEntry const* newComp = sExteriorComponentStore.LookupEntry(fixture.FixturePointId);
if (!newComp || newComp->Type == 9) // Base handled by GetCoreExteriorComponentID()
continue;
}
return result;
}
// Non-base core fixture (roof, door, etc.): find the DEFAULT root component
// of the same Slot in the group and override it.
if (groupID != 0)
{
std::vector<uint32> const* groupComps = sHousingMgr.GetComponentsInGroup(groupID);
if (groupComps)
{
for (uint32 defaultCompID : *groupComps)
{
if (defaultCompID == fixture.FixturePointId)
continue;
ExteriorComponentEntry const* defaultComp = sExteriorComponentStore.LookupEntry(defaultCompID);
if (defaultComp && defaultComp->Type == newComp->Type && defaultComp->ParentComponentID <= 0)
{
result[defaultCompID] = fixture.FixturePointId;
break;
}
}
}
}
}
std::unordered_map<uint8, uint32> Housing::GetRootComponentOverrides() const
{
// Build override map for player-selected root components per type.
// Core fixtures (OptionId == 0) where the component is a root (ParentComponentID == 0)
// represent the player's choice for that component type (base, roof, etc.).
std::unordered_map<uint8, uint32> result;
for (auto const& [pointId, fixture] : _fixtures)
{
if (fixture.OptionId != 0)
continue;
ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(fixture.FixturePointId);
if (!comp || comp->ParentComponentID != 0)
continue;
if (_houseType != 0 && comp->HouseExteriorWmoDataID != static_cast<uint32>(_houseType))
continue;
result[comp->Type] = fixture.FixturePointId;
}
return result;
}
+1
View File
@@ -160,6 +160,7 @@ public:
std::vector<Fixture const*> GetFixtures() const;
std::unordered_map<uint32, uint32> GetFixtureOverrideMap() const;
uint32 GetCoreExteriorComponentID() const;
std::unordered_map<uint8, uint32> GetRootComponentOverrides() const;
// Catalog operations
HousingResult AddToCatalog(uint32 decorEntryId, uint8 sourceType = DECOR_SOURCE_STANDARD, std::string sourceValue = {});
+173 -139
View File
@@ -385,21 +385,21 @@ void HousingMap::SpawnPlotGameObjects()
TC_LOG_DEBUG("housing", "HousingMap::SpawnPlotGameObjects: Plot {} using ExteriorComponentID={}, WmoDataID={}",
plotIdx, exteriorComponentID, houseExteriorWmoDataID);
// Build fixture override map from player's saved fixture selections
FixtureOverrideMap fixtureOverrides;
if (housing)
fixtureOverrides = housing->GetFixtureOverrideMap();
// Build fixture + root override maps from player's saved fixture selections
FixtureOverrideMap fixtureOverrides = housing->GetFixtureOverrideMap();
FixtureOverrideMap const* overridesPtr = fixtureOverrides.empty() ? nullptr : &fixtureOverrides;
RootOverrideMap rootOverrides = housing->GetRootComponentOverrides();
RootOverrideMap const* rootOvrPtr = rootOverrides.empty() ? nullptr : &rootOverrides;
GameObject* houseGo = nullptr;
if (housing && housing->HasCustomPosition())
if (housing->HasCustomPosition())
{
Position customPos = housing->GetHousePosition();
houseGo = SpawnHouseForPlot(plotIdx, &customPos, exteriorComponentID, houseExteriorWmoDataID, overridesPtr);
houseGo = SpawnHouseForPlot(plotIdx, &customPos, exteriorComponentID, houseExteriorWmoDataID, overridesPtr, rootOvrPtr);
}
else
{
houseGo = SpawnHouseForPlot(plotIdx, nullptr, exteriorComponentID, houseExteriorWmoDataID, overridesPtr);
houseGo = SpawnHouseForPlot(plotIdx, nullptr, exteriorComponentID, houseExteriorWmoDataID, overridesPtr, rootOvrPtr);
}
++houseCount;
if (houseGo)
@@ -676,15 +676,17 @@ bool HousingMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/)
auto fixtureOverrides = housing->GetFixtureOverrideMap();
FixtureOverrideMap const* overridesPtr = fixtureOverrides.empty() ? nullptr : &fixtureOverrides;
auto rootOverrides = housing->GetRootComponentOverrides();
RootOverrideMap const* rootOvrPtr = rootOverrides.empty() ? nullptr : &rootOverrides;
GameObject* go = nullptr;
if (housing->HasCustomPosition())
{
Position customPos = housing->GetHousePosition();
go = SpawnHouseForPlot(plotIdx, &customPos, exteriorComponentID, houseExteriorWmoDataID, overridesPtr);
go = SpawnHouseForPlot(plotIdx, &customPos, exteriorComponentID, houseExteriorWmoDataID, overridesPtr, rootOvrPtr);
}
else
go = SpawnHouseForPlot(plotIdx, nullptr, exteriorComponentID, houseExteriorWmoDataID, overridesPtr);
go = SpawnHouseForPlot(plotIdx, nullptr, exteriorComponentID, houseExteriorWmoDataID, overridesPtr, rootOvrPtr);
TC_LOG_DEBUG("housing", "HousingMap::AddPlayerToMap: SpawnHouseForPlot result for plot {}: {}",
plotIdx, go ? go->GetGUID().ToString() : "FAILED");
@@ -713,8 +715,14 @@ bool HousingMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/)
int32 lateExtCompID = static_cast<int32>(housing->GetCoreExteriorComponentID());
int32 lateWmoDataID = static_cast<int32>(housing->GetHouseType());
auto lateFixtureOvr = housing->GetFixtureOverrideMap();
FixtureOverrideMap const* lateFixturePtr = lateFixtureOvr.empty() ? nullptr : &lateFixtureOvr;
auto lateRootOvr = housing->GetRootComponentOverrides();
RootOverrideMap const* lateRootPtr = lateRootOvr.empty() ? nullptr : &lateRootOvr;
SpawnFullHouseMeshObjects(plotIdx, pos, rot,
housing->GetHouseGuid(), lateExtCompID, lateWmoDataID, faction);
housing->GetHouseGuid(), lateExtCompID, lateWmoDataID, faction,
lateFixturePtr, lateRootPtr);
// Also spawn room entity + Geobox if not already present
if (_roomEntities.find(plotIdx) == _roomEntities.end())
@@ -1507,7 +1515,8 @@ void HousingMap::RemovePlayerHousing(ObjectGuid playerGuid)
GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* customPos,
int32 exteriorComponentID, int32 houseExteriorWmoDataID,
FixtureOverrideMap const* fixtureOverrides /*= nullptr*/)
FixtureOverrideMap const* fixtureOverrides /*= nullptr*/,
RootOverrideMap const* rootOverrides /*= nullptr*/)
{
if (!_neighborhood)
return nullptr;
@@ -1627,7 +1636,7 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo
faction == NEIGHBORHOOD_FACTION_ALLIANCE ? "Alliance" : "Horde");
SpawnFullHouseMeshObjects(plotIndex, pos, rot, plotInfo->HouseGuid,
exteriorComponentID, houseExteriorWmoDataID, faction, fixtureOverrides);
exteriorComponentID, houseExteriorWmoDataID, faction, fixtureOverrides, rootOverrides);
// Spawn room entity + component mesh with Geobox for this plot.
// The client uses the MeshObject Geobox to validate decor placement bounds.
@@ -1635,42 +1644,95 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo
SpawnRoomForPlot(plotIndex, pos, rot, plotInfo->HouseGuid);
}
// Spawn the front door GO (entry 586576, type Goober, displayId 116973)
// The interactive door GO must be at the actual doorway (top of stairs), NOT at
// the door mesh origin (which sits at stair base level, ~0.56 below house floor).
// Door mesh local offset from base: (9.2805, -3.4555, -0.5611) ? mesh origin at stair foot
// Adjusted offset: raised Z to door frame level, pulled X back toward actual door threshold.
// Spawn the front door interactive GO.
// The door's GO entry and position come from DB2:
// 1. Resolve the default door component (Type=11) for this house's WMO data
// 2. Get the door component's GameObjectID for the GO entry
// 3. Find the door hook on the base component for the hook offset
// 4. Get the ExitPoint on the door component for the interaction offset
// 5. World position = house pos + hook offset + exit point offset
GameObject* doorGo = nullptr;
uint32 doorEntry = 586576; // retail "Founder's Point Front Door"
uint32 doorEntry = 0;
float doorLocalX = 0.0f;
float doorLocalY = 0.0f;
float doorLocalZ = 0.0f;
// Door interaction GO local-space offset (adjusted from mesh origin to doorway threshold)
float doorLocalX = 8.0f;
float doorLocalY = -3.4555f;
float doorLocalZ = 1.5f;
// Try to get the offset from DB2 ExteriorComponentExitPoint if available
int32 groupID = sHousingMgr.GetGroupForComponent(static_cast<uint32>(exteriorComponentID));
if (groupID != 0)
// Resolve door component from DB2
uint32 doorCompID = 0;
if (fixtureOverrides)
{
std::vector<uint32> const* groupComps = sHousingMgr.GetComponentsInGroup(groupID);
if (groupComps)
// Check if player has a custom door at a hook ? scan hooks for Type=11
auto const* baseHooks = sHousingMgr.GetHooksOnComponent(static_cast<uint32>(exteriorComponentID));
if (baseHooks)
{
for (uint32 compID : *groupComps)
for (ExteriorComponentHookEntry const* hook : *baseHooks)
{
ExteriorComponentExitPointEntry const* exitPt = sHousingMgr.GetExitPoint(compID);
if (exitPt)
if (hook && hook->ExteriorComponentTypeID == 11)
{
doorLocalX = exitPt->Position[0];
doorLocalY = exitPt->Position[1];
doorLocalZ = exitPt->Position[2];
TC_LOG_DEBUG("housing", "HousingMap::SpawnHouseForPlot: Door offset from DB2 "
"ExitPoint (comp={}) = ({:.2f}, {:.2f}, {:.2f})",
compID, doorLocalX, doorLocalY, doorLocalZ);
auto ovrItr = fixtureOverrides->find(hook->ID);
if (ovrItr != fixtureOverrides->end())
doorCompID = ovrItr->second;
break;
}
}
}
}
if (!doorCompID && houseExteriorWmoDataID > 0)
doorCompID = sHousingMgr.GetDefaultFixtureForType(11 /*Door*/, static_cast<uint32>(houseExteriorWmoDataID));
ExteriorComponentEntry const* doorComp = doorCompID ? sExteriorComponentStore.LookupEntry(doorCompID) : nullptr;
if (doorComp)
{
doorEntry = doorComp->GameObjectID > 0 ? static_cast<uint32>(doorComp->GameObjectID) : 0;
// Door position: start from the door component's Position (local-space offset from base)
doorLocalX = doorComp->Position[0];
doorLocalY = doorComp->Position[1];
doorLocalZ = doorComp->Position[2];
// Also look for a hook offset on the base component (Type=11 hook)
auto const* baseHooks = sHousingMgr.GetHooksOnComponent(static_cast<uint32>(exteriorComponentID));
if (baseHooks)
{
for (ExteriorComponentHookEntry const* hook : *baseHooks)
{
if (hook && hook->ExteriorComponentTypeID == 11)
{
// Hook position provides the attachment point on the base
doorLocalX = hook->Position[0];
doorLocalY = hook->Position[1];
doorLocalZ = hook->Position[2];
// Add ExitPoint offset (interaction point relative to door)
ExteriorComponentExitPointEntry const* exitPt = sHousingMgr.GetExitPoint(doorCompID);
if (exitPt)
{
doorLocalX += exitPt->Position[0];
doorLocalY += exitPt->Position[1];
doorLocalZ += exitPt->Position[2];
}
TC_LOG_DEBUG("housing", "HousingMap::SpawnHouseForPlot: Door comp={} entry={} "
"hook=({:.2f},{:.2f},{:.2f}) exitPt=({:.2f},{:.2f},{:.2f}) "
"final=({:.2f},{:.2f},{:.2f})",
doorCompID, doorEntry,
hook->Position[0], hook->Position[1], hook->Position[2],
exitPt ? exitPt->Position[0] : 0.0f,
exitPt ? exitPt->Position[1] : 0.0f,
exitPt ? exitPt->Position[2] : 0.0f,
doorLocalX, doorLocalY, doorLocalZ);
break;
}
}
}
}
if (!doorEntry)
{
TC_LOG_ERROR("housing", "HousingMap::SpawnHouseForPlot: No door component found "
"for plot {} (extComp={}, wmoData={}) ? door GO will not spawn",
plotIndex, exteriorComponentID, houseExteriorWmoDataID);
}
// Transform local-space door offset to world space
float cosFacing = std::cos(facing);
@@ -1680,37 +1742,41 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo
float doorZ = z + doorLocalZ;
Position doorPos(doorX, doorY, doorZ, facing);
GameObjectTemplate const* doorTemplate = sObjectMgr->GetGameObjectTemplate(doorEntry);
if (doorTemplate)
if (doorEntry)
{
doorGo = GameObject::CreateGameObject(doorEntry, this, doorPos, rot, 255, GO_STATE_READY);
if (doorGo)
GameObjectTemplate const* doorTemplate = sObjectMgr->GetGameObjectTemplate(doorEntry);
if (doorTemplate)
{
doorGo->SetFlag(GO_FLAG_NODESPAWN);
PhasingHandler::InitDbPhaseShift(doorGo->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
if (AddToMap(doorGo))
doorGo = GameObject::CreateGameObject(doorEntry, this, doorPos, rot, 255, GO_STATE_READY);
if (doorGo)
{
_houseGameObjects[plotIndex] = doorGo->GetGUID();
TC_LOG_DEBUG("housing", "HousingMap::SpawnHouseForPlot: Door GO spawned - entry={} guid={} displayId={} at ({:.1f}, {:.1f}, {:.1f}) (house center: {:.1f}, {:.1f}, {:.1f}) for plot {}",
doorEntry, doorGo->GetGUID().ToString(), doorGo->GetDisplayId(), doorX, doorY, doorZ, x, y, z, plotIndex);
doorGo->SetFlag(GO_FLAG_NODESPAWN);
PhasingHandler::InitDbPhaseShift(doorGo->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
if (AddToMap(doorGo))
{
_houseGameObjects[plotIndex] = doorGo->GetGUID();
TC_LOG_DEBUG("housing", "HousingMap::SpawnHouseForPlot: Door GO spawned - entry={} guid={} "
"at ({:.1f}, {:.1f}, {:.1f}) (house center: {:.1f}, {:.1f}, {:.1f}) for plot {}",
doorEntry, doorGo->GetGUID().ToString(), doorX, doorY, doorZ, x, y, z, plotIndex);
}
else
{
TC_LOG_ERROR("housing", "HousingMap::SpawnHouseForPlot: Door GO AddToMap FAILED for plot {}", plotIndex);
delete doorGo;
doorGo = nullptr;
}
}
else
{
TC_LOG_ERROR("housing", "HousingMap::SpawnHouseForPlot: Door GO AddToMap FAILED for plot {}", plotIndex);
delete doorGo;
doorGo = nullptr;
TC_LOG_ERROR("housing", "HousingMap::SpawnHouseForPlot: CreateGameObject FAILED for door entry {} at plot {}", doorEntry, plotIndex);
}
}
else
{
TC_LOG_ERROR("housing", "HousingMap::SpawnHouseForPlot: CreateGameObject FAILED for door entry {} at plot {}", doorEntry, plotIndex);
TC_LOG_ERROR("housing", "HousingMap::SpawnHouseForPlot: Door GO template {} NOT FOUND for plot {}", doorEntry, plotIndex);
}
}
else
{
TC_LOG_ERROR("housing", "HousingMap::SpawnHouseForPlot: Door GO template {} NOT FOUND - door will not spawn for plot {}", doorEntry, plotIndex);
}
return doorGo;
}
@@ -1986,26 +2052,25 @@ void HousingMap::SpawnFullHouseMeshObjects(uint8 plotIndex, Position const& hous
QuaternionData const& houseRot, ObjectGuid houseGuid,
int32 exteriorComponentID, int32 houseExteriorWmoDataID,
int32 factionRestriction /*= NEIGHBORHOOD_FACTION_ALLIANCE*/,
FixtureOverrideMap const* fixtureOverrides /*= nullptr*/)
FixtureOverrideMap const* fixtureOverrides /*= nullptr*/,
RootOverrideMap const* rootOverrides /*= nullptr*/)
{
// === DATA-DRIVEN EXTERIOR SPAWNING ===
// Try to build the house from DB2 ExteriorComponent tree first.
// If the data-driven approach yields 0 meshes, fall back to hardcoded methods.
// Build the house from DB2 ExteriorComponent tree.
// 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).
//
// Root selection per type:
// 1. Check rootOverrides (player's explicit choice for that type)
// 2. Use coreExtCompID for the core type
// 3. Fall back to DB2 default (Flags & 0x1 IsDefault)
uint32 coreExtCompID = static_cast<uint32>(exteriorComponentID);
int32 groupID = sHousingMgr.GetGroupForComponent(coreExtCompID);
ExteriorComponentEntry const* coreComp = sExteriorComponentStore.LookupEntry(coreExtCompID);
if (coreComp && coreComp->ModelFileDataID > 0 && coreComp->HouseExteriorWmoDataID > 0)
{
// 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;
@@ -2026,29 +2091,30 @@ void HousingMap::SpawnFullHouseMeshObjects(uint8 plotIndex, Position const& hous
{
uint32 selectedCompID = 0;
if (type == coreComp->Type)
// 1. Check player's root overrides for this type
if (rootOverrides)
{
// For the base type, use the player's selected coreExtCompID
auto ovrItr = rootOverrides->find(type);
if (ovrItr != rootOverrides->end())
selectedCompID = ovrItr->second;
}
// 2. For the core type, use the player's selected coreExtCompID
if (!selectedCompID && type == coreComp->Type)
selectedCompID = coreExtCompID;
}
else
// 3. Fall back to DB2 default for this type + wmoDataID
if (!selectedCompID)
{
// For other types (roof, etc.), pick IsDefault or first available
// TODO: check fixture overrides for player-selected roof variant
for (uint32 id : compIDs)
{
ExteriorComponentEntry const* rc = sExteriorComponentStore.LookupEntry(id);
if (!rc) continue;
if (!selectedCompID)
selectedCompID = id; // fallback: first available
if (rc->Flags & 0x1)
{
selectedCompID = id; // prefer IsDefault
break;
}
}
uint32 defaultID = sHousingMgr.GetDefaultFixtureForType(type, wmoDataID);
if (defaultID)
selectedCompID = defaultID;
}
// 4. Last resort: first available in the list
if (!selectedCompID && !compIDs.empty())
selectedCompID = compIDs[0];
if (selectedCompID)
{
ExteriorComponentEntry const* selComp = sExteriorComponentStore.LookupEntry(selectedCompID);
@@ -2075,14 +2141,15 @@ void HousingMap::SpawnFullHouseMeshObjects(uint8 plotIndex, Position const& hous
return;
}
TC_LOG_WARN("housing", "HousingMap::SpawnFullHouseMeshObjects: Data-driven spawn "
"yielded 0 meshes for plot {} wmoDataID {} ? falling back to hardcoded",
plotIndex, wmoDataID);
TC_LOG_ERROR("housing", "HousingMap::SpawnFullHouseMeshObjects: Data-driven spawn "
"yielded 0 meshes for plot {} wmoDataID {} coreComp {} ? no DB2 data available",
plotIndex, wmoDataID, coreExtCompID);
return;
}
else if (!coreComp)
{
TC_LOG_DEBUG("housing", "HousingMap::SpawnFullHouseMeshObjects: ExteriorComponent {} not found "
"? using hardcoded spawn for plot {}", exteriorComponentID, plotIndex);
TC_LOG_ERROR("housing", "HousingMap::SpawnFullHouseMeshObjects: ExteriorComponent {} not found "
"? cannot spawn house for plot {}", exteriorComponentID, plotIndex);
}
// === HARDCODED FALLBACK ===
@@ -2321,8 +2388,6 @@ 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)
@@ -2332,7 +2397,9 @@ uint32 HousingMap::SpawnExtCompTree(uint8 plotIndex, uint32 extCompID,
if (!hook)
continue;
// Find what component attaches at this hook ? check player fixture overrides first
// Find what component attaches at this hook:
// 1. Check player's fixture overrides (hookID ? componentID)
// 2. Fall back to DB2 default: look up by hook's ComponentType + house's WmoDataID
ExteriorComponentEntry const* childComp = nullptr;
if (fixtureOverrides)
{
@@ -2341,12 +2408,16 @@ uint32 HousingMap::SpawnExtCompTree(uint8 plotIndex, uint32 extCompID,
childComp = sExteriorComponentStore.LookupEntry(overrideItr->second);
}
if (!childComp)
childComp = sHousingMgr.GetComponentAtHook(static_cast<int32>(hook->ID), extCompID);
{
uint32 defaultCompID = sHousingMgr.GetDefaultFixtureForType(
static_cast<uint8>(hook->ExteriorComponentTypeID),
static_cast<uint32>(houseExteriorWmoDataID));
if (defaultCompID)
childComp = sExteriorComponentStore.LookupEntry(defaultCompID);
}
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] : "",
@@ -2376,47 +2447,10 @@ 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);
}
}
// NOTE: ExteriorComponent.ParentComponentID links are color/dye variants of the same shape,
// NOT structural children. They share the same mesh with a different dye (Field_011).
// These are NOT spawned as additional meshes ? the player's fixture selection determines
// which variant is used, handled via GetRootComponentOverrides() / fixture overrides.
return count;
}
+9 -3
View File
@@ -60,11 +60,16 @@ public:
// When provided, SpawnExtCompTree uses these instead of the DB2 default component at each hook.
using FixtureOverrideMap = std::unordered_map<uint32 /*hookID*/, uint32 /*extCompID*/>;
// Root override map: componentType ? componentID from player's root fixture selections
// (e.g., player chose a specific roof variant). When provided, SpawnFullHouseMeshObjects
// uses these instead of the DB2 default root for that type.
using RootOverrideMap = std::unordered_map<uint8 /*componentType*/, uint32 /*compID*/>;
// House structure GO management
// Sniff-verified defaults: ExteriorComponentID=141 (Stucco Base), HouseExteriorWmoDataID=9 (Human theme)
GameObject* SpawnHouseForPlot(uint8 plotIndex, Position const* customPos,
int32 exteriorComponentID, int32 houseExteriorWmoDataID,
FixtureOverrideMap const* fixtureOverrides = nullptr);
FixtureOverrideMap const* fixtureOverrides = nullptr,
RootOverrideMap const* rootOverrides = nullptr);
void DespawnHouseForPlot(uint8 plotIndex);
GameObject* GetHouseGameObject(uint8 plotIndex);
int8 GetPlotIndexForHouseGO(ObjectGuid goGuid) const;
@@ -83,7 +88,8 @@ public:
QuaternionData const& houseRot, ObjectGuid houseGuid,
int32 exteriorComponentID, int32 houseExteriorWmoDataID,
int32 factionRestriction = NEIGHBORHOOD_FACTION_ALLIANCE,
FixtureOverrideMap const* fixtureOverrides = nullptr);
FixtureOverrideMap const* fixtureOverrides = nullptr,
RootOverrideMap const* rootOverrides = nullptr);
void SpawnHordeHouseMeshObjects(uint8 plotIndex, Position const& housePos,
QuaternionData const& houseRot, ObjectGuid houseGuid,
int32 exteriorComponentID, int32 houseExteriorWmoDataID);
+67 -75
View File
@@ -32,6 +32,7 @@
#include "Timer.h"
#include "World.h"
#include <algorithm>
#include <unordered_set>
namespace
{
@@ -1261,12 +1262,12 @@ int32 HousingMgr::GetTextureIdForComponentType(uint8 componentType) const
void HousingMgr::BuildExteriorComponentIndexes()
{
_hooksByExtComp.clear();
_extCompByHookId.clear();
_exitPointByExtComp.clear();
_groupByExtComp.clear();
_extCompsByGroup.clear();
_childrenByExtComp.clear();
_rootCompsByWmoDataId.clear();
_defaultFixtureByTypeWmo.clear();
// 1. Build hook index: which hooks are parented to each component
// Note: store iteration only yields a subset of entries due to DB2 ParentIndexField
@@ -1280,10 +1281,18 @@ void HousingMgr::BuildExteriorComponentIndexes()
}
// 1a. Build child index and root-by-WMO index from ExteriorComponent.
// ParentComponentID > 0 ? child of that component.
// ParentComponentID == 0 ? root component, indexed by HouseExteriorWmoDataID.
// ParentComponentID > 0 ? color/dye variant of that component.
// ParentComponentID == 0 ? base variant, and if the Type is a structural root,
// it's indexed by HouseExteriorWmoDataID for independent spawning.
//
// Structural root check: look up ExteriorComponentType by comp->Type.
// Only types with ParentComponentType == 0 are structural roots (Base=9, Roof=10).
// Types like Door(11), Window(12), Chimney(16) have ParentComponentType > 0
// and are spawned as hook children, NOT as independent roots.
//
// ExteriorComponent uses ParentIndexField (HouseExteriorWmoDataID), so use
// LookupEntry over GetNumRows() to reach all entries.
std::unordered_set<uint8> structuralRootTypes;
for (uint32 i = 0; i < sExteriorComponentStore.GetNumRows(); ++i)
{
ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(i);
@@ -1293,12 +1302,35 @@ void HousingMgr::BuildExteriorComponentIndexes()
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);
}
{
// Check if this component's Type is a structural root.
// ExteriorComponentType DB2: Base(9) and Roof(10) have ParentComponentType=0.
// All fixture types (Door=11, Window=12, etc.) have ParentComponentType > 0.
// Try DB2 lookup first; fall back to known root types if store is unreliable.
bool isStructuralRoot = false;
ExteriorComponentTypeEntry const* typeEntry = sExteriorComponentTypeStore.LookupEntry(comp->Type);
if (typeEntry)
isStructuralRoot = (typeEntry->ParentComponentType == 0);
else
isStructuralRoot = (comp->Type == 9 || comp->Type == 10); // Base, Roof
// 1b. Build group indexes from ExteriorComponentXGroup (needed by step 2)
if (isStructuralRoot)
{
_rootCompsByWmoDataId[comp->HouseExteriorWmoDataID].push_back(comp->ID);
structuralRootTypes.insert(comp->Type);
}
}
}
TC_LOG_DEBUG("housing", "HousingMgr: structural root types: ({})",
[&]() {
std::string s;
for (uint8 t : structuralRootTypes)
s += (s.empty() ? "" : ",") + std::to_string(t);
return s.empty() ? "none" : s;
}());
// 1c. Build group indexes from ExteriorComponentXGroup (for UI fixture panels)
for (ExteriorComponentXGroupEntry const* xg : sExteriorComponentXGroupStore)
{
if (!xg)
@@ -1309,79 +1341,39 @@ 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.
// 2. Build fixture resolution index: (componentType, wmoDataID) ? default component ID.
// For each hook on a parent component, the fixture that goes there is determined by:
// - Hook's ExteriorComponentTypeID (e.g., Door=11, Window=12, Chimney=16)
// - Parent component's HouseExteriorWmoDataID (e.g., 9=Human, 55=NightElf)
// The default fixture is the root component (ParentComponentID==0) with matching
// Type and WmoDataID that has Flags & 0x1 (IsDefault).
//
// 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)
// This replaces the old GroupXHook?Group?XGroup chain which was incorrect ?
// groups are for UI organization, not fixture resolution.
for (uint32 i = 0; i < sExteriorComponentStore.GetNumRows(); ++i)
{
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)
ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(i);
if (!comp || comp->ParentComponentID != 0 || comp->HouseExteriorWmoDataID == 0)
continue;
int32 hookType = hook->ExteriorComponentTypeID;
int32 parentCompID = hook->ExteriorComponentID;
int64 compositeKey = (int64(hookId) << 32) | uint32(parentCompID);
ExteriorComponentEntry const* bestComp = nullptr;
ExteriorComponentEntry const* fallbackComp = nullptr;
uint64 key = (uint64(comp->Type) << 32) | comp->HouseExteriorWmoDataID;
bool isDefault = (comp->Flags & 0x1) != 0;
// Find default component from linked groups
for (int32 groupID : groupList)
auto existing = _defaultFixtureByTypeWmo.find(key);
if (existing == _defaultFixtureByTypeWmo.end())
{
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;
// First component for this (type, wmo) ? insert it
_defaultFixtureByTypeWmo[key] = comp->ID;
}
if (!bestComp)
bestComp = fallbackComp;
if (bestComp)
else if (isDefault)
{
_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);
// This component is the default ? override any non-default already stored
_defaultFixtureByTypeWmo[key] = comp->ID;
}
}
TC_LOG_DEBUG("housing", "HousingMgr: Built _defaultFixtureByTypeWmo with {} entries", uint32(_defaultFixtureByTypeWmo.size()));
// 3. Build exit point index
for (ExteriorComponentExitPointEntry const* exitPt : sExteriorComponentExitPointStore)
{
@@ -1391,8 +1383,8 @@ void HousingMgr::BuildExteriorComponentIndexes()
}
TC_LOG_INFO("housing", "HousingMgr::BuildExteriorComponentIndexes: "
"hooks={} compByHook={} exitPoints={} groups={} compsInGroups={} parentChildren={} wmoRoots={}",
uint32(_hooksByExtComp.size()), uint32(_extCompByHookId.size()),
"hooks={} fixtureByTypeWmo={} exitPoints={} groups={} compsInGroups={} parentChildren={} wmoRoots={}",
uint32(_hooksByExtComp.size()), uint32(_defaultFixtureByTypeWmo.size()),
uint32(_exitPointByExtComp.size()), uint32(_groupByExtComp.size()),
uint32(_extCompsByGroup.size()), uint32(_childrenByExtComp.size()),
uint32(_rootCompsByWmoDataId.size()));
@@ -1405,11 +1397,11 @@ std::vector<ExteriorComponentHookEntry const*> const* HousingMgr::GetHooksOnComp
return itr != _hooksByExtComp.end() ? &itr->second : nullptr;
}
ExteriorComponentEntry const* HousingMgr::GetComponentAtHook(int32 hookID, uint32 parentCompID) const
uint32 HousingMgr::GetDefaultFixtureForType(uint8 componentType, uint32 wmoDataID) const
{
int64 compositeKey = (int64(hookID) << 32) | uint32(parentCompID);
auto itr = _extCompByHookId.find(compositeKey);
return itr != _extCompByHookId.end() ? itr->second : nullptr;
uint64 key = (uint64(componentType) << 32) | wmoDataID;
auto itr = _defaultFixtureByTypeWmo.find(key);
return itr != _defaultFixtureByTypeWmo.end() ? itr->second : 0;
}
ExteriorComponentExitPointEntry const* HousingMgr::GetExitPoint(uint32 extCompID) const
+8 -2
View File
@@ -330,13 +330,16 @@ public:
// ExteriorComponent indexed lookups
std::vector<ExteriorComponentHookEntry const*> const* GetHooksOnComponent(uint32 extCompID) 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;
// Fixture resolution: given a hook's component type and the house's WmoDataID,
// returns the default fixture component ID (Flags & 0x1, ParentComponentID == 0).
uint32 GetDefaultFixtureForType(uint8 componentType, uint32 wmoDataID) const;
// Find the first HouseRoom entry with visual components (not the base room 18)
uint32 GetDefaultVisualRoomEntry() const;
@@ -434,12 +437,15 @@ private:
// ExteriorComponent indexes
std::unordered_map<uint32 /*extCompID*/, std::vector<ExteriorComponentHookEntry const*>> _hooksByExtComp;
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;
// Fixture resolution: (componentType, wmoDataID) ? default component ID
// Key = (uint64(componentType) << 32) | wmoDataID
std::unordered_map<uint64, uint32> _defaultFixtureByTypeWmo;
};
#define sHousingMgr HousingMgr::Instance()