Housing: Fix edit mode decor list by sending all MeshObject CREATEs in same packet

The Placed Decor list was empty or incomplete because the client correlates
MeshObject FHousingDecor_C.DecorGUID with Account FHousingStorage_C entries
only when both arrive together. MeshObjects created via normal grid visibility
(separate earlier packets) arrived before FHousingStorage_C was populated,
so the client never associated them with decor entries.

Key fixes:
- Always send Account entity as CREATE (not VALUES_UPDATE) on edit mode entry
  since the initial login CREATE has no FHousingStorage_C data
- Re-send CREATE for ALL decor MeshObjects in the same UPDATE_OBJECT packet
  as the Account entity, ensuring the client has complete correlation data
- Add plot boundary spell visual activation on edit mode entry
- Improve edit mode diagnostics with mesh tracking counters
- Fix various housing packet and neighborhood handler improvements
This commit is contained in:
luis
2026-03-12 07:56:50 -03:00
parent 4f7372c043
commit 1a7d471b08
10 changed files with 194 additions and 51 deletions
@@ -1653,6 +1653,19 @@ void AreaTrigger::InitHousingPlotData(uint32 plotId, ObjectGuid ownerGuid, Objec
.ModifyValue(&UF::AreaTriggerActionSetPeriodModifier::Field_4), 1.0f);
}
// Set ExtraScaleCurve to match retail: ParameterCurve=0x3F800001, OverrideActive=true.
// Sniff-verified across 6 instances in builds 66102/66263 ? all housing plot ATs
// have this exact ExtraScaleCurve configuration (all other curve fields remain zero).
// This tells the client to flatten the terrain and remove grass within the plot
// boundary, preparing the surface for decor placement.
{
auto areaTriggerData = m_values.ModifyValue(&AreaTrigger::m_areaTriggerData);
SetUpdateFieldValue(areaTriggerData.ModifyValue(&UF::AreaTriggerData::ExtraScaleCurve)
.ModifyValue(&UF::ScaleCurve::ParameterCurve), uint32(0x3F800001));
SetUpdateFieldValue(areaTriggerData.ModifyValue(&UF::AreaTriggerData::ExtraScaleCurve)
.ModifyValue(&UF::ScaleCurve::OverrideActive), true);
}
TC_LOG_ERROR("housing", "AreaTrigger::InitHousingPlotData: AT {} plot={} owner={} houseGuid={} bnetGuid={}"
" | SpellForVisuals={} SpellXSpellVisualID={} DecalPropertiesID={}"
" | ShapeType={} BoundsRadius2D={:.2f} PeriodModifier=({},{})"
+15 -4
View File
@@ -19004,9 +19004,20 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
}
// Populate initiative data for the player's neighborhood
// Try housing first, then fall back to neighborhood membership
ObjectGuid initNhGuid;
if (!_housings.empty() && _housings[0] && !_housings[0]->GetNeighborhoodGuid().IsEmpty())
initNhGuid = _housings[0]->GetNeighborhoodGuid();
else
{
ObjectGuid nhGuid = _housings[0]->GetNeighborhoodGuid();
auto neighborhoods = sNeighborhoodMgr.GetNeighborhoodsForPlayer(GetGUID());
if (!neighborhoods.empty())
initNhGuid = neighborhoods[0]->GetGuid();
}
if (!initNhGuid.IsEmpty())
{
ObjectGuid nhGuid = initNhGuid;
uint64 nhLowGuid = nhGuid.GetCounter();
ActiveInitiative* activeInit = sInitiativeManager.GetActiveInitiative(nhLowGuid);
@@ -19022,16 +19033,16 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
// Check both NeighborhoodInitiative.Duration and InitiativeCycle.Duration.
// If neither provides a duration, use a 7-day default so the client shows
// the endeavor as active rather than expired (Duration=0 ? hidden).
// Duration from DB2 is in days ? convert to seconds.
// DB2 Duration is already in seconds (NOT days).
// Sniff-verified: RemainingDuration is in seconds (sniff value 972957 ? 11.25 days).
int64 durationSec = 0;
if (initEntry && initEntry->Duration > 0)
durationSec = static_cast<int64>(initEntry->Duration) * 86400;
durationSec = static_cast<int64>(initEntry->Duration);
else if (cycleID)
{
InitiativeCycleEntry const* cycleEntry = sInitiativeCycleStore.LookupEntry(cycleID);
if (cycleEntry && cycleEntry->Duration > 0)
durationSec = static_cast<int64>(cycleEntry->Duration) * 86400;
durationSec = static_cast<int64>(cycleEntry->Duration);
}
if (durationSec <= 0)
durationSec = 7 * DAY; // 7-day fallback
+62 -23
View File
@@ -529,16 +529,14 @@ void WorldSession::HandleHousingDecorSetEditMode(WorldPackets::Housing::HousingD
// Player VALUES_UPDATE (EditorMode=1 + UNIT_FLAG_PACIFIED + UNIT_FLAG2_NO_ACTIONS)
player->BuildValuesUpdateBlockForPlayer(&updateData, player);
// Account + HousingPlayerHouseEntity: send CREATE if the client doesn't
// have them (they get destroyed during map transfers to the housing map),
// otherwise send VALUES_UPDATE.
if (player->HaveAtClient(&GetBattlenetAccount()))
GetBattlenetAccount().BuildValuesUpdateBlockForPlayer(&updateData, player);
else
{
GetBattlenetAccount().BuildCreateUpdateBlockForPlayer(&updateData, player);
player->m_clientGUIDs.insert(GetBattlenetAccount().GetGUID());
}
// Account entity: ALWAYS send CREATE (not VALUES_UPDATE) when entering edit mode.
// The initial Account CREATE (during login's SendInitSelf) has NO FHousingStorage_C
// data. PopulateCatalogStorageEntries() added Decor map entries above, and sending
// a VALUES_UPDATE for a MapUpdateField that was empty at CREATE time may not
// properly convey the new entries to the client. CREATE includes all current values.
// The client handles receiving a second CREATE for an existing entity gracefully.
GetBattlenetAccount().BuildCreateUpdateBlockForPlayer(&updateData, player);
player->m_clientGUIDs.insert(GetBattlenetAccount().GetGUID());
if (player->HaveAtClient(&GetHousingPlayerHouseEntity()))
GetHousingPlayerHouseEntity().BuildValuesUpdateBlockForPlayer(&updateData, player);
@@ -548,6 +546,30 @@ void WorldSession::HandleHousingDecorSetEditMode(WorldPackets::Housing::HousingD
player->m_clientGUIDs.insert(GetHousingPlayerHouseEntity().GetGUID());
}
// Include CREATE for ALL decor MeshObjects in this same UPDATE_OBJECT packet.
// The client correlates MeshObject FHousingDecor_C.DecorGUID with Account
// FHousingStorage_C entries to build the Placed Decor list. MeshObjects that
// were CREATEd via normal grid visibility (separate earlier packet) arrived
// BEFORE FHousingStorage_C was populated, so the client doesn't associate them
// with decor entries. Re-sending CREATE in this packet (alongside the Account
// entity) ensures the client has all data in the same context.
if (HousingMap* housingMap = dynamic_cast<HousingMap*>(player->GetMap()))
{
uint32 meshCreateCount = 0;
for (auto const& [decorGuid, meshObjGuid] : housingMap->GetDecorGuidMap())
{
MeshObject* meshObj = housingMap->GetMeshObject(meshObjGuid);
if (!meshObj || !meshObj->IsInWorld())
continue;
meshObj->BuildCreateUpdateBlockForPlayer(&updateData, player);
player->m_clientGUIDs.insert(meshObjGuid);
++meshCreateCount;
}
if (meshCreateCount > 0)
TC_LOG_ERROR("housing", " EditMode: Force-sent {} decor MeshObject CREATEs to player {}", meshCreateCount, player->GetGUID().ToString());
}
updateData.BuildPacket(&updatePacket);
player->SendDirectMessage(&updatePacket);
@@ -564,35 +586,52 @@ void WorldSession::HandleHousingDecorSetEditMode(WorldPackets::Housing::HousingD
if (HousingMap* housingMap = dynamic_cast<HousingMap*>(player->GetMap()))
{
uint32 meshDecorCount = 0;
uint32 meshInWorld = 0;
uint32 meshHasFrag = 0;
uint32 meshAtClient = 0;
for (auto const& [decorGuid, meshObjGuid] : housingMap->GetDecorGuidMap())
{
MeshObject* meshObj = housingMap->GetMeshObject(meshObjGuid);
ObjectGuid meshDecorGuid;
if (meshObj && meshObj->HasHousingDecorData())
meshDecorGuid = *meshObj->m_housingDecorData->DecorGUID;
bool inWorld = meshObj && meshObj->IsInWorld();
bool hasFrag = meshObj && meshObj->HasHousingDecorData();
bool atClient = player->m_clientGUIDs.count(meshObjGuid) > 0;
if (inWorld) ++meshInWorld;
if (hasFrag) ++meshHasFrag;
if (atClient) ++meshAtClient;
TC_LOG_INFO("housing", " [DIAG] MeshDecor: mapKey={} meshGuid={} fragmentDecorGuid={} hasFrag={} inWorld={}",
decorGuid.ToString(), meshObjGuid.ToString(),
meshDecorGuid.ToString(),
meshObj ? meshObj->HasHousingDecorData() : false,
meshObj ? meshObj->IsInWorld() : false);
TC_LOG_ERROR("housing", " [DIAG] MeshDecor: decorKey={} meshGuid={} inWorld={} hasFrag={} atClient={}",
decorGuid.ToString(), meshObjGuid.ToString(), inWorld, hasFrag, atClient);
++meshDecorCount;
}
// Also log the placed decor GUIDs from the Housing object (what's in the Account storage)
uint32 exteriorCount = 0;
uint32 matchCount = 0;
for (auto const& [decorGuid, decor] : housing->GetPlacedDecorMap())
{
bool isExterior = decor.RoomGuid.IsEmpty();
bool hasMeshObject = housingMap->GetDecorGuidMap().count(decorGuid) > 0;
TC_LOG_INFO("housing", " [DIAG] HousingDecor: decorGuid={} entryId={} exterior={} hasMesh={}",
decorGuid.ToString(), decor.DecorEntryId,
decor.RoomGuid.IsEmpty(), hasMeshObject);
if (isExterior) ++exteriorCount;
if (isExterior && hasMeshObject) ++matchCount;
TC_LOG_ERROR("housing", " [DIAG] HousingDecor: decorGuid={} entryId={} exterior={} hasMesh={}",
decorGuid.ToString(), decor.DecorEntryId, isExterior, hasMeshObject);
}
TC_LOG_INFO("housing", " [DIAG] Summary: meshDecor={} housingPlaced={} storagePop={}",
meshDecorCount, uint32(housing->GetPlacedDecorMap().size()),
TC_LOG_ERROR("housing", " [DIAG] Summary: meshTracked={} meshInWorld={} meshHasFrag={} meshAtClient={} "
"housingPlaced={} exterior={} matched={} storagePop={}",
meshDecorCount, meshInWorld, meshHasFrag, meshAtClient,
uint32(housing->GetPlacedDecorMap().size()), exteriorCount, matchCount,
housing->IsStoragePopulated());
}
// Play the plot boundary spell visual on the player's plot AT.
// This activates the glowing border decal around the plot when in edit mode.
if (HousingMap* housingMap = dynamic_cast<HousingMap*>(player->GetMap()))
{
if (AreaTrigger* plotAt = housingMap->GetPlotAreaTrigger(housing->GetPlotIndex()))
plotAt->PlaySpellVisual(510142);
}
TC_LOG_DEBUG("housing", " EditMode ENTER: PlayerGUID={} BNetAccountGuid={}",
player->GetGUID().ToString(), response.BNetAccountGuid.ToString());
}
@@ -1921,14 +1921,22 @@ void WorldSession::HandleNeighborhoodInitiativeServiceStatusCheck(WorldPackets::
// Without this, GetNeighborhoodInitiativeInfo() returns nil and the endeavor UI is empty.
// Sniff-verified: the live server sends this SMSG proactively (not only in response to
// CMSG_INITIATIVE_UPDATE_ACTIVE_NEIGHBORHOOD which the client rarely/never sends).
ObjectGuid nhObjGuid;
if (Housing* housing = player->GetHousing())
nhObjGuid = housing->GetNeighborhoodGuid();
// Fallback: if player has no house, find their neighborhood via membership
if (nhObjGuid.IsEmpty())
{
ObjectGuid nhObjGuid = housing->GetNeighborhoodGuid();
if (!nhObjGuid.IsEmpty())
{
uint64 nhGuid = nhObjGuid.GetCounter();
sInitiativeManager.SendPlayerInitiativeInfo(this, nhObjGuid, nhGuid);
}
auto neighborhoods = sNeighborhoodMgr.GetNeighborhoodsForPlayer(player->GetGUID());
if (!neighborhoods.empty())
nhObjGuid = neighborhoods[0]->GetGuid();
}
if (!nhObjGuid.IsEmpty())
{
uint64 nhGuid = nhObjGuid.GetCounter();
sInitiativeManager.SendPlayerInitiativeInfo(this, nhObjGuid, nhGuid);
}
}
@@ -804,6 +804,9 @@ bool HouseInteriorMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/
initStatus.ServiceEnabled = true;
player->SendDirectMessage(initStatus.Write());
}
// Toggle WS[30906]=1 to signal the client that the player is inside a house interior.
player->SendUpdateWorldState(WORLDSTATE_HOUSING_INTERIOR, 1);
}
else
{
@@ -829,6 +832,9 @@ void HouseInteriorMap::RemovePlayerFromMap(Player* player, bool remove)
if (housing)
housing->SetInInterior(false);
// Toggle WS[30906]=0 to signal the client that the player left the house interior.
player->SendUpdateWorldState(WORLDSTATE_HOUSING_INTERIOR, 0);
TC_LOG_ERROR("housing", "HouseInteriorMap::RemovePlayerFromMap: Player {} leaving interior "
"(owner={}, map={}, instanceId={}, _roomsSpawned={}, roomMeshEntries={}, decorEntries={}, this={})",
player->GetGUID().ToString(), _owner.ToString(), GetId(), GetInstanceId(),
+3
View File
@@ -758,6 +758,9 @@ static constexpr uint32 WORLDSTATE_HOUSING_COUNTER_3 = 13438;
static constexpr uint32 WORLDSTATE_HOUSING_COUNTER_4 = 16035;
static constexpr uint32 WORLDSTATE_HOUSING_COUNTER_5 = 16711;
// WS[30906]: Toggled 1 when inside a house interior (MapID=2783), 0 when leaving.
static constexpr uint32 WORLDSTATE_HOUSING_INTERIOR = 30906;
// Interval and increment for housing WorldState counter updates
static constexpr uint32 HOUSING_WORLDSTATE_INTERVAL_MS = 300;
static constexpr uint32 HOUSING_WORLDSTATE_INCREMENT = 1333;
+14
View File
@@ -730,6 +730,14 @@ bool HousingMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/)
if (!Map::AddPlayerToMap(player, initPlayer))
return false;
// Force immediate visibility update so all MeshObjects (house pieces, decor) get
// CREATE_OBJECT sent to the player NOW, not deferred to the next map tick.
// Map::AddPlayerToMap calls UpdateObjectVisibility(false) which only sets
// NOTIFY_VISIBILITY_CHANGED ? the actual grid visit is deferred until the next
// relocation processing tick. Without forcing here, the client won't have MeshObject
// entities when entering edit mode, causing an empty Placed Decor list.
player->UpdateVisibilityForPlayer();
// === DIAGNOSTIC: Report plot GO state when player enters ===
{
TC_LOG_DEBUG("housing", "=== HOUSING DIAGNOSTIC for player {} entering map {} ===", player->GetGUID().ToString(), GetId());
@@ -2454,7 +2462,13 @@ void HousingMap::SpawnAllDecorForPlot(uint8 plotIndex, Housing const* housing)
return;
if (_decorSpawnedPlots.count(plotIndex))
{
TC_LOG_ERROR("housing", "HousingMap::SpawnAllDecorForPlot: Plot {} already in _decorSpawnedPlots ? skipping respawn "
"(decorGuidMap.size={} decorGOs[{}].size={})",
plotIndex, uint32(_decorGuidToGoGuid.size()),
plotIndex, _decorGameObjects.count(plotIndex) ? uint32(_decorGameObjects[plotIndex].size()) : 0);
return; // Already spawned
}
ObjectGuid houseGuid = housing->GetHouseGuid();
uint32 spawnCount = 0;
@@ -801,7 +801,7 @@ void InitiativeManager::SendPlayerInitiativeInfo(WorldSession* session, ObjectGu
if (auto cycleIt = _initiativeActiveCycle.find(active->InitiativeID); cycleIt != _initiativeActiveCycle.end())
{
if (InitiativeCycleEntry const* cycle = sInitiativeCycleStore.LookupEntry(cycleIt->second))
totalDurationSec = static_cast<int64>(cycle->Duration) * 86400;
totalDurationSec = static_cast<int64>(cycle->Duration); // DB2 Duration is already in seconds
}
// Fallback: if DB2 has no duration, use 7 days
if (totalDurationSec <= 0)
+44 -13
View File
@@ -126,6 +126,33 @@ void NeighborhoodMgr::LoadFromDB()
} while (result->NextRow());
TC_LOG_INFO("server.loading", ">> Loaded {} neighborhoods in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
// Debug dump all neighborhoods and their plot states
for (auto const& [guid, neighborhood] : _neighborhoods)
{
TC_LOG_INFO("housing", "NEIGHBORHOOD_DUMP: guid={} name='{}' mapId={} faction={} public={} "
"members={} occupiedPlots={} owner={}",
guid.ToString(), neighborhood->GetName(), neighborhood->GetNeighborhoodMapID(),
neighborhood->GetFactionRestriction(), neighborhood->IsPublic(),
neighborhood->GetMemberCount(), neighborhood->GetOccupiedPlotCount(),
neighborhood->GetOwnerGuid().ToString());
for (auto const& member : neighborhood->GetMembers())
{
TC_LOG_INFO("housing", " MEMBER: player={} role={} plotIndex={} houseGuid={}",
member.PlayerGuid.ToString(), member.Role, member.PlotIndex, member.HouseGuid.ToString());
}
for (uint8 i = 0; i < MAX_NEIGHBORHOOD_PLOTS; ++i)
{
auto const& plot = neighborhood->GetPlots()[i];
if (plot.IsOccupied())
{
TC_LOG_INFO("housing", " PLOT[{}]: owner={} house={} bnet={}",
i, plot.OwnerGuid.ToString(), plot.HouseGuid.ToString(), plot.OwnerBnetGuid.ToString());
}
}
}
}
Neighborhood* NeighborhoodMgr::CreateNeighborhood(ObjectGuid ownerGuid, std::string const& name, uint32 neighborhoodMapID, int32 factionRestriction, bool isPublic /*= false*/)
@@ -398,12 +425,12 @@ Neighborhood* NeighborhoodMgr::FindOrCreatePublicNeighborhood(uint32 teamId)
return nullptr;
}
// Look for an existing public neighborhood — no membership changes
// Look for an existing public neighborhood ? no membership changes
Neighborhood* found = FindPublicNeighborhoodForMap(targetMapId);
if (found)
return found;
// None exists yet — EnsurePublicNeighborhoods should have created them at startup.
// None exists yet ? EnsurePublicNeighborhoods should have created them at startup.
// Force-run it now as a fallback, then retry.
TC_LOG_WARN("housing", "FindOrCreatePublicNeighborhood: No public neighborhood for map {}, running EnsurePublicNeighborhoods", targetMapId);
EnsurePublicNeighborhoods();
@@ -465,7 +492,7 @@ void NeighborhoodMgr::VerifyNeighborhoodFactions()
correctFaction = NEIGHBORHOOD_FACTION_HORDE;
if (correctFaction == NEIGHBORHOOD_FACTION_NONE)
continue; // Ambiguous or no faction — skip
continue; // Ambiguous or no faction ? skip
int32 currentFaction = nb->GetFactionRestriction();
if (currentFaction == correctFaction)
@@ -549,7 +576,7 @@ void NeighborhoodMgr::EnsurePublicNeighborhoods()
if (hasAlliancePublic && hasHordePublic)
TC_LOG_INFO("server.loading", ">> Public neighborhoods verified for both factions");
else if (!hasAlliancePublic || !hasHordePublic)
TC_LOG_WARN("server.loading", ">> Missing public neighborhood for {} — no system-generatable NeighborhoodMap found",
TC_LOG_WARN("server.loading", ">> Missing public neighborhood for {} ? no system-generatable NeighborhoodMap found",
!hasAlliancePublic ? "Alliance" : "Horde");
}
@@ -609,13 +636,13 @@ void NeighborhoodMgr::MigrateWrongFactionResidents()
fields[3].GetUInt8(),
fields[4].GetUInt32(),
fields[5].GetUInt8()
});
});
} while (result->NextRow());
// Build set of existing memberships for quick lookup: (playerGuid, nbGuid)
std::set<std::pair<uint64, uint64>> membershipSet;
for (auto const& m : allMembers)
membershipSet.insert({m.PlayerGuidLow, m.NbGuidLow});
membershipSet.insert({ m.PlayerGuidLow, m.NbGuidLow });
// Pre-populate used plots in each target neighborhood
std::set<uint8> usedPlotsInAlliance;
@@ -638,7 +665,7 @@ void NeighborhoodMgr::MigrateWrongFactionResidents()
if (m.NbGuidLow == correctNbLow)
continue; // Already in correct faction's neighborhood
bool alreadyInCorrect = membershipSet.count({m.PlayerGuidLow, correctNbLow}) > 0;
bool alreadyInCorrect = membershipSet.count({ m.PlayerGuidLow, correctNbLow }) > 0;
// Delete old wrong-faction membership
CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_NEIGHBORHOOD_MEMBER);
@@ -648,7 +675,7 @@ void NeighborhoodMgr::MigrateWrongFactionResidents()
if (!alreadyInCorrect)
{
// Player doesn't have a membership in the correct neighborhood yet — create one
// Player doesn't have a membership in the correct neighborhood yet ? create one
std::set<uint8>& usedPlots = (correctNbLow == allianceNbLow) ? usedPlotsInAlliance : usedPlotsInHorde;
uint8 newPlotIndex = m.PlotIndex;
@@ -706,7 +733,7 @@ void NeighborhoodMgr::MigrateWrongFactionResidents()
if (migratedCount > 0)
{
TC_LOG_INFO("server.loading", ">> Migrated {} resident(s) to correct faction neighborhoods — reloading", migratedCount);
TC_LOG_INFO("server.loading", ">> Migrated {} resident(s) to correct faction neighborhoods ? reloading", migratedCount);
LoadFromDB(); // Reload to pick up the changes
}
}
@@ -821,7 +848,7 @@ void NeighborhoodMgr::CheckAndExpandNeighborhoods()
uint32 occupiedPlots = neighborhood->GetOccupiedPlotCount();
uint32 memberCount = neighborhood->GetMemberCount();
// Check both occupied plots AND member count — either can be the bottleneck.
// Check both occupied plots AND member count ? either can be the bottleneck.
// A neighborhood might have many members (invited/added) but few plots occupied,
// or vice versa. Use the higher of the two for capacity assessment.
uint32 usage = std::max(occupiedPlots, memberCount);
@@ -837,7 +864,7 @@ void NeighborhoodMgr::CheckAndExpandNeighborhoods()
if (hasCapacity)
continue;
// All public neighborhoods for this faction are at or above 50% — create a new one
// All public neighborhoods for this faction are at or above 50% ? create a new one
// Find the correct NeighborhoodMapID for this faction
uint32 targetMapId = 0;
for (auto const& [id, data] : sHousingMgr.GetAllNeighborhoodMapData())
@@ -851,9 +878,13 @@ void NeighborhoodMgr::CheckAndExpandNeighborhoods()
continue;
if (faction == NEIGHBORHOOD_FACTION_ALLIANCE && isAlliance)
{ targetMapId = id; break; }
{
targetMapId = id; break;
}
else if (faction == NEIGHBORHOOD_FACTION_HORDE && isHorde)
{ targetMapId = id; break; }
{
targetMapId = id; break;
}
}
if (targetMapId == 0)
@@ -1646,8 +1646,14 @@ namespace WorldPackets::Housing
for (auto const& entry : Entries)
WriteJamCliHouseFinderNeighborhood(_worldPacket, entry);
TC_LOG_DEBUG("network.opcode", "SMSG_HOUSING_SVCS_GET_HOUSE_FINDER_INFO_RESPONSE Result: {} EntryCount: {}",
Result, Entries.size());
TC_LOG_INFO("housing", "SMSG_HOUSING_SVCS_GET_HOUSE_FINDER_INFO_RESPONSE Result: {} EntryCount: {} PacketSize: {}",
Result, Entries.size(), _worldPacket.size());
for (size_t i = 0; i < Entries.size(); ++i)
{
auto const& e = Entries[i];
TC_LOG_INFO("housing", " LIST_ENTRY[{}]: nbGuid={} houses={} Field1=0x{:016X} Field2={} ExtraFlags=0x{:02X}",
i, e.NeighborhoodGUID.ToString(), e.Houses.size(), e.Field1, e.Field2, e.ExtraFlags);
}
return &_worldPacket;
}
@@ -1658,8 +1664,20 @@ namespace WorldPackets::Housing
_worldPacket << uint8(Result);
WriteJamCliHouseFinderNeighborhood(_worldPacket, Neighborhood);
TC_LOG_DEBUG("network.opcode", "SMSG_HOUSING_SVCS_GET_HOUSE_FINDER_NEIGHBORHOOD_RESPONSE Result: {} Houses: {}",
Result, Neighborhood.Houses.size());
TC_LOG_INFO("housing", "SMSG_HOUSING_SVCS_GET_HOUSE_FINDER_NEIGHBORHOOD_RESPONSE Result: {} Houses: {} PacketSize: {}",
Result, Neighborhood.Houses.size(), _worldPacket.size());
// Hex dump of the first 256 bytes for wire format verification
std::string hexDump;
size_t dumpLen = std::min<size_t>(_worldPacket.size(), 256);
for (size_t i = 0; i < dumpLen; ++i)
{
char buf[4];
snprintf(buf, sizeof(buf), "%02X ", _worldPacket[i]);
hexDump += buf;
if ((i + 1) % 32 == 0) hexDump += "\n ";
}
TC_LOG_INFO("housing", " PACKET_HEX (first {} bytes):\n {}", dumpLen, hexDump);
return &_worldPacket;
}