Housing: Re-CREATE all fixture entities after rebuild to fix hook recognition
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user