From ddb75df3712f9fb614008c00531cded4cd06e41c Mon Sep 17 00:00:00 2001 From: luis Date: Mon, 16 Mar 2026 20:14:57 -0300 Subject: [PATCH] Housing: Re-CREATE all fixture entities after rebuild to fix hook recognition --- .../game/Entities/MeshObject/MeshObject.cpp | 1 + .../game/Entities/MeshObject/MeshObject.h | 2 + .../game/Entities/Object/BaseEntity.cpp | 6 +- src/server/game/Entities/Player/Player.cpp | 2 +- src/server/game/Handlers/HousingHandler.cpp | 44 ++- src/server/game/Housing/Housing.cpp | 275 ++++++++++++++++-- src/server/game/Housing/HousingMap.cpp | 130 ++++++--- src/server/game/Housing/HousingMgr.cpp | 50 +++- src/server/game/Housing/HousingMgr.h | 12 +- src/server/scripts/World/at_housing_plot.cpp | 39 ++- 10 files changed, 441 insertions(+), 120 deletions(-) diff --git a/src/server/game/Entities/MeshObject/MeshObject.cpp b/src/server/game/Entities/MeshObject/MeshObject.cpp index 7fa367566..9ce7c3fa4 100644 --- a/src/server/game/Entities/MeshObject/MeshObject.cpp +++ b/src/server/game/Entities/MeshObject/MeshObject.cpp @@ -122,6 +122,7 @@ bool MeshObject::Create(Map* map, Position const& pos, QuaternionData const& rot // Store movement block data (used by BaseEntity::BuildCreateUpdateBlockMovement) _attachParentGUID = attachParent; + _positionLocalSpace = pos; // local-space offset (for movement block MeshObject section) _rotationLocalSpace = rotation; _scaleLocalSpace = scale; _attachmentFlags = attachFlags; diff --git a/src/server/game/Entities/MeshObject/MeshObject.h b/src/server/game/Entities/MeshObject/MeshObject.h index e35664d5e..0bddaef17 100644 --- a/src/server/game/Entities/MeshObject/MeshObject.h +++ b/src/server/game/Entities/MeshObject/MeshObject.h @@ -52,6 +52,7 @@ public: int32 GetFileDataID() const { return m_meshObjectData->FileDataID; } ObjectGuid const& GetAttachParentGUID() const { return _attachParentGUID; } QuaternionData const& GetLocalRotation() const { return _rotationLocalSpace; } + Position const& GetLocalPosition() const { return _positionLocalSpace; } float GetLocalScale() const { return _scaleLocalSpace; } uint8 GetAttachmentFlags() const { return _attachmentFlags; } bool IsExteriorRoot() const { return _isExteriorRoot; } @@ -113,6 +114,7 @@ private: // Movement block data (serialized in BaseEntity::BuildCreateUpdateBlockMovement) ObjectGuid _attachParentGUID; + Position _positionLocalSpace; // local-space offset from parent (for movement block MeshObject section) QuaternionData _rotationLocalSpace; float _scaleLocalSpace = 1.0f; uint8 _attachmentFlags = 0; diff --git a/src/server/game/Entities/Object/BaseEntity.cpp b/src/server/game/Entities/Object/BaseEntity.cpp index c9d1613fd..026c3fd1a 100644 --- a/src/server/game/Entities/Object/BaseEntity.cpp +++ b/src/server/game/Entities/Object/BaseEntity.cpp @@ -453,7 +453,11 @@ void BaseEntity::BuildMovementUpdate(ByteBuffer& data, CreateObjectBits flags, P { MeshObject const* meshObj = static_cast(this); data << meshObj->GetAttachParentGUID(); - data << TaggedPosition(meshObj->GetPositionX(), meshObj->GetPositionY(), meshObj->GetPositionZ()); + // Use the stored local-space position (offset from parent), NOT GetPositionX/Y/Z() + // which returns the parent's world position (set by Relocate for grid placement). + // The client uses this to position the child mesh relative to its parent entity. + Position const& localPos = meshObj->GetLocalPosition(); + data << TaggedPosition(localPos.GetPositionX(), localPos.GetPositionY(), localPos.GetPositionZ()); QuaternionData const& rot = meshObj->GetLocalRotation(); data << rot.x << rot.y << rot.z << rot.w; data << meshObj->GetLocalScale(); diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 3dffdc8e4..4dd0c7354 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -25815,7 +25815,7 @@ void Player::SendInitialPacketsAfterAddToMap() statusResponse.OwnerPlayerGuid = GetGUID(); statusResponse.NeighborhoodGuid = housing->GetNeighborhoodGuid(); statusResponse.Status = 0; - statusResponse.FlagByte = 0xE0; // bit7=houseEditing, bit6=plotEntry, bit5=houseEntry (sniff-verified owner value) + statusResponse.FlagByte = 0xE0; // bit7=houseEditing, bit6=plotEntry, bit5=houseEntry } // No house: all fields stay at defaults (empty GUIDs, Status=0, FlagByte=0). SendDirectMessage(statusResponse.Write()); diff --git a/src/server/game/Handlers/HousingHandler.cpp b/src/server/game/Handlers/HousingHandler.cpp index 91f0f3dc7..b57a30088 100644 --- a/src/server/game/Handlers/HousingHandler.cpp +++ b/src/server/game/Handlers/HousingHandler.cpp @@ -266,10 +266,12 @@ void WorldSession::HandleHouseExteriorSetHousePosition(WorldPackets::Housing::Ho // Respawn at new position with current exterior component, house type, and fixture selections Position newPos(posX, posY, posZ, facing); auto fixtureOverrides = housing->GetFixtureOverrideMap(); + auto rootOverrides = housing->GetRootComponentOverrides(); housingMap->SpawnHouseForPlot(plotIndex, &newPos, static_cast(housing->GetCoreExteriorComponentID()), static_cast(housing->GetHouseType()), - fixtureOverrides.empty() ? nullptr : &fixtureOverrides); + fixtureOverrides.empty() ? nullptr : &fixtureOverrides, + rootOverrides.empty() ? nullptr : &rootOverrides); housingMap->SpawnAllDecorForPlot(plotIndex, housing); } @@ -1636,12 +1638,14 @@ void WorldSession::HandleHousingFixtureSetCoreFixture(WorldPackets::Housing::Hou { uint8 plotIndex = housing->GetPlotIndex(); auto fixtureOverrides = housing->GetFixtureOverrideMap(); + auto rootOverrides = housing->GetRootComponentOverrides(); housingMap->DespawnAllDecorForPlot(plotIndex); housingMap->DespawnHouseForPlot(plotIndex); housingMap->SpawnHouseForPlot(plotIndex, nullptr, static_cast(housing->GetCoreExteriorComponentID()), static_cast(housing->GetHouseType()), - fixtureOverrides.empty() ? nullptr : &fixtureOverrides); + fixtureOverrides.empty() ? nullptr : &fixtureOverrides, + rootOverrides.empty() ? nullptr : &rootOverrides); housingMap->SpawnAllDecorForPlot(plotIndex, housing); } @@ -1703,10 +1707,9 @@ void WorldSession::HandleHousingFixtureCreateFixture(WorldPackets::Housing::Hous HousingResult result = housing->SelectFixtureOption(hookID, componentID); - WorldPackets::Housing::HousingFixtureCreateFixtureResponse response; - response.Result = static_cast(result); - SendPacket(response.Write()); - + // Spawn the fixture BEFORE sending the response so we can populate FixtureGuid. + // The client's CREATE_FIXTURE_RESPONSE handler uses this GUID to identify the new entity. + ObjectGuid newFixtureGuid; if (result == HOUSING_RESULT_SUCCESS) { WorldPackets::Housing::AccountExteriorFixtureCollectionUpdate collectionUpdate; @@ -1725,10 +1728,21 @@ void WorldSession::HandleHousingFixtureCreateFixture(WorldPackets::Housing::Hous housingMap->DespawnSingleMeshObject(plotIndex, oldMesh->GetGUID()); } - housingMap->SpawnFixtureAtHook(plotIndex, hookID, componentID, + MeshObject* newMesh = housingMap->SpawnFixtureAtHook(plotIndex, hookID, componentID, housing->GetHouseGuid(), static_cast(housing->GetHouseType()), player); + if (newMesh) + newFixtureGuid = newMesh->GetFixtureGuid(); } + } + // Send response with the fixture's Housing GUID (empty on failure) + WorldPackets::Housing::HousingFixtureCreateFixtureResponse response; + response.Result = static_cast(result); + response.FixtureGuid = newFixtureGuid; + SendPacket(response.Write()); + + if (result == HOUSING_RESULT_SUCCESS) + { // Sniff-verified: UPDATE_OBJECT (~279B) follows the response SendFixtureUpdateObject(player, housing); } @@ -1863,12 +1877,14 @@ void WorldSession::HandleHousingFixtureSetHouseSize(WorldPackets::Housing::Housi { uint8 plotIndex = housing->GetPlotIndex(); auto fixtureOverrides = housing->GetFixtureOverrideMap(); + auto rootOverrides = housing->GetRootComponentOverrides(); housingMap->DespawnAllDecorForPlot(plotIndex); housingMap->DespawnHouseForPlot(plotIndex); housingMap->SpawnHouseForPlot(plotIndex, nullptr, static_cast(housing->GetCoreExteriorComponentID()), static_cast(housing->GetHouseType()), - fixtureOverrides.empty() ? nullptr : &fixtureOverrides); + fixtureOverrides.empty() ? nullptr : &fixtureOverrides, + rootOverrides.empty() ? nullptr : &rootOverrides); housingMap->SpawnAllDecorForPlot(plotIndex, housing); } @@ -1934,12 +1950,14 @@ void WorldSession::HandleHousingFixtureSetHouseType(WorldPackets::Housing::Housi { uint8 plotIndex = housing->GetPlotIndex(); auto fixtureOverrides = housing->GetFixtureOverrideMap(); + auto rootOverrides = housing->GetRootComponentOverrides(); housingMap->DespawnAllDecorForPlot(plotIndex); housingMap->DespawnHouseForPlot(plotIndex); housingMap->SpawnHouseForPlot(plotIndex, nullptr, static_cast(housing->GetCoreExteriorComponentID()), static_cast(wmoDataID), - fixtureOverrides.empty() ? nullptr : &fixtureOverrides); + fixtureOverrides.empty() ? nullptr : &fixtureOverrides, + rootOverrides.empty() ? nullptr : &rootOverrides); housingMap->SpawnAllDecorForPlot(plotIndex, housing); } @@ -3662,7 +3680,7 @@ void WorldSession::HandleHousingHouseStatus(WorldPackets::Housing::HousingHouseS response.OwnerPlayerGuid = player->GetGUID(); response.NeighborhoodGuid = ownHousing->GetNeighborhoodGuid(); response.Status = ownHousing->IsInInterior() ? 1 : 0; - response.FlagByte = 0xE0; // bit7=houseEditing, bit6=plotEntry, bit5=houseEntry (sniff-verified) + response.FlagByte = 0xE0; // bit7=houseEditing, bit6=plotEntry, bit5=houseEntry } } else if (ownHousing) @@ -3673,7 +3691,7 @@ void WorldSession::HandleHousingHouseStatus(WorldPackets::Housing::HousingHouseS response.OwnerPlayerGuid = player->GetGUID(); response.NeighborhoodGuid = ownHousing->GetNeighborhoodGuid(); response.Status = ownHousing->IsInInterior() ? 1 : 0; - response.FlagByte = 0xE0; // bit7=houseEditing, bit6=plotEntry, bit5=houseEntry (sniff-verified) + response.FlagByte = 0xE0; // bit7=houseEditing, bit6=plotEntry, bit5=houseEntry } // No house and not on a plot: all fields stay at defaults (empty GUIDs, Status=0). WorldPacket const* statusPkt = response.Write(); @@ -4437,12 +4455,14 @@ void WorldSession::HandleHousingFixtureCreateBasicHouse(WorldPackets::Housing::H { uint8 plotIndex = housing->GetPlotIndex(); auto fixtureOverrides = housing->GetFixtureOverrideMap(); + auto rootOverrides = housing->GetRootComponentOverrides(); housingMap->DespawnAllDecorForPlot(plotIndex); housingMap->DespawnHouseForPlot(plotIndex); housingMap->SpawnHouseForPlot(plotIndex, nullptr, static_cast(housing->GetCoreExteriorComponentID()), static_cast(styleID), - fixtureOverrides.empty() ? nullptr : &fixtureOverrides); + fixtureOverrides.empty() ? nullptr : &fixtureOverrides, + rootOverrides.empty() ? nullptr : &rootOverrides); housingMap->SpawnAllDecorForPlot(plotIndex, housing); } } diff --git a/src/server/game/Housing/Housing.cpp b/src/server/game/Housing/Housing.cpp index 734834035..f683c2432 100644 --- a/src/server/game/Housing/Housing.cpp +++ b/src/server/game/Housing/Housing.cpp @@ -315,13 +315,41 @@ bool Housing::LoadFromDB(PreparedQueryResult housing, PreparedQueryResult decor, fixture.OptionId = fields[1].GetUInt32(); } while (fixtures->NextRow()); + + // Log all loaded fixtures for debugging + for (auto const& [pointId, fix] : _fixtures) + { + TC_LOG_INFO("housing", "Housing::LoadFromDB: Fixture pointId={} optionId={}", fix.FixturePointId, fix.OptionId); + } } - // Migration: populate starter fixtures for houses created before persistence was added - if (_fixtures.empty() && _houseType != 0) + // Migration: populate starter fixtures for houses created before persistence was added. + // Also handles existing houses that have fixtures but are missing starter roots (Base/Roof) or door. + bool hasBaseRoot = false, hasRoofRoot = false, hasDoor = false; + for (auto const& [pointId, fix] : _fixtures) { - TC_LOG_INFO("housing", "Housing::LoadFromDB: No fixtures found for house {} ? populating starter fixtures (migration)", - _houseGuid.ToString()); + if (fix.OptionId == 0) + { + ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(fix.FixturePointId); + if (!comp) + continue; + if (_houseType != 0 && comp->HouseExteriorWmoDataID != static_cast(_houseType)) + continue; + if (comp->Type == HOUSING_FIXTURE_TYPE_BASE) hasBaseRoot = true; + if (comp->Type == HOUSING_FIXTURE_TYPE_ROOF) hasRoofRoot = true; + } + else + { + ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(fix.OptionId); + if (comp && comp->Type == HOUSING_FIXTURE_TYPE_DOOR) + hasDoor = true; + } + } + + if ((!hasBaseRoot || !hasRoofRoot || !hasDoor) && _houseType != 0) + { + TC_LOG_INFO("housing", "Housing::LoadFromDB: Missing starter fixtures (base={}, roof={}, door={}) for house {} ? populating (migration)", + hasBaseRoot, hasRoofRoot, hasDoor, _houseGuid.ToString()); PopulateStarterFixtures(); } @@ -1335,11 +1363,11 @@ HousingResult Housing::PlaceRoom(uint32 roomEntryId, uint32 slotIndex, uint32 or _owner->GetName(), roomEntryId, slotIndex, _houseGuid.ToString(), _roomWeightUsed, GetMaxRoomBudget()); - // Account-level notification: room instance added + // Account-level notification: room collection update if (_owner->GetSession()) { - WorldPackets::Housing::AccountHousingRoomAdded notif; - notif.RoomGuid = roomGuid; + WorldPackets::Housing::AccountRoomCollectionUpdate notif; + notif.RoomID = roomEntryId; _owner->GetSession()->SendPacket(notif.Write()); } @@ -1563,11 +1591,11 @@ HousingResult Housing::ApplyRoomTheme(ObjectGuid roomGuid, uint32 themeSetId, st TC_LOG_DEBUG("housing", "Housing::ApplyRoomTheme: Player {} applied theme {} to room {} ({} components) in house {}", _owner->GetName(), themeSetId, roomGuid.ToString(), componentIds.size(), _houseGuid.ToString()); - // Account-level notification: theme applied + // Account-level notification: theme collection update if (_owner->GetSession()) { - WorldPackets::Housing::AccountHousingThemeAdded notif; - notif.ThemeGuid = roomGuid; + WorldPackets::Housing::AccountRoomThemeCollectionUpdate notif; + notif.ThemeID = themeSetId; _owner->GetSession()->SendPacket(notif.Write()); } @@ -1603,11 +1631,11 @@ HousingResult Housing::ApplyRoomWallpaper(ObjectGuid roomGuid, uint32 wallpaperI TC_LOG_DEBUG("housing", "Housing::ApplyRoomWallpaper: Player {} applied wallpaper {} (material {}) to room {} ({} components) in house {}", _owner->GetName(), wallpaperId, materialId, roomGuid.ToString(), componentIds.size(), _houseGuid.ToString()); - // Account-level notification: wallpaper/material texture changed + // Account-level notification: material collection update if (_owner->GetSession()) { - WorldPackets::Housing::AccountHousingRoomComponentTextureAdded notif; - notif.TextureGuid = roomGuid; + WorldPackets::Housing::AccountRoomMaterialCollectionUpdate notif; + notif.MaterialID = materialId; _owner->GetSession()->SendPacket(notif.Write()); } @@ -1671,6 +1699,86 @@ HousingResult Housing::SelectFixtureOption(uint32 fixturePointId, uint32 optionI if (_houseGuid.IsEmpty()) return HOUSING_RESULT_HOUSE_NOT_FOUND; + // Root fixture selections (optionId == 0) use componentID as fixturePointId ? skip hook validation + if (optionId != 0) + { + // Validate hook exists in DB2 + ExteriorComponentHookEntry const* hookEntry = sExteriorComponentHookStore.LookupEntry(fixturePointId); + if (!hookEntry) + { + TC_LOG_DEBUG("housing", "SelectFixtureOption: hookID {} not found in DB2", fixturePointId); + return HOUSING_RESULT_FIXTURE_NOT_FOUND; + } + + // Validate component exists in DB2 + ExteriorComponentEntry const* compEntry = sExteriorComponentStore.LookupEntry(optionId); + if (!compEntry) + { + TC_LOG_DEBUG("housing", "SelectFixtureOption: componentID {} not found in DB2", optionId); + return HOUSING_RESULT_FIXTURE_NOT_FOUND; + } + + // Validate component type matches hook's expected type + if (compEntry->Type != hookEntry->ExteriorComponentTypeID) + { + TC_LOG_DEBUG("housing", "SelectFixtureOption: type mismatch ? component {} type {} vs hook {} expected type {}", + optionId, compEntry->Type, fixturePointId, hookEntry->ExteriorComponentTypeID); + return HOUSING_RESULT_GENERIC_FAILURE; + } + + // Enforce one door per base component: if placing a door, check no other hook already has one + if (compEntry->Type == HOUSING_FIXTURE_TYPE_DOOR) + { + for (auto const& [pointId, fixture] : _fixtures) + { + if (pointId == fixturePointId || fixture.OptionId == 0) + continue; + ExteriorComponentEntry const* existingComp = sExteriorComponentStore.LookupEntry(fixture.OptionId); + if (existingComp && existingComp->Type == HOUSING_FIXTURE_TYPE_DOOR) + { + TC_LOG_DEBUG("housing", "SelectFixtureOption: door already exists at hook {} (comp {}), rejecting new door at hook {}", + pointId, fixture.OptionId, fixturePointId); + return HOUSING_RESULT_GENERIC_FAILURE; + } + } + } + } + else + { + // Root fixture (optionId == 0): fixturePointId is a componentID. + // Remove any existing root fixture of the SAME type to prevent accumulation. + // E.g., switching Base from Stucco(142) to Cottage(3797) must remove the old 142 entry. + ExteriorComponentEntry const* newComp = sExteriorComponentStore.LookupEntry(fixturePointId); + if (newComp) + { + uint8 newType = newComp->Type; + std::vector toRemove; + for (auto const& [pointId, fixture] : _fixtures) + { + if (fixture.OptionId != 0 || pointId == fixturePointId) + continue; + ExteriorComponentEntry const* oldComp = sExteriorComponentStore.LookupEntry(fixture.FixturePointId); + if (oldComp && oldComp->Type == newType) + { + TC_LOG_INFO("housing", "SelectFixtureOption: replacing root type {} ? removing old comp {} in favor of new comp {}", + newType, pointId, fixturePointId); + toRemove.push_back(pointId); + } + } + for (uint32 oldKey : toRemove) + { + _fixtures.erase(oldKey); + // Delete old entry from DB + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_HOUSING_FIXTURE_SINGLE); + stmt->setUInt64(0, _owner->GetGUID().GetCounter()); + stmt->setUInt32(1, oldKey); + CharacterDatabase.Execute(stmt); + if (_fixtureWeightUsed > 0) + --_fixtureWeightUsed; + } + } + } + bool isNew = _fixtures.find(fixturePointId) == _fixtures.end(); if (isNew && _fixtures.size() >= MAX_HOUSING_FIXTURES_PER_HOUSE) return HOUSING_RESULT_FIXTURE_NOT_FOUND; @@ -1705,11 +1813,11 @@ HousingResult Housing::SelectFixtureOption(uint32 fixturePointId, uint32 optionI _owner->GetName(), fixturePointId, optionId, _houseGuid.ToString(), _fixtureWeightUsed, GetMaxFixtureBudget()); - // Account-level notification: new fixture added + // Account-level notification: fixture collection update if (isNew && _owner->GetSession()) { - WorldPackets::Housing::AccountHousingFixtureAdded notif; - notif.FixtureGuid = _houseGuid; + WorldPackets::Housing::AccountExteriorFixtureCollectionUpdate notif; + notif.FixtureID = optionId; _owner->GetSession()->SendPacket(notif.Write()); } @@ -1795,8 +1903,9 @@ std::unordered_map Housing::GetFixtureOverrideMap() const std::unordered_map 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.). + // Core fixtures (OptionId == 0) represent the player's choice for a structural root type. + // These include both base variants (ParentComponentID == 0) and color/style variants + // (ParentComponentID != 0) ? color variants are valid selections via SetCoreFixture. std::unordered_map result; for (auto const& [pointId, fixture] : _fixtures) @@ -1805,13 +1914,33 @@ std::unordered_map Housing::GetRootComponentOverrides() const continue; ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(fixture.FixturePointId); - if (!comp || comp->ParentComponentID != 0) + if (!comp) + { + TC_LOG_DEBUG("housing", "GetRootComponentOverrides: fixturePointId={} ? DB2 lookup failed", fixture.FixturePointId); continue; + } + // Only structural root types (Base=9, Roof=10) are valid here. + // Fixture types (Door=11, Window=12, etc.) stored with OptionId=0 would be invalid. + if (comp->Type != HOUSING_FIXTURE_TYPE_BASE && comp->Type != HOUSING_FIXTURE_TYPE_ROOF) + { + TC_LOG_DEBUG("housing", "GetRootComponentOverrides: comp={} type={} ? not a structural root type, skipping", + comp->ID, comp->Type); + continue; + } if (_houseType != 0 && comp->HouseExteriorWmoDataID != static_cast(_houseType)) + { + TC_LOG_DEBUG("housing", "GetRootComponentOverrides: comp={} type={} ? wmo={} != houseType={} (wrong style)", + comp->ID, comp->Type, comp->HouseExteriorWmoDataID, _houseType); continue; + } result[comp->Type] = fixture.FixturePointId; + TC_LOG_DEBUG("housing", "GetRootComponentOverrides: type={} ? comp={} (wmo={}, parentComp={})", + comp->Type, fixture.FixturePointId, comp->HouseExteriorWmoDataID, comp->ParentComponentID); } + + TC_LOG_INFO("housing", "GetRootComponentOverrides: {} types resolved from {} fixtures (houseType={})", + uint32(result.size()), uint32(_fixtures.size()), _houseType); return result; } @@ -2352,17 +2481,39 @@ void Housing::PopulateStarterFixtures() // Starter house = Base(9) + Roof(10) as root components. // Door(11) auto-resolves from hook system via GetDefaultFixtureForType. // Root components are stored as { FixturePointId = componentID, OptionId = 0 }. + // Only add types that don't already have a valid root in _fixtures. static constexpr uint8 starterTypes[] = { HOUSING_FIXTURE_TYPE_BASE, HOUSING_FIXTURE_TYPE_ROOF }; + // Determine which root types already exist + std::unordered_set existingRootTypes; + for (auto const& [pointId, fix] : _fixtures) + { + if (fix.OptionId != 0) + continue; + ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(fix.FixturePointId); + if (!comp) + continue; + if (_houseType != 0 && comp->HouseExteriorWmoDataID != static_cast(_houseType)) + continue; + existingRootTypes.insert(comp->Type); + } + uint64 ownerGuid = _owner->GetGUID().GetCounter(); for (uint8 fixtureType : starterTypes) { - uint32 compID = sHousingMgr.GetDefaultFixtureForType(fixtureType, _houseType); + if (existingRootTypes.count(fixtureType)) + { + TC_LOG_INFO("housing", "Housing::PopulateStarterFixtures: type={} already has a root ? skipping", + fixtureType); + continue; + } + + uint32 compID = sHousingMgr.GetDefaultFixtureForType(fixtureType, _houseType, _houseSize); if (!compID) { - TC_LOG_ERROR("housing", "Housing::PopulateStarterFixtures: No default component for type={} wmo={} ? skipping", - fixtureType, _houseType); + TC_LOG_ERROR("housing", "Housing::PopulateStarterFixtures: No default component for type={} wmo={} size={} ? skipping", + fixtureType, _houseType, _houseSize); continue; } @@ -2382,4 +2533,84 @@ void Housing::PopulateStarterFixtures() TC_LOG_INFO("housing", "Housing::PopulateStarterFixtures: Added type={} compID={} wmo={} for player {}", fixtureType, compID, _houseType, _owner->GetName()); } + + // --- Starter door --- + // Every new house starts with a door at the first door hook on the base component. + // Check if a door fixture already exists (any hook-based fixture with a door component). + bool hasDoor = false; + for (auto const& [pointId, fix] : _fixtures) + { + if (fix.OptionId == 0) + continue; + ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(fix.OptionId); + if (comp && comp->Type == HOUSING_FIXTURE_TYPE_DOOR) + { + hasDoor = true; + break; + } + } + + if (!hasDoor) + { + // Find the base component to get its door hooks + uint32 baseCompID = 0; + for (auto const& [pointId, fix] : _fixtures) + { + if (fix.OptionId != 0) + continue; + ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(fix.FixturePointId); + if (comp && comp->Type == HOUSING_FIXTURE_TYPE_BASE) + { + baseCompID = fix.FixturePointId; + break; + } + } + + if (baseCompID) + { + auto const* hooks = sHousingMgr.GetHooksOnComponent(baseCompID); + if (hooks) + { + // Find the first door hook (ExteriorComponentTypeID == 11) + uint32 doorHookID = 0; + for (ExteriorComponentHookEntry const* hook : *hooks) + { + if (hook && hook->ExteriorComponentTypeID == HOUSING_FIXTURE_TYPE_DOOR) + { + doorHookID = hook->ID; + break; + } + } + + if (doorHookID) + { + uint32 doorCompID = sHousingMgr.GetDefaultFixtureForType(HOUSING_FIXTURE_TYPE_DOOR, _houseType, _houseSize); + if (doorCompID) + { + Fixture& doorFixture = _fixtures[doorHookID]; + doorFixture.FixturePointId = doorHookID; + doorFixture.OptionId = doorCompID; + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_HOUSING_FIXTURES); + uint8 index = 0; + stmt->setUInt64(index++, ownerGuid); + stmt->setUInt32(index++, doorHookID); + stmt->setUInt32(index++, doorCompID); + CharacterDatabase.Execute(stmt); + + TC_LOG_INFO("housing", "Housing::PopulateStarterFixtures: Added starter door compID={} at hookID={} for player {}", + doorCompID, doorHookID, _owner->GetName()); + } + else + { + TC_LOG_ERROR("housing", "Housing::PopulateStarterFixtures: No default door component for wmo={} size={}", _houseType, _houseSize); + } + } + else + { + TC_LOG_ERROR("housing", "Housing::PopulateStarterFixtures: No door hooks found on base component {}", baseCompID); + } + } + } + } } diff --git a/src/server/game/Housing/HousingMap.cpp b/src/server/game/Housing/HousingMap.cpp index 420ad90e6..ea74c7d7e 100644 --- a/src/server/game/Housing/HousingMap.cpp +++ b/src/server/game/Housing/HousingMap.cpp @@ -19,7 +19,9 @@ #include "Account.h" #include "HousingPlayerHouseEntity.h" #include +#include #include +#include #include #include "AreaTrigger.h" #include "EventProcessor.h" @@ -882,34 +884,55 @@ bool HousingMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/) playerGuid.ToString()); } - // Re-CREATE the player's exterior root MeshObject so the client's - // Tag_HouseExteriorRoot singleton (qword_7FF72A4CC368) points to THIS - // plot's root rather than whichever plot happened to be created last - // during SpawnPlotGameObjects(). The client's fragment 225 create - // handler overwrites the singleton on each CREATE ? we exploit this - // to guarantee our root is the active one before any fixture edits. + // Send SMSG_HOUSING_FIXTURE_CREATE_BASIC_HOUSE_RESPONSE with Result=0. + // This triggers the client's fixture frame initialization: the handler + // calls teardown + rebuild, which sets the fixture manager's house GUID + // (state+96/+104) from the NeighborhoodSystem. This MUST come BEFORE any + // fixture entity CREATEs, because the CREATE callback compares each + // entity's FHousingFixture_C::HouseGUID against state+96/+104 ? if they + // don't match, the entity is silently skipped and the hook shows "None". + { + WorldPackets::Housing::HousingFixtureCreateBasicHouseResponse fixtureInit; + fixtureInit.Result = static_cast(HOUSING_RESULT_SUCCESS); + p->SendDirectMessage(fixtureInit.Write()); + TC_LOG_DEBUG("housing", "HousingMap deferred ENTER_PLOT: Sent CREATE_BASIC_HOUSE_RESPONSE (fixture init) for plot {}", + deferredPlotIndex); + } + + // Re-CREATE ALL fixture MeshObjects for this plot AFTER the rebuild. + // The rebuild (triggered by CREATE_BASIC_HOUSE_RESPONSE above) sets the + // fixture manager's house GUID. The CREATE callback then compares each + // entity's HouseGUID against it ? if they match, it registers the entity + // at its hook point. Without this re-CREATE, entities that were already + // sent during the initial map load are never re-processed. + // (Same pattern as the decor fix: re-CREATE after the system is ready.) { auto const& meshMap = hMap->GetPlotMeshObjects(); auto meshItr = meshMap.find(deferredPlotIndex); if (meshItr != meshMap.end()) { + UpdateData fixtureUpdate(p->GetMapId()); + uint32 fixtureCreateCount = 0; + for (ObjectGuid const& meshGuid : meshItr->second) { MeshObject* meshObj = hMap->GetMeshObject(meshGuid); - if (meshObj && meshObj->IsExteriorRoot() && meshObj->IsInWorld()) + if (meshObj && meshObj->IsInWorld() && meshObj->m_housingFixtureData.has_value()) { - UpdateData updateData(p->GetMapId()); - meshObj->BuildCreateUpdateBlockForPlayer(&updateData, p); + meshObj->BuildCreateUpdateBlockForPlayer(&fixtureUpdate, p); p->m_clientGUIDs.insert(meshGuid); - WorldPacket updatePacket; - updateData.BuildPacket(&updatePacket); - p->SendDirectMessage(&updatePacket); - - TC_LOG_DEBUG("housing", "HousingMap deferred ENTER_PLOT: Re-CREATE root MeshObject {} for plot {} (singleton refresh)", - meshGuid.ToString(), deferredPlotIndex); - break; + ++fixtureCreateCount; } } + if (fixtureCreateCount > 0) + { + WorldPacket fixturePacket; + fixtureUpdate.BuildPacket(&fixturePacket); + p->SendDirectMessage(&fixturePacket); + } + + TC_LOG_DEBUG("housing", "HousingMap deferred ENTER_PLOT: Re-CREATE {} fixture MeshObjects for plot {} (post-rebuild)", + fixtureCreateCount, deferredPlotIndex); } } @@ -1660,13 +1683,25 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo float doorLocalY = 0.0f; float doorLocalZ = 0.0f; - // Resolve door component from DB2 - uint32 doorCompID = 0; - if (fixtureOverrides) + // Resolve door component from DB2. + // Use the actually-spawned base component (from rootOverrides) for hook lookups, + // not the raw coreExtCompID which may differ after migration/override. + uint32 actualBaseCompID = static_cast(exteriorComponentID); + if (rootOverrides) { - // Check if player has a custom door at a hook ? scan hooks for Type=11 - auto const* baseHooks = sHousingMgr.GetHooksOnComponent(static_cast(exteriorComponentID)); - if (baseHooks) + auto baseOvr = rootOverrides->find(HOUSING_FIXTURE_TYPE_BASE); + if (baseOvr != rootOverrides->end()) + actualBaseCompID = baseOvr->second; + } + + uint32 doorCompID = 0; + uint32 doorHookID = 0; // Track which hook the door is at (for position) + { + // Find the door from the player's explicit fixture override. + // No auto-resolve: the player selects their door via the fixture editor. + // The door GO is only spawned when a door fixture exists. + auto const* baseHooks = sHousingMgr.GetHooksOnComponent(actualBaseCompID); + if (baseHooks && fixtureOverrides) { for (ExteriorComponentHookEntry const* hook : *baseHooks) { @@ -1674,14 +1709,15 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo { auto ovrItr = fixtureOverrides->find(hook->ID); if (ovrItr != fixtureOverrides->end()) + { doorCompID = ovrItr->second; - break; + doorHookID = hook->ID; + break; + } } } } } - if (!doorCompID && houseExteriorWmoDataID > 0) - doorCompID = sHousingMgr.GetDefaultFixtureForType(11 /*Door*/, static_cast(houseExteriorWmoDataID)); ExteriorComponentEntry const* doorComp = doorCompID ? sExteriorComponentStore.LookupEntry(doorCompID) : nullptr; if (doorComp) @@ -1693,13 +1729,13 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo 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(exteriorComponentID)); - if (baseHooks) + // Use the selected door hook's position for door GO placement + auto const* baseHooks = sHousingMgr.GetHooksOnComponent(actualBaseCompID); + if (baseHooks && doorHookID) { for (ExteriorComponentHookEntry const* hook : *baseHooks) { - if (hook && hook->ExteriorComponentTypeID == 11) + if (hook && hook->ID == doorHookID) { // Hook position provides the attachment point on the base doorLocalX = hook->Position[0]; @@ -2008,13 +2044,13 @@ MeshObject* HousingMap::SpawnHouseMeshObject(uint8 plotIndex, int32 fileDataID, // 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(exteriorComponentHookID) - : static_cast(exteriorComponentID); + // Use subType=5 (fixture), realm, sequential counter, houseGuid counter. + // Generate a unique fixture GUID using a monotonic counter. + static std::atomic s_fixtureCounter{ 1 }; + uint32 fixtureSeq = s_fixtureCounter.fetch_add(1, std::memory_order_relaxed); ObjectGuid fixtureGuid = ObjectGuid::Create( /*subType*/ 5, /*arg1*/ sRealmList->GetCurrentRealmId().Realm, - /*arg2*/ fixtureArg2, houseGuid.GetCounter()); + /*arg2*/ fixtureSeq, 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, @@ -2400,6 +2436,14 @@ uint32 HousingMap::SpawnExtCompTree(uint8 plotIndex, uint32 extCompID, // Recurse into hooks on this component auto const* hooks = sHousingMgr.GetHooksOnComponent(extCompID); + TC_LOG_INFO("housing", "SpawnExtCompTree: comp={} has {} hooks, fixtureOverrides={}", + extCompID, hooks ? uint32(hooks->size()) : 0, fixtureOverrides != nullptr); + + // Spawn child components at hooks from player fixture overrides only. + // No auto-resolve: doors, windows, chimneys etc. are all player-selected via the + // fixture editor. The door GO for house entry is positioned independently and doesn't + // require a door mesh to exist. This avoids the "wrong side" problem where a heuristic + // can't reliably determine the model's front face across different plot rotations. if (hooks) { for (ExteriorComponentHookEntry const* hook : *hooks) @@ -2407,9 +2451,7 @@ uint32 HousingMap::SpawnExtCompTree(uint8 plotIndex, uint32 extCompID, if (!hook) continue; - // 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 + // Only spawn hook children that the player has explicitly selected ExteriorComponentEntry const* childComp = nullptr; if (fixtureOverrides) { @@ -2417,23 +2459,17 @@ uint32 HousingMap::SpawnExtCompTree(uint8 plotIndex, uint32 extCompID, if (overrideItr != fixtureOverrides->end()) childComp = sExteriorComponentStore.LookupEntry(overrideItr->second); } - if (!childComp) - { - uint32 defaultCompID = sHousingMgr.GetDefaultFixtureForType( - static_cast(hook->ExteriorComponentTypeID), - static_cast(houseExteriorWmoDataID)); - if (defaultCompID) - childComp = sExteriorComponentStore.LookupEntry(defaultCompID); - } if (!childComp) continue; - TC_LOG_DEBUG("housing", "SpawnExtCompTree: parent={} hook={} (type={}) ? child comp {} '{}' (ParentComp={}, ModelFDID={})", + TC_LOG_INFO("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 the local-space coordinates where the child + // mesh attaches on the parent. Use hook position directly as the child's + // PositionLocalSpace ? the client handles the attachment via AttachParentGUID. Position hookPos(hook->Position[0], hook->Position[1], hook->Position[2], 0.0f); QuaternionData hookRot; // Hook rotation is in degrees ? convert to quaternion (XYZ extrinsic Euler) diff --git a/src/server/game/Housing/HousingMgr.cpp b/src/server/game/Housing/HousingMgr.cpp index 6107c1c52..76d0e7b7e 100644 --- a/src/server/game/Housing/HousingMgr.cpp +++ b/src/server/game/Housing/HousingMgr.cpp @@ -1271,17 +1271,15 @@ void HousingMgr::BuildExteriorComponentIndexes() _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 - // sparse indexing. Use LookupEntry(i) over GetNumRows() to reach all entries. - for (uint32 i = 0; i < sExteriorComponentHookStore.GetNumRows(); ++i) + // 1. Build hook index: which hooks are parented to each component. + // ExteriorComponentHook has IndexField=2 (ID in data) and ParentIndexField=4. + // Use the store's range-based iterator which correctly iterates unique entries. + for (ExteriorComponentHookEntry const* hook : sExteriorComponentHookStore) { - ExteriorComponentHookEntry const* hook = sExteriorComponentHookStore.LookupEntry(i); if (!hook) continue; _hooksByExtComp[hook->ExteriorComponentID].push_back(hook); } - // 1a. Build child index and root-by-WMO index from ExteriorComponent. // ParentComponentID > 0 ? color/dye variant of that component. // ParentComponentID == 0 ? base variant, and if the Type is a structural root, @@ -1292,12 +1290,11 @@ void HousingMgr::BuildExteriorComponentIndexes() // 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. + // ExteriorComponent uses ParentIndexField (HouseExteriorWmoDataID). + // Use range-based iterator, NOT LookupEntry(i), which maps by parent ID. std::unordered_set structuralRootTypes; - for (uint32 i = 0; i < sExteriorComponentStore.GetNumRows(); ++i) + for (ExteriorComponentEntry const* comp : sExteriorComponentStore) { - ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(i); if (!comp) continue; @@ -1352,19 +1349,19 @@ void HousingMgr::BuildExteriorComponentIndexes() // // 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) + for (ExteriorComponentEntry const* comp : sExteriorComponentStore) { - ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(i); if (!comp || comp->ParentComponentID != 0 || comp->HouseExteriorWmoDataID == 0) continue; - uint64 key = (uint64(comp->Type) << 32) | comp->HouseExteriorWmoDataID; + // Key includes size so different house sizes get the right defaults + uint64 key = (uint64(comp->Type) << 40) | (uint64(comp->HouseExteriorWmoDataID) << 8) | comp->Size; bool isDefault = (comp->Flags & 0x1) != 0; auto existing = _defaultFixtureByTypeWmo.find(key); if (existing == _defaultFixtureByTypeWmo.end()) { - // First component for this (type, wmo) ? insert it + // First component for this (type, wmo, size) ? insert it _defaultFixtureByTypeWmo[key] = comp->ID; } else if (isDefault) @@ -1399,9 +1396,30 @@ std::vector const* HousingMgr::GetHooksOnComp return itr != _hooksByExtComp.end() ? &itr->second : nullptr; } -uint32 HousingMgr::GetDefaultFixtureForType(uint8 componentType, uint32 wmoDataID) const +uint32 HousingMgr::GetDefaultFixtureForType(uint8 componentType, uint32 wmoDataID, uint8 houseSize /*= 0*/) const { - uint64 key = (uint64(componentType) << 32) | wmoDataID; + // Try exact size match first + if (houseSize > 0) + { + uint64 key = (uint64(componentType) << 40) | (uint64(wmoDataID) << 8) | houseSize; + auto itr = _defaultFixtureByTypeWmo.find(key); + if (itr != _defaultFixtureByTypeWmo.end()) + return itr->second; + } + + // Fallback: scan all sizes for this (type, wmo) ? useful when caller doesn't know the size + for (uint8 sz = 1; sz <= 4; ++sz) + { + if (sz == houseSize) + continue; // already tried + uint64 key = (uint64(componentType) << 40) | (uint64(wmoDataID) << 8) | sz; + auto itr = _defaultFixtureByTypeWmo.find(key); + if (itr != _defaultFixtureByTypeWmo.end()) + return itr->second; + } + + // Also try size=0 in case any components have Size=0 + uint64 key = (uint64(componentType) << 40) | (uint64(wmoDataID) << 8); auto itr = _defaultFixtureByTypeWmo.find(key); return itr != _defaultFixtureByTypeWmo.end() ? itr->second : 0; } diff --git a/src/server/game/Housing/HousingMgr.h b/src/server/game/Housing/HousingMgr.h index 52ab23c95..e52dada7f 100644 --- a/src/server/game/Housing/HousingMgr.h +++ b/src/server/game/Housing/HousingMgr.h @@ -336,9 +336,11 @@ public: std::vector const* GetRootComponentsForWmoData(uint32 wmoDataID) const; std::vector 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; + // Fixture resolution: given a hook's component type, the house's WmoDataID, and + // optionally the house size, returns the default fixture component ID + // (Flags & 0x1, ParentComponentID == 0). + // If houseSize is 0, returns any size match; otherwise filters to exact size. + uint32 GetDefaultFixtureForType(uint8 componentType, uint32 wmoDataID, uint8 houseSize = 0) const; // Racial house style: maps player race to the appropriate HouseExteriorWmoDataID. // Night Elf ? 55, Blood Elf ? 56, other Alliance ? 9 (Human), other Horde ? 87 (Orc). @@ -447,8 +449,8 @@ private: std::unordered_map> _childrenByExtComp; std::unordered_map> _rootCompsByWmoDataId; - // Fixture resolution: (componentType, wmoDataID) ? default component ID - // Key = (uint64(componentType) << 32) | wmoDataID + // Fixture resolution: (componentType, wmoDataID, size) ? default component ID + // Key = (uint64(componentType) << 40) | (uint64(wmoDataID) << 8) | size std::unordered_map _defaultFixtureByTypeWmo; }; diff --git a/src/server/scripts/World/at_housing_plot.cpp b/src/server/scripts/World/at_housing_plot.cpp index 1c3d3202f..b8192f9af 100644 --- a/src/server/scripts/World/at_housing_plot.cpp +++ b/src/server/scripts/World/at_housing_plot.cpp @@ -128,35 +128,42 @@ struct at_housing_plot : AreaTriggerAI } } - // Re-CREATE the player's exterior root MeshObject so the client's - // Tag_HouseExteriorRoot singleton points to THIS plot's root. - // With multiple occupied plots, the singleton retains whichever root - // was created last during SpawnPlotGameObjects(). Re-sending CREATE - // for our root forces the client's fragment 225 handler to overwrite - // the singleton, guaranteeing GUID match in the fixture response handler. + // Send CREATE_BASIC_HOUSE_RESPONSE to trigger the fixture manager rebuild, + // then re-CREATE ALL fixture MeshObjects so the CREATE callback can match + // their HouseGUID against the fixture manager's now-populated state+96/+104. if (isOwnPlot) { + WorldPackets::Housing::HousingFixtureCreateBasicHouseResponse fixtureInit; + fixtureInit.Result = static_cast(HOUSING_RESULT_SUCCESS); + player->SendDirectMessage(fixtureInit.Write()); + auto const& meshMap = housingMap->GetPlotMeshObjects(); auto meshItr = meshMap.find(static_cast(plotId)); if (meshItr != meshMap.end()) { + UpdateData fixtureUpdate(player->GetMapId()); + uint32 fixtureCreateCount = 0; + for (ObjectGuid const& meshGuid : meshItr->second) { MeshObject* meshObj = housingMap->GetMeshObject(meshGuid); - if (meshObj && meshObj->IsExteriorRoot() && meshObj->IsInWorld()) + if (meshObj && meshObj->IsInWorld() && meshObj->m_housingFixtureData.has_value()) { - UpdateData updateData(player->GetMapId()); - meshObj->BuildCreateUpdateBlockForPlayer(&updateData, player); + meshObj->BuildCreateUpdateBlockForPlayer(&fixtureUpdate, player); player->m_clientGUIDs.insert(meshGuid); - WorldPacket updatePacket; - updateData.BuildPacket(&updatePacket); - player->SendDirectMessage(&updatePacket); - - TC_LOG_DEBUG("housing", "at_housing_plot: Re-CREATE root MeshObject {} for plot {} (singleton refresh)", - meshGuid.ToString(), plotId); - break; + ++fixtureCreateCount; } } + + if (fixtureCreateCount > 0) + { + WorldPacket fixturePacket; + fixtureUpdate.BuildPacket(&fixturePacket); + player->SendDirectMessage(&fixturePacket); + } + + TC_LOG_DEBUG("housing", "at_housing_plot: Re-CREATE {} fixture MeshObjects for plot {} (post-rebuild)", + fixtureCreateCount, plotId); } }