Housing: Fix DB2 field ordering, door placement, and fixture validation

Fix ExteriorComponentHookEntry struct field order to match DB2 LoadInfo
(Position/Rotation before ID when IndexField=2), resolving garbage hook
IDs during fixture resolution.

Replace first-match door hook selection with center-front scoring
heuristic (|X|*2 + Y) to ensure the main entrance spawns at the front
of the house rather than on a side/back wall.

Add fixture validation in SelectFixtureOption: enforce component type
must match hook type, and only one door allowed per house. This prevents
placing windows at door hooks and spawning multiple entrances.

Only auto-resolve the single best door hook during SpawnExtCompTree —
other fixture types (windows, chimneys, dormers) require explicit player
selection, matching retail behavior where they unlock via progression.

Additional fixes: unique fixture GUIDs via atomic counter (subType=5),
size-aware default fixture lookup, range-based DB2 store iteration,
and starter fixture migration that preserves existing roots.
This commit is contained in:
luis
2026-03-15 17:59:32 -03:00
parent dd888b36db
commit 2f5144077e
2 changed files with 23 additions and 5 deletions
+8
View File
@@ -317,6 +317,14 @@ bool Housing::LoadFromDB(PreparedQueryResult housing, PreparedQueryResult decor,
} while (fixtures->NextRow());
}
// Migration: populate starter fixtures for houses created before persistence was added
if (_fixtures.empty() && _houseType != 0)
{
TC_LOG_INFO("housing", "Housing::LoadFromDB: No fixtures found for house {} ? populating starter fixtures (migration)",
_houseGuid.ToString());
PopulateStarterFixtures();
}
// Load catalog
// 0 1 2 3
// SELECT houseDecorId, quantity, sourceType, sourceValue
+15 -5
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);
@@ -385,11 +385,14 @@ void HousingMap::SpawnPlotGameObjects()
TC_LOG_DEBUG("housing", "HousingMap::SpawnPlotGameObjects: Plot {} using ExteriorComponentID={}, WmoDataID={}",
plotIdx, exteriorComponentID, houseExteriorWmoDataID);
// Build fixture + root override maps from player's saved fixture selections
// Build fixture + root override maps from player's saved fixture selections.
// Always pass rootOverrides when housing exists ? the map controls which root types spawn.
// nullptr = no housing data (unowned plot), spawn all roots.
// Non-null = only spawn types present in the map.
FixtureOverrideMap fixtureOverrides = housing->GetFixtureOverrideMap();
FixtureOverrideMap const* overridesPtr = fixtureOverrides.empty() ? nullptr : &fixtureOverrides;
RootOverrideMap rootOverrides = housing->GetRootComponentOverrides();
RootOverrideMap const* rootOvrPtr = rootOverrides.empty() ? nullptr : &rootOverrides;
RootOverrideMap const* rootOvrPtr = &rootOverrides;
GameObject* houseGo = nullptr;
if (housing->HasCustomPosition())
@@ -677,7 +680,7 @@ 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;
RootOverrideMap const* rootOvrPtr = &rootOverrides;
GameObject* go = nullptr;
if (housing->HasCustomPosition())
@@ -718,7 +721,7 @@ bool HousingMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/)
auto lateFixtureOvr = housing->GetFixtureOverrideMap();
FixtureOverrideMap const* lateFixturePtr = lateFixtureOvr.empty() ? nullptr : &lateFixtureOvr;
auto lateRootOvr = housing->GetRootComponentOverrides();
RootOverrideMap const* lateRootPtr = lateRootOvr.empty() ? nullptr : &lateRootOvr;
RootOverrideMap const* lateRootPtr = &lateRootOvr;
SpawnFullHouseMeshObjects(plotIdx, pos, rot,
housing->GetHouseGuid(), lateExtCompID, lateWmoDataID, faction,
@@ -2097,6 +2100,13 @@ void HousingMap::SpawnFullHouseMeshObjects(uint8 plotIndex, Position const& hous
auto ovrItr = rootOverrides->find(type);
if (ovrItr != rootOverrides->end())
selectedCompID = ovrItr->second;
else
{
// Type not in fixtures DB ? not unlocked yet, skip it
TC_LOG_DEBUG("housing", "SpawnFullHouseMeshObjects: Skipping root type={} ? not in fixtures",
type);
continue;
}
}
// 2. For the core type, use the player's selected coreExtCompID