Housing: fix Visit teleport for offline owners + spawn 4 Group B mirrors per plot

Visit teleport (TELEPORT_TO_PLOT, TeleportType=5):
  CanVisitorAccess() returned false whenever the plot owner was offline
  because the helper takes Player const* and aborts on null. Result: the
  client received PERMISSION_DENIED and never teleported, even on plots
  whose settings were "ANYONE".

  Fix: add CanVisitorAccessPlot(visitor, ownerGuid, settingsFlags, isInterior)
  that resolves friend / guild / neighborhood relationships from the
  visitor side + sCharacterCache, so the check works for offline owners.
  Settings come from a new PlotInfo::HouseSettingsFlags mirrored from
  character_housing.settingsFlags at neighborhood preload, refreshed when
  Housing::SaveSettings runs while the owner is online.

  TeleportToPlot now passes the persisted plot settings into the new
  helper instead of bailing on a null Player*.

Group B Entity mirrors:
  Audit 2026-04-21 (FINDINGS.md sec 1.2) found retail emits 4 per-piece
  Entity mirrors per plot - one FMirroredPositionData_C-only mirror
  attached to each visible exterior fixture MeshObject (Base/Roof/Door/
  Window - ExteriorComponent Type 9/10/11/12). We were emitting one,
  anchored to the root only.

  _houseMeshMirrorEntities is now a vector<unique_ptr<HousingMirrorEntity>>
  per plot. SpawnHouseForPlot iterates the plot's MeshObjects, and pairs
  one Group B mirror per fixture-tier mesh. MakeHouseMeshMirrorGuid now
  packs (bnetId << 16) | (plot << 8) | piece so each mirror has a unique
  GUID. Player::BuildCreateUpdateBlockForPlayer iterates the new
  GetHouseMeshMirrors() list so all per-piece mirrors land in the
  initial UPDATE_OBJECT bundle.

Schema:
  CHAR_SEL_NEIGHBORHOOD_MEMBERS extended with ch.settingsFlags so
  Neighborhood::LoadFromDB can populate PlotInfo::HouseSettingsFlags
  for every member's plot at startup.
This commit is contained in:
agatho
2026-04-27 21:12:18 +02:00
parent dc1a9167dc
commit e62aabb735
10 changed files with 190 additions and 55 deletions
@@ -884,7 +884,7 @@ void CharacterDatabaseConnection::DoPrepareStatements()
PrepareStatement(CHAR_UPD_NEIGHBORHOOD_PUBLIC, "UPDATE neighborhoods SET isPublic = ? WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_UPD_NEIGHBORHOOD_NAME, "UPDATE neighborhoods SET name = ? WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_DEL_NEIGHBORHOOD, "DELETE FROM neighborhoods WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_NEIGHBORHOOD_MEMBERS, "SELECT nm.playerGuid, nm.role, nm.joinTime, nm.plotIndex, ch.houseId, c.account, ch.houseLevel, ch.favor, ch.houseName, ch.houseType FROM neighborhood_members nm LEFT JOIN character_housing ch ON nm.playerGuid = ch.guid LEFT JOIN characters c ON nm.playerGuid = c.guid WHERE nm.neighborhoodGuid = ?", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_NEIGHBORHOOD_MEMBERS, "SELECT nm.playerGuid, nm.role, nm.joinTime, nm.plotIndex, ch.houseId, c.account, ch.houseLevel, ch.favor, ch.houseName, ch.houseType, ch.settingsFlags FROM neighborhood_members nm LEFT JOIN character_housing ch ON nm.playerGuid = ch.guid LEFT JOIN characters c ON nm.playerGuid = c.guid WHERE nm.neighborhoodGuid = ?", CONNECTION_SYNCH);
// Owner-keyed batch fetches used to preload all occupied-plot exterior and
// interior spawn data at neighborhood init, so houses render for every plot
// regardless of whether the owner is online. Filtered by the neighborhood's
+5 -3
View File
@@ -3713,8 +3713,10 @@ void Player::BuildCreateUpdateBlockForPlayer(UpdateData* data, Player* target) c
m->BuildCreateUpdateBlockForPlayer(data, target);
++mirrorCount;
}
// Group B mesh mirror (untagged, AttachParent=MeshObject).
if (HousingMirrorEntity* bm = hmap->GetHouseMeshMirror(plot.PlotIndex))
// Group B per-piece mirrors (untagged, AttachParent=fixture
// MeshObject). Retail emits one per visible exterior fixture
// (Base/Roof/Door/Window — typically 4 per plot).
for (HousingMirrorEntity* bm : hmap->GetHouseMeshMirrors(plot.PlotIndex))
{
bm->BuildCreateUpdateBlockForPlayer(data, target);
++mirrorCount;
@@ -3739,7 +3741,7 @@ void Player::BuildCreateUpdateBlockForPlayer(UpdateData* data, Player* target) c
{
if (HousingMirrorEntity* ownMirror = hmap->GetHouseMirror(ownPlotIndex))
ownMirror->BuildCreateUpdateBlockForPlayer(data, target);
if (HousingMirrorEntity* ownMeshMirror = hmap->GetHouseMeshMirror(ownPlotIndex))
for (HousingMirrorEntity* ownMeshMirror : hmap->GetHouseMeshMirrors(ownPlotIndex))
ownMeshMirror->BuildCreateUpdateBlockForPlayer(data, target);
}
+7 -5
View File
@@ -3527,18 +3527,20 @@ void WorldSession::HandleHousingSvcsTeleportToPlot(WorldPackets::Housing::Housin
if (targetPlot)
{
// Per-house access check: verify visitor has permission to access this plot
// Per-house access check: verify visitor has permission to access this plot.
// Owner can be offline — fall back to the persisted plotInfo->HouseSettingsFlags
// (mirrored from character_housing.settingsFlags at neighborhood preload).
if (!neighborhood->IsMember(player->GetGUID()))
{
Neighborhood::PlotInfo const* plotInfo = neighborhood->GetPlotInfo(static_cast<uint8>(plotIndex));
if (plotInfo && plotInfo->IsOccupied())
{
Player* ownerPlayer = ObjectAccessor::FindPlayer(plotInfo->OwnerGuid);
uint32 settingsFlags = HOUSE_SETTING_DEFAULT;
if (ownerPlayer && ownerPlayer->GetHousing())
settingsFlags = ownerPlayer->GetHousing()->GetSettingsFlags();
uint32 settingsFlags = (ownerPlayer && ownerPlayer->GetHousing())
? ownerPlayer->GetHousing()->GetSettingsFlags()
: plotInfo->HouseSettingsFlags;
if (!sHousingMgr.CanVisitorAccess(player, ownerPlayer, settingsFlags, false))
if (!sHousingMgr.CanVisitorAccessPlot(player, plotInfo->OwnerGuid, settingsFlags, false))
{
WorldPackets::Housing::HousingSvcsNotifyPermissionsFailure denied;
denied.FailureType = static_cast<uint8>(HOUSING_RESULT_PERMISSION_DENIED);
+5
View File
@@ -2556,6 +2556,11 @@ void Housing::SaveSettings(uint32 settingsFlags)
stmt->setUInt64(1, _owner->GetGUID().GetCounter());
CharacterDatabase.Execute(stmt);
// Mirror onto the in-memory neighborhood plot so visitor permission checks
// (CanVisitorAccessPlot) work correctly when the owner is offline.
if (Neighborhood* nbh = sNeighborhoodMgr.GetNeighborhood(_neighborhoodGuid))
nbh->UpdatePlotSettingsFlags(_owner->GetGUID(), _settingsFlags);
SyncUpdateFields();
TC_LOG_DEBUG("housing", "Housing::SaveSettings: Player {} updated house settings to {} in house {}",
+61 -33
View File
@@ -2012,14 +2012,23 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo
_houseMirrorEntities[plotIndex] = std::move(mirror);
}
// Group B Entity mirror — FMirroredPositionData_C only (no tags),
// AttachParent = exterior-root MeshObject (not the room). Sniff
// idx 9984 shows 4 Group B mirrors alongside the 4 Group A ones.
// Purpose: pure spatial anchor off the house's visible mesh.
// Lookup: first MeshObject in this plot's list whose FHousingFixture_C
// ExteriorComponentType==9 (Base) and no AttachParent (i.e. a root).
// Group B Entity mirrors — FMirroredPositionData_C only (no tags),
// one per visible exterior fixture MeshObject (Base/Roof/Door/Window —
// ExteriorComponent Type 9/10/11/12). AttachParent = the piece's own
// MeshObject. Sniff idx 9984 shows 4 Group B mirrors per plot, one
// anchored to each fixture mesh. Without these per-piece anchors the
// client lacks spatial hooks for door-hover detection and expert-mode
// placement preview off non-root meshes.
{
ObjectGuid exteriorRootGuid;
std::vector<std::unique_ptr<HousingMirrorEntity>>& mirrors = _houseMeshMirrorEntities[plotIndex];
mirrors.clear();
uint32 const bnetId = static_cast<uint32>(plotInfo->OwnerBnetGuid.GetCounter());
uint8 pieceIndex = 0;
QuaternionData identity;
identity.x = identity.y = identity.z = 0.0f;
identity.w = 1.0f;
Position const localPos(0.0f, 0.0f, 0.0f, 0.0f);
auto meshItr = _meshObjects.find(plotIndex);
if (meshItr != _meshObjects.end())
{
@@ -2029,35 +2038,35 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo
if (!mesh || !mesh->m_housingFixtureData.has_value())
continue;
UF::HousingFixtureData const& fd = *mesh->m_housingFixtureData;
if (uint8(fd.ExteriorComponentType) == 9 && fd.AttachParentGUID->IsEmpty())
{
exteriorRootGuid = meshGuid;
break;
}
uint8 const compType = uint8(fd.ExteriorComponentType);
// Pair Group B mirrors with the visible fixture types only:
// 9=Base, 10=Roof, 11=Door, 12=Window. Other component types
// (decorative subpieces, hooks) don't get retail-side mirrors.
if (compType < 9 || compType > 12)
continue;
ObjectGuid mirrorGuid = MakeHouseMeshMirrorGuid(plotIndex, bnetId, pieceIndex);
auto mirror = std::make_unique<HousingMirrorEntity>(this, mirrorGuid);
mirror->InitPositionData(meshGuid,
localPos, identity, /*scale*/ 1.0f, /*attachmentFlags*/ 3,
/*isExteriorRoot*/ false);
TC_LOG_DEBUG("housing", "HousingMap::SpawnHouseForPlot: spawned Group B mirror[{}] {} "
"for plot {} (attach={} [mesh type={}])",
pieceIndex, mirrorGuid.ToString(), plotIndex, meshGuid.ToString(), compType);
mirrors.push_back(std::move(mirror));
++pieceIndex;
}
}
if (!exteriorRootGuid.IsEmpty())
if (mirrors.empty())
{
uint32 bnetId = static_cast<uint32>(plotInfo->OwnerBnetGuid.GetCounter());
ObjectGuid meshMirrorGuid = MakeHouseMeshMirrorGuid(plotIndex, bnetId);
auto meshMirror = std::make_unique<HousingMirrorEntity>(this, meshMirrorGuid);
Position const localPos(0.0f, 0.0f, 0.0f, 0.0f);
QuaternionData identity;
identity.x = identity.y = identity.z = 0.0f;
identity.w = 1.0f;
meshMirror->InitPositionData(exteriorRootGuid,
localPos, identity, /*scale*/ 1.0f, /*attachmentFlags*/ 3,
/*isExteriorRoot*/ false);
TC_LOG_DEBUG("housing", "HousingMap::SpawnHouseForPlot: spawned Group B mesh mirror {} for plot {} "
"(attach={} [mesh root])",
meshMirrorGuid.ToString(), plotIndex, exteriorRootGuid.ToString());
_houseMeshMirrorEntities[plotIndex] = std::move(meshMirror);
TC_LOG_WARN("housing", "HousingMap::SpawnHouseForPlot: no fixture MeshObjects found for plot {}; "
"Group B mirrors skipped (client spatial anchors off house meshes will be missing)", plotIndex);
}
else
{
TC_LOG_WARN("housing", "HousingMap::SpawnHouseForPlot: no exterior-root MeshObject found for plot {}; "
"Group B mirror skipped (client spatial anchor off house mesh will be missing)", plotIndex);
TC_LOG_DEBUG("housing", "HousingMap::SpawnHouseForPlot: emitted {} Group B mirrors for plot {}",
mirrors.size(), plotIndex);
}
}
}
@@ -3143,7 +3152,11 @@ ObjectGuid HousingMap::MakeHouseMirrorGuid(uint8 plotIndex, uint32 bnetAccountId
HousingMirrorEntity* HousingMap::GetHouseMeshMirror(uint8 plotIndex) const
{
auto itr = _houseMeshMirrorEntities.find(plotIndex);
return itr != _houseMeshMirrorEntities.end() ? itr->second.get() : nullptr;
if (itr == _houseMeshMirrorEntities.end() || itr->second.empty())
return nullptr;
// Returns the first (root-piece) Group B mirror for legacy callers that
// only need any anchor; per-piece access goes via _houseMeshMirrorEntities.
return itr->second.front().get();
}
ObjectGuid HousingMap::GetHouseMeshMirrorGuid(uint8 plotIndex) const
@@ -3153,12 +3166,27 @@ ObjectGuid HousingMap::GetHouseMeshMirrorGuid(uint8 plotIndex) const
return ObjectGuid::Empty;
}
ObjectGuid HousingMap::MakeHouseMeshMirrorGuid(uint8 plotIndex, uint32 bnetAccountId) const
std::vector<HousingMirrorEntity*> HousingMap::GetHouseMeshMirrors(uint8 plotIndex) const
{
std::vector<HousingMirrorEntity*> result;
auto itr = _houseMeshMirrorEntities.find(plotIndex);
if (itr == _houseMeshMirrorEntities.end())
return result;
result.reserve(itr->second.size());
for (auto const& mirror : itr->second)
result.push_back(mirror.get());
return result;
}
ObjectGuid HousingMap::MakeHouseMeshMirrorGuid(uint8 plotIndex, uint32 bnetAccountId, uint8 pieceIndex /*= 0*/) const
{
// Distinct synthetic entry from Group A (37361) so Group A/B guids never collide.
// Same counter packing: (bnetId << 8) | plotIndex.
// Counter packs the (bnet, plot, piece) triple so each per-piece Group B
// mirror has a unique GUID across the realm: (bnetId << 16) | (plot << 8) | piece.
constexpr uint32 HOUSING_MESH_MIRROR_ENTRY = 37362;
uint64 counter = (static_cast<uint64>(bnetAccountId) << 8) | static_cast<uint64>(plotIndex);
uint64 counter = (static_cast<uint64>(bnetAccountId) << 16)
| (static_cast<uint64>(plotIndex) << 8)
| static_cast<uint64>(pieceIndex);
return ObjectGuid::Create<HighGuid::Entity>(GetId(), HOUSING_MESH_MIRROR_ENTRY, counter);
}
+16 -11
View File
@@ -95,16 +95,20 @@ public:
// and bnet owner are known from NeighborhoodMirror data.
ObjectGuid MakeHouseMirrorGuid(uint8 plotIndex, uint32 bnetAccountId) const;
// "Group B" mesh-level mirror. Retail pairs each Group A (house-exterior
// root) Entity mirror with a SECOND untagged FMirroredPositionData_C
// Entity mirror whose AttachParent is the exterior root MeshObject (not
// the room / Housing/2). Sniff idx 9984: 4 Group A (44-byte values) +
// 4 Group B (52-byte values, AttachParent=MeshObject serialises longer).
// Without Group B the client's spatial index for the house-exterior mesh
// is missing, which breaks AoE/icon resolution off the MeshObject root.
// "Group B" per-piece mesh-level mirrors. Retail pairs EACH visible
// exterior fixture MeshObject (Base/Roof/Door/Window — ExteriorComponent
// Type 9/10/11/12) with one untagged FMirroredPositionData_C Entity
// mirror whose AttachParent is the piece's MeshObject. Sniff idx 9984:
// 4 Group A (44-byte values) + 4 Group B (52-byte values, AttachParent=
// MeshObject serialises longer). Without these per-piece anchors the
// client's spatial index is missing finer-grained hooks (door-hover
// detection, expert-mode placement preview off non-root meshes).
HousingMirrorEntity* GetHouseMeshMirror(uint8 plotIndex) const;
ObjectGuid GetHouseMeshMirrorGuid(uint8 plotIndex) const;
ObjectGuid MakeHouseMeshMirrorGuid(uint8 plotIndex, uint32 bnetAccountId) const;
ObjectGuid MakeHouseMeshMirrorGuid(uint8 plotIndex, uint32 bnetAccountId, uint8 pieceIndex = 0) const;
// Full list of per-piece Group B mirrors for this plot (one per fixture
// MeshObject of Type 9/10/11/12). Returns empty if no house spawned.
std::vector<HousingMirrorEntity*> GetHouseMeshMirrors(uint8 plotIndex) const;
// Lightweight Housing/2 identity room entity (HighGuid::Housing subType=2,
// objectType=18) — the authoritative room. Retail-verified architecture:
@@ -212,9 +216,10 @@ private:
// 1:1 with the exterior root MeshObject.
std::unordered_map<uint8, std::unique_ptr<HousingMirrorEntity>> _houseMirrorEntities;
// "Group B" mesh-level Entity mirror, attached to the exterior root
// MeshObject (Type=9). 1:1 with the house root mesh; co-spawned/despawned.
std::unordered_map<uint8, std::unique_ptr<HousingMirrorEntity>> _houseMeshMirrorEntities;
// "Group B" per-piece mesh-level Entity mirrors, one per visible exterior
// fixture MeshObject (Type 9/10/11/12). Co-spawned with the house and
// despawned together. Vector since retail emits 4 of these per plot.
std::unordered_map<uint8, std::vector<std::unique_ptr<HousingMirrorEntity>>> _houseMeshMirrorEntities;
// Lightweight Housing/2 identity room GUID per plot. The entity itself is
// a WorldObject owned by the map's object store; we only track its GUID.
+66
View File
@@ -16,6 +16,7 @@
*/
#include "HousingMgr.h"
#include "CharacterCache.h"
#include "DB2Stores.h"
#include "DB2Structure.h"
#include "GameObjectData.h"
@@ -24,6 +25,8 @@
#include "Housing.h"
#include "Log.h"
#include "Neighborhood.h"
#include "NeighborhoodMgr.h"
#include "ObjectAccessor.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "RaceMask.h"
@@ -834,6 +837,69 @@ std::vector<std::pair<uint32, int32>> HousingMgr::GetStarterDecorWithQuantities(
return result;
}
bool HousingMgr::CanVisitorAccessPlot(Player const* visitor, ObjectGuid ownerGuid, uint32 settingsFlags, bool isInterior) const
{
if (!visitor || ownerGuid.IsEmpty())
return false;
if (visitor->GetGUID() == ownerGuid)
return true;
uint32 anyoneFlag = isInterior ? HOUSE_SETTING_HOUSE_ACCESS_ANYONE : HOUSE_SETTING_PLOT_ACCESS_ANYONE;
uint32 neighborsFlag = isInterior ? HOUSE_SETTING_HOUSE_ACCESS_NEIGHBORS : HOUSE_SETTING_PLOT_ACCESS_NEIGHBORS;
uint32 guildFlag = isInterior ? HOUSE_SETTING_HOUSE_ACCESS_GUILD : HOUSE_SETTING_PLOT_ACCESS_GUILD;
uint32 friendsFlag = isInterior ? HOUSE_SETTING_HOUSE_ACCESS_FRIENDS : HOUSE_SETTING_PLOT_ACCESS_FRIENDS;
uint32 partyFlag = isInterior ? HOUSE_SETTING_HOUSE_ACCESS_PARTY : HOUSE_SETTING_PLOT_ACCESS_PARTY;
uint32 accessMask = isInterior
? (HOUSE_SETTING_HOUSE_ACCESS_ANYONE | HOUSE_SETTING_HOUSE_ACCESS_NEIGHBORS |
HOUSE_SETTING_HOUSE_ACCESS_GUILD | HOUSE_SETTING_HOUSE_ACCESS_FRIENDS | HOUSE_SETTING_HOUSE_ACCESS_PARTY)
: (HOUSE_SETTING_PLOT_ACCESS_ANYONE | HOUSE_SETTING_PLOT_ACCESS_NEIGHBORS |
HOUSE_SETTING_PLOT_ACCESS_GUILD | HOUSE_SETTING_PLOT_ACCESS_FRIENDS | HOUSE_SETTING_PLOT_ACCESS_PARTY);
if ((settingsFlags & accessMask) == 0)
return true; // No restrictions configured — open to all
if (settingsFlags & anyoneFlag)
return true;
Player* ownerPlayer = ObjectAccessor::FindPlayer(ownerGuid);
if (settingsFlags & partyFlag)
{
// Party requires both online — same Group instance.
if (ownerPlayer && visitor->GetGroup() && visitor->GetGroup() == ownerPlayer->GetGroup())
return true;
}
if (settingsFlags & guildFlag)
{
ObjectGuid::LowType ownerGuildId = ownerPlayer
? ownerPlayer->GetGuildId()
: sCharacterCache->GetCharacterGuildIdByGuid(ownerGuid);
if (ownerGuildId != 0 && visitor->GetGuildId() == ownerGuildId)
return true;
}
if (settingsFlags & friendsFlag)
{
// Friends are mutual on retail — visitor's social manager has the same record.
if (visitor->GetSocial() && visitor->GetSocial()->HasFriend(ownerGuid))
return true;
}
if (settingsFlags & neighborsFlag)
{
// Both are residents of the same neighborhood. Works offline because
// neighborhood membership is stored on Neighborhood objects, not Player.
for (Neighborhood const* nbh : sNeighborhoodMgr.GetNeighborhoodsForPlayer(ownerGuid))
if (nbh->IsMember(visitor->GetGUID()))
return true;
}
return false;
}
bool HousingMgr::CanVisitorAccess(Player const* visitor, Player const* owner, uint32 settingsFlags, bool isInterior) const
{
if (!visitor || !owner)
+5
View File
@@ -370,6 +370,11 @@ public:
// accessMask = HOUSE_SETTING_HOUSE_ACCESS_* for interior, HOUSE_SETTING_PLOT_ACCESS_* for exterior
bool CanVisitorAccess(Player const* visitor, Player const* owner, uint32 settingsFlags, bool isInterior) const;
// Same as CanVisitorAccess but works when owner is offline — uses CharacterCache + the
// visitor's own social/group/guild/neighborhood data to resolve friend/party/guild/neighbor
// relationships symmetrically. Settings come from the persisted plotInfo->HouseSettingsFlags.
bool CanVisitorAccessPlot(Player const* visitor, ObjectGuid ownerGuid, uint32 settingsFlags, bool isInterior) const;
// Validation
HousingResult ValidateDecorPlacement(uint32 decorId, Position const& pos, uint32 houseLevel) const;
+18 -2
View File
@@ -67,8 +67,8 @@ bool Neighborhood::LoadFromDB(PreparedQueryResult neighborhood, PreparedQueryRes
{
Field* memberFields = members->Fetch();
// 0 1 2 3 4 5 6 7 8 9
// SELECT nm.playerGuid, nm.role, nm.joinTime, nm.plotIndex, ch.houseId, c.account, ch.houseLevel, ch.favor, ch.houseName, ch.houseType
// 0 1 2 3 4 5 6 7 8 9 10
// SELECT nm.playerGuid, nm.role, nm.joinTime, nm.plotIndex, ch.houseId, c.account, ch.houseLevel, ch.favor, ch.houseName, ch.houseType, ch.settingsFlags
// FROM neighborhood_members nm LEFT JOIN character_housing ch ON nm.playerGuid = ch.guid
// LEFT JOIN characters c ON nm.playerGuid = c.guid
// WHERE nm.neighborhoodGuid = ?
@@ -116,6 +116,8 @@ bool Neighborhood::LoadFromDB(PreparedQueryResult neighborhood, PreparedQueryRes
_plots[member.PlotIndex].HouseName = memberFields[8].GetString();
if (!memberFields[9].IsNull())
_plots[member.PlotIndex].HouseType = memberFields[9].GetUInt32();
if (!memberFields[10].IsNull())
_plots[member.PlotIndex].HouseSettingsFlags = memberFields[10].GetUInt32();
TC_LOG_INFO("housing", "Neighborhood::LoadFromDB plot[{}] owner={} lvl={} favor={} name='{}' "
"(ch.houseLevel.IsNull={} ch.favor.IsNull={} ch.houseName.IsNull={})",
@@ -1083,6 +1085,20 @@ void Neighborhood::UpdatePlotHouseInfo(uint8 plotIndex, ObjectGuid houseGuid, Ob
plotIndex, houseGuid.ToString(), ownerBnetGuid.ToString(), _name);
}
void Neighborhood::UpdatePlotSettingsFlags(ObjectGuid ownerGuid, uint32 settingsFlags)
{
for (PlotInfo& plot : _plots)
{
if (plot.IsOccupied() && plot.OwnerGuid == ownerGuid)
{
plot.HouseSettingsFlags = settingsFlags;
TC_LOG_DEBUG("housing", "Neighborhood::UpdatePlotSettingsFlags: plot {} owner {} settings=0x{:X} in '{}'",
plot.PlotIndex, ownerGuid.ToString(), settingsFlags, _name);
return;
}
}
}
HousingResult Neighborhood::MoveHouse(ObjectGuid sourcePlotOwner, uint8 newPlotIndex)
{
if (newPlotIndex >= MAX_NEIGHBORHOOD_PLOTS)
+6
View File
@@ -85,6 +85,11 @@ public:
// the owner is currently online.
std::vector<Housing::Room> Rooms;
// Mirrored from character_housing.settingsFlags so visitor permission
// checks (CanVisitorAccess) work when the plot owner is offline.
// Refreshed when an online owner mutates their Housing settings.
uint32 HouseSettingsFlags = 0;
bool IsOccupied() const { return PlotIndex != INVALID_PLOT_INDEX; }
};
@@ -154,6 +159,7 @@ public:
// Plot management
HousingResult PurchasePlot(ObjectGuid playerGuid, uint8 plotIndex);
void UpdatePlotHouseInfo(uint8 plotIndex, ObjectGuid houseGuid, ObjectGuid ownerBnetGuid);
void UpdatePlotSettingsFlags(ObjectGuid ownerGuid, uint32 settingsFlags);
HousingResult MoveHouse(ObjectGuid sourcePlotOwner, uint8 newPlotIndex);
void SetPlotAreaTriggerGuid(uint8 plotIndex, ObjectGuid atGuid);