Housing: Re-CREATE all fixture entities after rebuild to fix hook recognition

This commit is contained in:
luis
2026-03-16 20:14:57 -03:00
parent c869b64ab9
commit ddb75df371
10 changed files with 441 additions and 120 deletions
@@ -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;
@@ -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;
@@ -453,7 +453,11 @@ void BaseEntity::BuildMovementUpdate(ByteBuffer& data, CreateObjectBits flags, P
{
MeshObject const* meshObj = static_cast<MeshObject const*>(this);
data << meshObj->GetAttachParentGUID();
data << TaggedPosition<Position::XYZ>(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<Position::XYZ>(localPos.GetPositionX(), localPos.GetPositionY(), localPos.GetPositionZ());
QuaternionData const& rot = meshObj->GetLocalRotation();
data << rot.x << rot.y << rot.z << rot.w;
data << meshObj->GetLocalScale();
+1 -1
View File
@@ -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());
+32 -12
View File
@@ -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<int32>(housing->GetCoreExteriorComponentID()),
static_cast<int32>(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<int32>(housing->GetCoreExteriorComponentID()),
static_cast<int32>(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<uint8>(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<int32>(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<uint8>(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<int32>(housing->GetCoreExteriorComponentID()),
static_cast<int32>(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<int32>(housing->GetCoreExteriorComponentID()),
static_cast<int32>(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<int32>(housing->GetCoreExteriorComponentID()),
static_cast<int32>(styleID),
fixtureOverrides.empty() ? nullptr : &fixtureOverrides);
fixtureOverrides.empty() ? nullptr : &fixtureOverrides,
rootOverrides.empty() ? nullptr : &rootOverrides);
housingMap->SpawnAllDecorForPlot(plotIndex, housing);
}
}
+253 -22
View File
@@ -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<uint32>(_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<uint32> 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<uint32, uint32> Housing::GetFixtureOverrideMap() const
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.).
// 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<uint8, uint32> result;
for (auto const& [pointId, fixture] : _fixtures)
@@ -1805,13 +1914,33 @@ std::unordered_map<uint8, uint32> 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<uint32>(_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<uint8> 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<uint32>(_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);
}
}
}
}
}
+83 -47
View File
@@ -19,7 +19,9 @@
#include "Account.h"
#include "HousingPlayerHouseEntity.h"
#include <algorithm>
#include <atomic>
#include <cmath>
#include <limits>
#include <set>
#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<uint8>(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<uint32>(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<uint32>(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<uint32>(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<uint32>(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<uint32>(exteriorComponentHookID)
: static_cast<uint32>(exteriorComponentID);
// Use subType=5 (fixture), realm, sequential counter, houseGuid counter.
// Generate a unique fixture GUID using a monotonic counter.
static std::atomic<uint32> s_fixtureCounter{ 1 };
uint32 fixtureSeq = s_fixtureCounter.fetch_add(1, std::memory_order_relaxed);
ObjectGuid fixtureGuid = ObjectGuid::Create<HighGuid::Housing>(
/*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<uint8>(hook->ExteriorComponentTypeID),
static_cast<uint32>(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)
+34 -16
View File
@@ -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<uint8> 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<ExteriorComponentHookEntry const*> 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;
}
+7 -5
View File
@@ -336,9 +336,11 @@ public:
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;
// 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<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
// Fixture resolution: (componentType, wmoDataID, size) ? default component ID
// Key = (uint64(componentType) << 40) | (uint64(wmoDataID) << 8) | size
std::unordered_map<uint64, uint32> _defaultFixtureByTypeWmo;
};
+23 -16
View File
@@ -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<uint8>(HOUSING_RESULT_SUCCESS);
player->SendDirectMessage(fixtureInit.Write());
auto const& meshMap = housingMap->GetPlotMeshObjects();
auto meshItr = meshMap.find(static_cast<uint8>(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);
}
}