From ea1391c385fb71480e69771cd17b4e7e42d6fd2f Mon Sep 17 00:00:00 2001 From: agatho Date: Mon, 27 Apr 2026 23:49:35 +0200 Subject: [PATCH] Housing: extend Group A mirrors from 1 per plot to 4 per fixture piece Audit 2026-04-21 (FINDINGS.md sec 1.1) measured 4 Group A Entity mirrors per plot at retail idx 9984; we were spawning one. The previous fix covered only the Type-9 (Base) root, leaving the Roof/Door/Window companions on retail uncreated. _houseMirrorEntities is now a vector> per plot. SpawnHouseForPlot iterates the same fixture-tier MeshObjects the Group B pass walks (ExteriorComponent Type 9/10/11/12) and pairs a Group A mirror with each. All four share AttachParent = Housing/2 room identity (sniff-decoded "01 c1 XX 12 40 dc" - subType=2, arg2=18) and local pos (0,0,0) - the room is positioned at the plot centre, so the chain resolves there for every piece. Tagging is now an enum (None / Piece / PieceAndRoot) on HousingMirror Entity::InitPositionData. The Type-9 root (pieceIndex 0) keeps both Tag_HouseExteriorPiece and Tag_HouseExteriorRoot - this is the canonical GUID referenced by FHousingPlayerHouse_C.EntityGUID and the world-map icon picker. The other three (Roof/Door/Window) carry only Tag_HouseExteriorPiece. Group B per-mesh mirrors switch to Tagging::None which is identical behaviour to the prior isExteriorRoot=false path. MakeHouseMirrorGuid gains a pieceIndex parameter (default 0) and packs (bnetId<<16)|(plot<<8)|piece so each per-piece GUID is unique while the default-arg call site that proxy emission uses still resolves to the root mirror's GUID. Player::BuildCreateUpdateBlockForPlayer iterates the new GetHouseMirrors() list so all four Group A mirrors land in the initial UPDATE_OBJECT bundle alongside the four Group B ones (8 mirrors per plot total, matching retail idx 9984). --- .../Entities/Housing/HousingMirrorEntity.cpp | 24 ++- .../Entities/Housing/HousingMirrorEntity.h | 25 ++- src/server/game/Entities/Player/Player.cpp | 9 +- src/server/game/Housing/HousingMap.cpp | 144 ++++++++++++------ src/server/game/Housing/HousingMap.h | 27 +++- 5 files changed, 159 insertions(+), 70 deletions(-) diff --git a/src/server/game/Entities/Housing/HousingMirrorEntity.cpp b/src/server/game/Entities/Housing/HousingMirrorEntity.cpp index 79c066bea..eb35bb4a2 100644 --- a/src/server/game/Entities/Housing/HousingMirrorEntity.cpp +++ b/src/server/game/Entities/Housing/HousingMirrorEntity.cpp @@ -35,7 +35,7 @@ HousingMirrorEntity::~HousingMirrorEntity() = default; void HousingMirrorEntity::InitPositionData(ObjectGuid attachParent, Position const& position, QuaternionData const& rotation, - float scale, uint8 attachmentFlags, bool isExteriorRoot) + float scale, uint8 attachmentFlags, Tagging tagging) { auto posData = m_values.ModifyValue(&HousingMirrorEntity::m_mirroredPositionData) .ModifyValue(&UF::MirroredPositionData::PositionData); @@ -46,14 +46,22 @@ void HousingMirrorEntity::InitPositionData(ObjectGuid attachParent, SetUpdateFieldValue(posData.ModifyValue(&UF::MirroredMeshObjectData::ScaleLocalSpace), scale); SetUpdateFieldValue(posData.ModifyValue(&UF::MirroredMeshObjectData::AttachmentFlags), attachmentFlags); - // Retail house-exterior root mirrors carry BOTH tags (Group A at idx 9984 - // in dump_12.0.1.66838_2026-04-15_09-35-59). Mesh-level mirrors (Group B) - // carry neither. We expose isExteriorRoot for the house-exterior case; - // callers default it false for mesh-level mirrors. - if (isExteriorRoot) + // Retail (idx 9984 in dump_12.0.1.66838_2026-04-15_09-35-59) emits 4 + // Group A mirrors per plot. The Type-9 (Base) root mirror carries both + // Tag_HouseExteriorPiece and Tag_HouseExteriorRoot; the other three + // (Roof / Door / Window) carry Tag_HouseExteriorPiece only. Group B + // per-piece mesh mirrors carry neither tag. + switch (tagging) { - m_entityFragments.Add(WowCS::EntityFragment::Tag_HouseExteriorPiece, false); - m_entityFragments.Add(WowCS::EntityFragment::Tag_HouseExteriorRoot, false); + case Tagging::PieceAndRoot: + m_entityFragments.Add(WowCS::EntityFragment::Tag_HouseExteriorPiece, false); + m_entityFragments.Add(WowCS::EntityFragment::Tag_HouseExteriorRoot, false); + break; + case Tagging::Piece: + m_entityFragments.Add(WowCS::EntityFragment::Tag_HouseExteriorPiece, false); + break; + case Tagging::None: + break; } } diff --git a/src/server/game/Entities/Housing/HousingMirrorEntity.h b/src/server/game/Entities/Housing/HousingMirrorEntity.h index 7c3da4d9b..5f26a7b26 100644 --- a/src/server/game/Entities/Housing/HousingMirrorEntity.h +++ b/src/server/game/Entities/Housing/HousingMirrorEntity.h @@ -41,19 +41,28 @@ public: HousingMirrorEntity(Map* map, ObjectGuid guid); ~HousingMirrorEntity(); + // Tag set this mirror carries — controls which entity-fragment Tag_* + // entries are added when InitPositionData is called. + // None — Group B per-piece mesh anchor (no tags) + // Piece — Group A non-root piece (Tag_HouseExteriorPiece only) + // PieceAndRoot — Group A root piece (Tag_HouseExteriorPiece + + // Tag_HouseExteriorRoot) + enum class Tagging : uint8 + { + None, + Piece, + PieceAndRoot, + }; + // Populate the mirror's position fragment. // attachParent: the housing entity this mirror is attached to - // (HousingPlayerHouse for exterior root; MeshObject - // for per-piece mirrors) - // position/rotation/scale: local-space to attachParent (use (0,0,0) - // + identity + 1.0 when the mirror represents the - // house root itself) + // (room identity for Group A, MeshObject for Group B) + // position/rotation/scale: local-space to attachParent // attachmentFlags: usually 3 (sniff-typical) - // isExteriorRoot: if true, tags with Tag_HouseExteriorRoot AND - // Tag_HouseExteriorPiece to match retail Group A + // tagging: which Tag_HouseExterior* fragments to attach (see enum above) void InitPositionData(ObjectGuid attachParent, Position const& position, QuaternionData const& rotation, - float scale, uint8 attachmentFlags, bool isExteriorRoot); + float scale, uint8 attachmentFlags, Tagging tagging); void ClearUpdateMask(bool remove) override; std::string GetNameForLocaleIdx(LocaleConstant locale) const override; diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 13460e4fa..14d60471f 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -3707,8 +3707,11 @@ void Player::BuildCreateUpdateBlockForPlayer(UpdateData* data, Player* target) c proxy.BuildCreateUpdateBlockForPlayer(data, target); ++proxyCount; - // Bundle the mirror's CREATE into the same UPDATE_OBJECT. - if (HousingMirrorEntity* m = hmap->GetHouseMirror(plot.PlotIndex)) + // Bundle every Group A per-piece mirror's CREATE into the same + // UPDATE_OBJECT. Retail emits 4 (one per visible exterior fixture + // — Base/Roof/Door/Window). Index 0 is the Type-9 root mirror + // referenced by FHousingPlayerHouse_C.EntityGUID. + for (HousingMirrorEntity* m : hmap->GetHouseMirrors(plot.PlotIndex)) { m->BuildCreateUpdateBlockForPlayer(data, target); ++mirrorCount; @@ -3739,7 +3742,7 @@ void Player::BuildCreateUpdateBlockForPlayer(UpdateData* data, Player* target) c // resolves EntityGUID. if (hmap && ownPlotIndex != INVALID_PLOT_INDEX) { - if (HousingMirrorEntity* ownMirror = hmap->GetHouseMirror(ownPlotIndex)) + for (HousingMirrorEntity* ownMirror : hmap->GetHouseMirrors(ownPlotIndex)) ownMirror->BuildCreateUpdateBlockForPlayer(data, target); for (HousingMirrorEntity* ownMeshMirror : hmap->GetHouseMeshMirrors(ownPlotIndex)) ownMeshMirror->BuildCreateUpdateBlockForPlayer(data, target); diff --git a/src/server/game/Housing/HousingMap.cpp b/src/server/game/Housing/HousingMap.cpp index cd18afbec..e6929237b 100644 --- a/src/server/game/Housing/HousingMap.cpp +++ b/src/server/game/Housing/HousingMap.cpp @@ -1985,27 +1985,22 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo // Without this, ALL placement attempts fail with OutsidePlotBounds. SpawnRoomForPlot(plotIndex, pos, rot, plotInfo->HouseGuid); - // Spawn house-exterior root Entity mirror. - // Retail pairs each house with a HighGuid::Entity (57, objectType=18) - // carrying FMirroredPositionData_C + Tag_HouseExteriorPiece + - // Tag_HouseExteriorRoot. Sniff-verified at idx 9984 of - // dump_12.0.1.66838_2026-04-15_09-35-59 (4 Group A mirrors). + // Group A house-exterior Entity mirrors — one per visible exterior + // fixture MeshObject (Base/Roof/Door/Window — ExteriorComponent Type + // 9/10/11/12). All four share AttachParent = Housing/2 room identity + // (sniff idx 9984 in dump_12.0.1.66838_2026-04-15_09-35-59 decoded + // every Group A mirror's AttachParent as `01 c1 XX 12 40 dc` — + // subType=2 HousingRoom, arg2=18 HouseRoomID). The Type-9 root piece + // (pieceIndex 0) carries Tag_HouseExteriorPiece + Tag_HouseExteriorRoot + // and its GUID is the canonical "house mirror GUID" referenced by + // FHousingPlayerHouse_C.EntityGUID; the others carry Piece only. // - // CRITICAL: AttachParentGUID must be the ROOM entity, NOT the - // HousingPlayerHouse. Audit 2026-04-22 decoded retail Group A mirrors' - // AttachParent as `01 c1 XX 12 40 dc` — subType=2 (HousingRoom), - // arg2=18 (HouseRoomID). The HousingPlayerHouse has no position; - // attaching to it left the client unable to compute a world position - // for the mirror, so the world-map icon picker had nowhere to place - // the own-plot icon. The room entity carries the plot's world - // position in its TransportPosition, so chaining via AttachParent - // resolves to real coordinates. - // - // SpawnRoomForPlot was just called above and registered a room entity - // in _roomEntities[plotIndex]. Use that GUID. + // The HousingPlayerHouse has no position itself; the room entity + // carries the plot's world position so chaining via AttachParent + // resolves to real coordinates for the world-map icon picker. + // SpawnRoomForPlot was just called above and registered the room + // identity in _roomIdentityGuids[plotIndex]. { - // Prefer Housing/2 identity — retail-matching AttachParent for - // Group A Entity mirrors (with exterior tags). ObjectGuid roomParentGuid = GetRoomIdentityGuid(plotIndex); if (roomParentGuid.IsEmpty()) { @@ -2014,26 +2009,65 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo "mirror AttachParent falls back to HouseGuid (icon may not render)", plotIndex); } - uint32 bnetId = static_cast(plotInfo->OwnerBnetGuid.GetCounter()); - ObjectGuid mirrorGuid = MakeHouseMirrorGuid(plotIndex, bnetId); - auto mirror = std::make_unique(this, mirrorGuid); - // Local pos / rot relative to the room parent. Retail's Group A - // mirrors use small offsets (a few yards) and the root one uses - // zero pos + identity rot. We use zero pos + identity rot since - // our room entity is positioned at the plot centre. - Position const localPos(0.0f, 0.0f, 0.0f, 0.0f); + std::vector>& mirrors = _houseMirrorEntities[plotIndex]; + mirrors.clear(); + uint32 const bnetId = static_cast(plotInfo->OwnerBnetGuid.GetCounter()); + uint8 pieceIndex = 0; QuaternionData identity; identity.x = identity.y = identity.z = 0.0f; identity.w = 1.0f; - mirror->InitPositionData(roomParentGuid, - localPos, identity, /*scale*/ 1.0f, /*attachmentFlags*/ 3, - /*isExteriorRoot*/ true); - TC_LOG_DEBUG("housing", "HousingMap::SpawnHouseForPlot: spawned exterior mirror {} for plot {} " - "(attach={} [room], localPos=(0,0,0), plotWorldPos=({:.2f},{:.2f},{:.2f}))", - mirrorGuid.ToString(), plotIndex, - roomParentGuid.ToString(), - pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ()); - _houseMirrorEntities[plotIndex] = std::move(mirror); + // All Group A mirrors share the room identity as AttachParent and + // use (0,0,0) local pos — the room is positioned at the plot + // centre, so the chain resolves there for every piece. Without a + // fresh sniff parse showing exact non-root offsets, sharing the + // root's pos preserves the icon-resolution behaviour we already + // have for index 0; the additional pieces are pure registry + // entries used by the client for spatial categorisation. + Position const localPos(0.0f, 0.0f, 0.0f, 0.0f); + + auto meshItr = _meshObjects.find(plotIndex); + if (meshItr != _meshObjects.end()) + { + for (ObjectGuid const& meshGuid : meshItr->second) + { + MeshObject* mesh = GetMeshObject(meshGuid); + if (!mesh || !mesh->m_housingFixtureData.has_value()) + continue; + UF::HousingFixtureData const& fd = *mesh->m_housingFixtureData; + uint8 const compType = uint8(fd.ExteriorComponentType); + if (compType < 9 || compType > 12) + continue; + + ObjectGuid mirrorGuid = MakeHouseMirrorGuid(plotIndex, bnetId, pieceIndex); + auto mirror = std::make_unique(this, mirrorGuid); + HousingMirrorEntity::Tagging const tagging = (compType == 9) + ? HousingMirrorEntity::Tagging::PieceAndRoot + : HousingMirrorEntity::Tagging::Piece; + mirror->InitPositionData(roomParentGuid, + localPos, identity, /*scale*/ 1.0f, /*attachmentFlags*/ 3, + tagging); + TC_LOG_DEBUG("housing", "HousingMap::SpawnHouseForPlot: spawned Group A mirror[{}] {} " + "for plot {} (attach={} [room], type={}, tag={})", + pieceIndex, mirrorGuid.ToString(), plotIndex, roomParentGuid.ToString(), + compType, compType == 9 ? "PieceAndRoot" : "Piece"); + mirrors.push_back(std::move(mirror)); + ++pieceIndex; + } + } + + if (mirrors.empty()) + { + TC_LOG_ERROR("housing", "HousingMap::SpawnHouseForPlot: no fixture MeshObjects found for plot {}; " + "Group A mirrors skipped — FHousingPlayerHouse_C.EntityGUID will dangle and the " + "world-map icon will not resolve", plotIndex); + } + else + { + TC_LOG_DEBUG("housing", "HousingMap::SpawnHouseForPlot: emitted {} Group A mirrors for plot {} " + "(plotWorldPos=({:.2f},{:.2f},{:.2f}))", + mirrors.size(), plotIndex, + pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ()); + } } // Group B Entity mirrors — FMirroredPositionData_C only (no tags), @@ -2073,7 +2107,7 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo auto mirror = std::make_unique(this, mirrorGuid); mirror->InitPositionData(meshGuid, localPos, identity, /*scale*/ 1.0f, /*attachmentFlags*/ 3, - /*isExteriorRoot*/ false); + HousingMirrorEntity::Tagging::None); TC_LOG_DEBUG("housing", "HousingMap::SpawnHouseForPlot: spawned Group B mirror[{}] {} " "for plot {} (attach={} [mesh type={}])", pieceIndex, mirrorGuid.ToString(), plotIndex, meshGuid.ToString(), compType); @@ -3149,7 +3183,11 @@ void HousingMap::DespawnHouseForPlot(uint8 plotIndex) HousingMirrorEntity* HousingMap::GetHouseMirror(uint8 plotIndex) const { auto itr = _houseMirrorEntities.find(plotIndex); - return itr != _houseMirrorEntities.end() ? itr->second.get() : nullptr; + if (itr == _houseMirrorEntities.end() || itr->second.empty()) + return nullptr; + // Returns the Type-9 (Base) root mirror — the canonical mirror referenced + // by FHousingPlayerHouse_C.EntityGUID. Per-piece access via GetHouseMirrors. + return itr->second.front().get(); } ObjectGuid HousingMap::GetHouseMirrorGuid(uint8 plotIndex) const @@ -3159,17 +3197,33 @@ ObjectGuid HousingMap::GetHouseMirrorGuid(uint8 plotIndex) const return ObjectGuid::Empty; } -ObjectGuid HousingMap::MakeHouseMirrorGuid(uint8 plotIndex, uint32 bnetAccountId) const +std::vector HousingMap::GetHouseMirrors(uint8 plotIndex) const +{ + std::vector result; + auto itr = _houseMirrorEntities.find(plotIndex); + if (itr == _houseMirrorEntities.end()) + return result; + result.reserve(itr->second.size()); + for (auto const& mirror : itr->second) + result.push_back(mirror.get()); + return result; +} + +ObjectGuid HousingMap::MakeHouseMirrorGuid(uint8 plotIndex, uint32 bnetAccountId, uint8 pieceIndex /*= 0*/) const { // Deterministic convention: HighGuid::Entity, mapId=GetId() (neighborhood // world map), entry=37361 (synthetic entry for housing mirrors — picked - // outside the range of creature/gameobject entries in use), counter packs - // plotIndex into the low byte and bnetAccountId into the upper bits so it - // stays unique per (plot, owner). realmId/serverId from the current realm - // and map context. Same derivation can be used by proxy emission without - // needing the mirror object to exist. + // outside the range of creature/gameobject entries in use). Counter packs + // (bnetAccountId, plotIndex, pieceIndex) so each per-piece Group A mirror + // has a unique GUID and the mapping stays deterministic across runs. + // pieceIndex 0 is the Type-9 root mirror (canonical "house mirror GUID" + // referenced by FHousingPlayerHouse_C.EntityGUID); 1+ are Roof/Door/Window + // pieces. Same derivation is used by proxy emission for neighbour plots + // without needing the mirror object to exist. constexpr uint32 HOUSING_MIRROR_ENTRY = 37361; - uint64 counter = (static_cast(bnetAccountId) << 8) | static_cast(plotIndex); + uint64 counter = (static_cast(bnetAccountId) << 16) + | (static_cast(plotIndex) << 8) + | static_cast(pieceIndex); return ObjectGuid::Create(GetId(), HOUSING_MIRROR_ENTRY, counter); } diff --git a/src/server/game/Housing/HousingMap.h b/src/server/game/Housing/HousingMap.h index 3d28e670c..9de6c8238 100644 --- a/src/server/game/Housing/HousingMap.h +++ b/src/server/game/Housing/HousingMap.h @@ -90,10 +90,20 @@ public: // docs/HOUSING_ENTITY_MIRROR.md (pending) / memory/housing_entity_mirror_architecture.md. HousingMirrorEntity* GetHouseMirror(uint8 plotIndex) const; ObjectGuid GetHouseMirrorGuid(uint8 plotIndex) const; + // Full list of per-piece Group A mirrors for this plot (one per visible + // exterior fixture MeshObject, Type 9/10/11/12). Returns empty if no + // house spawned. Index 0 is the Type-9 root (PieceAndRoot tags); the + // others (Piece tag only) come from Roof/Door/Window pieces in spawn + // order. The root mirror's GUID is the canonical "house mirror GUID" + // referenced by FHousingPlayerHouse_C.EntityGUID. + std::vector GetHouseMirrors(uint8 plotIndex) const; // Deterministic mirror-GUID derivation that does not require the mirror to // exist yet — used by proxy emission for neighbour plots whose plot index - // and bnet owner are known from NeighborhoodMirror data. - ObjectGuid MakeHouseMirrorGuid(uint8 plotIndex, uint32 bnetAccountId) const; + // and bnet owner are known from NeighborhoodMirror data. pieceIndex defaults + // to 0 (the Type-9 root mirror) which is the canonical GUID referenced by + // FHousingPlayerHouse_C.EntityGUID; pieceIndex 1+ identify the non-root + // Group A mirrors (Roof/Door/Window). + ObjectGuid MakeHouseMirrorGuid(uint8 plotIndex, uint32 bnetAccountId, uint8 pieceIndex = 0) const; // "Group B" per-piece mesh-level mirrors. Retail pairs EACH visible // exterior fixture MeshObject (Base/Roof/Door/Window — ExteriorComponent @@ -211,10 +221,15 @@ private: // House structure GO tracking (plotIndex -> house GO GUID) std::unordered_map _houseGameObjects; - // House-exterior root Entity mirror (HighGuid::Entity, objectType=18) - // tracking (plotIndex -> mirror entity). Owned by the map; lifecycle - // 1:1 with the exterior root MeshObject. - std::unordered_map> _houseMirrorEntities; + // Group A house-exterior Entity mirrors (HighGuid::Entity, objectType=18) + // — one per visible exterior fixture MeshObject (Type 9/10/11/12). The + // Type-9 (Base) entry at index 0 carries both Tag_HouseExteriorPiece + + // Tag_HouseExteriorRoot and is the GUID referenced by every external + // proxy (FHousingPlayerHouse_C.EntityGUID). The other three carry + // Tag_HouseExteriorPiece only. Co-spawned with the house and despawned + // together. Vector since retail emits 4 of these per plot (sniff idx + // 9984 in dump_12.0.1.66838_2026-04-15_09-35-59). + std::unordered_map>> _houseMirrorEntities; // "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