Housing: Fix post-purchase dashboard with sniff-verified packet formats

Correct multiple packet format mismatches preventing the housing dashboard
from populating after buying a plot:

- Fix HouseStatusResponse wire format: 3 PackedGUIDs + uint32 (was 4 + 2 uint8s)
- Fix PlayerHousesInfoResponse to use JamCurrentHouseInfo with correct field
  mapping: OwnerGuid=HouseGUID, SecondaryOwnerGuid=PlotGUID,
  PlotGuid=NeighborhoodGUID, Flags=PlotIndex, HouseTypeId=32
- Add PlotGUID generation (HighGuid::Housing subType=2) to Housing class
- Send FirstTimeDecorAcquisition packets for starter decor items after purchase
- Send 2x UpdateHousesLevelFavor with 36-byte format after purchase
- Fix owner permissions to 0xE0 (bits 5,6,7) instead of 0xFF
- Add serverside spell 1266097 for cornerstone UILink click handling
- Fix cornerstone GO interaction to cast spell instead of direct handler call
- Add createTime tracking to Housing for optional HouseId field
- Register housing spell script and new opcode handlers
This commit is contained in:
luis
2026-02-20 21:34:48 -03:00
parent 76942f6735
commit 32dad27a0a
24 changed files with 1195 additions and 472 deletions
@@ -805,8 +805,8 @@ void CharacterDatabaseConnection::DoPrepareStatements()
PrepareStatement(CHAR_INS_CHARACTER_BANK_TAB_SETTINGS, "INSERT INTO character_bank_tab_settings (characterGuid, tabId, name, icon, description, depositFlags) VALUES (?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC);
//WowCommunity
// Housing
PrepareStatement(CHAR_SEL_CHARACTER_HOUSING, "SELECT houseId, neighborhoodGuid, plotIndex, houseLevel, favor, settingsFlags, exteriorLocked, houseSize, houseType FROM character_housing WHERE guid = ?", CONNECTION_ASYNC);
// Housing
PrepareStatement(CHAR_SEL_CHARACTER_HOUSING, "SELECT houseId, neighborhoodGuid, plotIndex, houseLevel, favor, settingsFlags, exteriorLocked, houseSize, houseType, createTime FROM character_housing WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_INS_CHARACTER_HOUSING, "INSERT INTO character_housing (guid, houseId, neighborhoodGuid, plotIndex, houseLevel, favor, settingsFlags, exteriorLocked, houseSize, houseType, createTime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UNIX_TIMESTAMP())", CONNECTION_ASYNC);
PrepareStatement(CHAR_INS_CHARACTER_HOUSING, "INSERT INTO character_housing (guid, houseId, neighborhoodGuid, plotIndex, houseLevel, favor, settingsFlags, createTime) VALUES (?, ?, ?, ?, ?, ?, ?, UNIX_TIMESTAMP())", CONNECTION_ASYNC);
PrepareStatement(CHAR_DEL_CHARACTER_HOUSING, "DELETE FROM character_housing WHERE guid = ?", CONNECTION_ASYNC);
+2 -2
View File
@@ -5184,10 +5184,10 @@ struct RoomComponentEntry
struct RoomComponentOptionEntry
{
uint32 ID;
int32 RoomComponentID;
uint8 Type;
int32 ModelFileDataID;
uint8 SubType;
int32 ModelFileDataID;
int32 RoomComponentID;
int32 MeshStyleFilterID;
int32 HouseThemeID;
int32 Flags;
@@ -3385,6 +3385,14 @@ void GameObject::Use(Unit* user, bool ignoreCastInProgress /*= false*/)
if (!player)
return;
TC_LOG_DEBUG("housing", "GameObject::Use(GAMEOBJECT_TYPE_UI_LINK): entry={} guid={} "
"UILinkType={} PlayerInteractionType={} spell={} player={}",
GetEntry(), GetGUID().ToString(),
GetGOInfo()->UILink.UILinkType,
GetGOInfo()->UILink.PlayerInteractionType,
GetGOInfo()->UILink.spell,
player->GetGUID().ToString());
if (GetGOInfo()->UILink.PlayerInteractionType)
{
WorldPackets::NPC::NPCInteractionOpenResult npcInteraction;
@@ -3393,8 +3401,23 @@ void GameObject::Use(Unit* user, bool ignoreCastInProgress /*= false*/)
npcInteraction.Success = true;
player->SendDirectMessage(npcInteraction.Write());
if (uint32 spellId = GetGOInfo()->UILink.spell)
TC_LOG_DEBUG("housing", " -> Sent SMSG_NPC_INTERACTION_OPEN_RESULT: npc={} interactionType={} success=true",
GetGUID().ToString(), GetGOInfo()->UILink.PlayerInteractionType);
uint32 spellId = GetGOInfo()->UILink.spell;
// Per-plot cornerstone GOs from DB2 CASC data have spell=0 in their
// template. The master template (entry 457142) has Data8=1266097 but
// the actual per-plot entries do not. Fall back to the known spell
// for CornerstoneInteraction (type 70).
if (!spellId && GetGOInfo()->UILink.PlayerInteractionType == 70)
spellId = 1266097; // [DNT] Trigger Convo for Unowned Plot
if (spellId)
{
TC_LOG_DEBUG("housing", " -> Casting spell {} on player", spellId);
player->CastSpell(player, spellId, true);
}
}
else
{
@@ -4698,4 +4721,9 @@ void GameObject::InitHousingCornerstoneData(uint64 cost, int32 plotIndex)
m_entityFragments.Add(WowCS::EntityFragment::FJamHousingCornerstone_C, IsInWorld(),
WowCS::GetRawFragmentData(m_housingCornerstoneData));
TC_LOG_DEBUG("housing", "GameObject::InitHousingCornerstoneData: entry={} guid={} cost={} plotIndex={} "
"isInWorld={} fragmentCount={} updateableCount={}",
GetEntry(), GetGUID().ToString(), cost, plotIndex,
IsInWorld(), m_entityFragments.Count, m_entityFragments.UpdateableCount);
}
+73 -9
View File
@@ -1990,7 +1990,20 @@ GameObject* Player::GetGameObjectIfCanInteractWith(ObjectGuid const& guid) const
return nullptr;
if (!go->IsWithinDistInMap(this))
{
// Debug: log interaction failures for housing cornerstones (type 48 = UI_LINK)
if (go->GetGoType() == GAMEOBJECT_TYPE_UI_LINK)
{
TC_LOG_DEBUG("housing", "Player::GetGameObjectIfCanInteractWith FAILED (distance/phase): "
"player={} go entry={} guid={} displayId={} dist={:.1f} "
"inMap={} inPhase={} atInteractDist={}",
GetGUID().ToString(), go->GetEntry(), go->GetGUID().ToString(),
go->GetGOInfo()->displayId, GetExactDist(go),
go->IsInMap(this), go->InSamePhase(this),
go->IsAtInteractDistance(this));
}
return nullptr;
}
return go;
}
@@ -24794,8 +24807,28 @@ void Player::SendInitialPacketsBeforeAddToMap()
if (HousingMap* housingMap = dynamic_cast<HousingMap*>(GetMap()))
if (Neighborhood* neighborhood = housingMap->GetNeighborhood())
worldServerInfo.NeighborhoodGUID = neighborhood->GetGuid();
//WowCommunity
SendDirectMessage(worldServerInfo.Write());
WorldPacket const* wsiPkt = worldServerInfo.Write();
SendDirectMessage(wsiPkt);
TC_LOG_ERROR("housing", "=== SMSG_WORLD_SERVER_INFO (login) ===\n"
" DifficultyID={}, IsTournament={}, XRealmPvp={}, BlockExit={}\n"
" HouseGUID: {} (lo={:016X} hi={:016X})\n"
" HouseOwnerAccountGUID: {} (lo={:016X} hi={:016X})\n"
" HouseCosmeticOwnerGUID: {} (lo={:016X} hi={:016X})\n"
" NeighborhoodGUID: {} (lo={:016X} hi={:016X})\n"
" Packet size={} bytes",
worldServerInfo.DifficultyID, worldServerInfo.IsTournamentRealm,
worldServerInfo.XRealmPvpAlert, worldServerInfo.BlockExitingLoadingScreen,
worldServerInfo.HouseGUID.ToString(),
worldServerInfo.HouseGUID.GetRawValue(0), worldServerInfo.HouseGUID.GetRawValue(1),
worldServerInfo.HouseOwnerAccountGUID.ToString(),
worldServerInfo.HouseOwnerAccountGUID.GetRawValue(0), worldServerInfo.HouseOwnerAccountGUID.GetRawValue(1),
worldServerInfo.HouseCosmeticOwnerGUID.ToString(),
worldServerInfo.HouseCosmeticOwnerGUID.GetRawValue(0), worldServerInfo.HouseCosmeticOwnerGUID.GetRawValue(1),
worldServerInfo.NeighborhoodGUID.ToString(),
worldServerInfo.NeighborhoodGUID.GetRawValue(0), worldServerInfo.NeighborhoodGUID.GetRawValue(1),
wsiPkt->size());
// Spell modifiers
SendSpellModifiers();
@@ -24950,13 +24983,11 @@ void Player::SendInitialPacketsAfterAddToMap()
if (housing)
{
statusResponse.HouseGuid = housing->GetHouseGuid();
statusResponse.OwnerBNetGuid = GetSession()->GetBattlenetAccountGUID();
statusResponse.OwnerPlayerGuid = GetGUID();
statusResponse.HouseStatus = 1; // Has house
statusResponse.PlotIndex = housing->GetPlotIndex();
statusResponse.StatusFlags = 0;
statusResponse.HouseTemplateGuid = ObjectGuid::Create<HighGuid::Housing>(3, 0, 7, 0);
statusResponse.PlotGuid = housing->GetPlotGuid();
statusResponse.Status = 0;
}
// No house: all fields stay at defaults (empty GUIDs, HouseStatus=0, PlotIndex=0xFF).
// No house: all fields stay at defaults (empty GUIDs, Status=0).
SendDirectMessage(statusResponse.Write());
// Populate NeighborhoodMirrorData on the Account entity so the
@@ -24989,6 +25020,27 @@ void Player::SendInitialPacketsAfterAddToMap()
}
}
// Proactively send the neighborhood name response BEFORE the roster.
// The client's NeighborhoodState singleton initializes all four display
// flags (+572..+575) to 1. Flag +574 is only cleared when the
// JamCliNeighborhoodName DataCache already contains the neighborhood
// name. By sending this packet first, we pre-populate that cache so
// the roster response's display function finds the name resolved.
{
WorldPackets::Housing::QueryNeighborhoodNameResponse nameResponse;
nameResponse.NeighborhoodGuid = neighborhood->GetGuid();
nameResponse.Allow = true;
nameResponse.Name = neighborhood->GetName();
SendDirectMessage(nameResponse.Write());
TC_LOG_ERROR("housing", "=== SMSG_QUERY_NEIGHBORHOOD_NAME_RESPONSE (0x460012) [login-preload] ===\n"
" Allow={}, Name='{}' (len={})\n"
" NeighborhoodGuid: {} (lo={:016X} hi={:016X})",
nameResponse.Allow, nameResponse.Name, nameResponse.Name.size(),
nameResponse.NeighborhoodGuid.ToString(),
nameResponse.NeighborhoodGuid.GetRawValue(0), nameResponse.NeighborhoodGuid.GetRawValue(1));
}
// Proactively send the roster so the client has plot occupancy data
// for the map without needing to request it.
WorldPackets::Neighborhood::NeighborhoodGetRosterResponse rosterResponse;
@@ -25009,7 +25061,19 @@ void Player::SendInitialPacketsAfterAddToMap()
data.HouseGuid = plotInfo->HouseGuid;
rosterResponse.Members.push_back(data);
}
SendDirectMessage(rosterResponse.Write());
WorldPacket const* loginRosterPkt = rosterResponse.Write();
SendDirectMessage(loginRosterPkt);
// Debug: log raw GUID bytes to verify roster packet populates handler context correctly
TC_LOG_ERROR("housing", "=== SMSG_NEIGHBORHOOD_GET_ROSTER_RESPONSE (0x5C0012) [login] ===\n"
" GroupNeighborhoodGuid: {} (lo={:016X} hi={:016X})\n"
" GroupOwnerGuid: {} (lo={:016X} hi={:016X})\n"
" NeighborhoodName='{}', Members={}, Packet size={} bytes",
rosterResponse.GroupNeighborhoodGuid.ToString(),
rosterResponse.GroupNeighborhoodGuid.GetRawValue(0), rosterResponse.GroupNeighborhoodGuid.GetRawValue(1),
rosterResponse.GroupOwnerGuid.ToString(),
rosterResponse.GroupOwnerGuid.GetRawValue(0), rosterResponse.GroupOwnerGuid.GetRawValue(1),
rosterResponse.NeighborhoodName, rosterResponse.Members.size(), loginRosterPkt->size());
TC_LOG_INFO("housing", "Player {} entered neighborhood map {} - sent HouseStatus + roster + NeighborhoodMirrorData (Neighborhood: '{}' {}, Members: {}, Plots: {}, HasHouse: {})",
GetGUID().ToString(), GetMapId(), neighborhood->GetName(), neighborhood->GetGuid().ToString(),
+207 -66
View File
@@ -29,10 +29,41 @@
#include "ObjectAccessor.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "CharacterCache.h"
#include "SocialMgr.h"
// ============================================================
// Decline Neighborhood Invites
// ============================================================
namespace
{
std::string HexDumpPacket(WorldPacket const* packet, size_t maxBytes = 128)
{
if (!packet || packet->size() == 0)
return "(empty)";
size_t len = std::min(packet->size(), maxBytes);
std::string result;
result.reserve(len * 3 + 32);
uint8 const* raw = packet->data();
for (size_t i = 0; i < len; ++i)
{
if (i > 0 && i % 32 == 0)
result += "\n ";
else if (i > 0)
result += ' ';
result += fmt::format("{:02X}", raw[i]);
}
if (len < packet->size())
result += fmt::format(" ...({} more)", packet->size() - len);
return result;
}
std::string GuidHex(ObjectGuid const& guid)
{
return fmt::format("lo={:016X} hi={:016X}", guid.GetRawValue(0), guid.GetRawValue(1));
}
}
// ============================================================
// Decline Neighborhood Invites
// ============================================================
void WorldSession::HandleDeclineNeighborhoodInvites(WorldPackets::Housing::DeclineNeighborhoodInvites const& declineNeighborhoodInvites)
{
@@ -119,11 +150,9 @@ void WorldSession::HandleHouseInteriorLeaveHouse(WorldPackets::Housing::HouseInt
// Send updated house status to acknowledge the interior exit
WorldPackets::Housing::HousingHouseStatusResponse response;
response.HouseGuid = housing->GetHouseGuid();
response.OwnerBNetGuid = GetBattlenetAccountGUID();
response.OwnerPlayerGuid = player->GetGUID();
response.HouseStatus = 1; // House active, not in interior
response.PlotIndex = housing->GetPlotIndex();
response.StatusFlags = 0;
response.HouseTemplateGuid = ObjectGuid::Create<HighGuid::Housing>(3, 0, 7, 0); // static template GUID
response.PlotGuid = housing->GetPlotGuid();
response.Status = 0;
SendPacket(response.Write());
TC_LOG_INFO("housing", "CMSG_HOUSE_INTERIOR_LEAVE_HOUSE: Player {} leaving house interior",
@@ -1122,14 +1151,24 @@ void WorldSession::HandleHousingSvcsPlayerViewHousesByBnetAccount(WorldPackets::
if (!player)
return;
// BNet account house lookup - uses the same player neighborhood query for now
// In a complete implementation this would query all characters on the BNet account
// Find all neighborhoods where the queried BNet account has a plot (owns a house)
std::vector<Neighborhood*> neighborhoods = sNeighborhoodMgr.GetNeighborhoodsByBnetAccount(housingSvcsPlayerViewHousesByBnetAccount.BnetAccountGuid);
WorldPackets::Housing::HousingSvcsPlayerViewHousesResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
response.Neighborhoods.reserve(neighborhoods.size());
for (Neighborhood const* neighborhood : neighborhoods)
{
WorldPackets::Housing::HousingSvcsPlayerViewHousesResponse::NeighborhoodInfoData info;
info.NeighborhoodGuid = neighborhood->GetGuid();
info.Name = neighborhood->GetName();
info.MapID = neighborhood->GetNeighborhoodMapID();
response.Neighborhoods.push_back(std::move(info));
}
SendPacket(response.Write());
TC_LOG_INFO("housing", "CMSG_HOUSING_SVCS_PLAYER_VIEW_HOUSES_BY_BNET_ACCOUNT BnetAccountGuid: {}",
housingSvcsPlayerViewHousesByBnetAccount.BnetAccountGuid.ToString());
TC_LOG_INFO("housing", "CMSG_HOUSING_SVCS_PLAYER_VIEW_HOUSES_BY_BNET_ACCOUNT BnetAccountGuid: {}, FoundNeighborhoods: {}",
housingSvcsPlayerViewHousesByBnetAccount.BnetAccountGuid.ToString(), uint32(neighborhoods.size()));
}
void WorldSession::HandleHousingSvcsGetPlayerHousesInfo(WorldPackets::Housing::HousingSvcsGetPlayerHousesInfo const& /*housingSvcsGetPlayerHousesInfo*/)
@@ -1143,11 +1182,19 @@ void WorldSession::HandleHousingSvcsGetPlayerHousesInfo(WorldPackets::Housing::H
WorldPackets::Housing::HousingSvcsGetPlayerHousesInfoResponse response;
for (Housing const* housing : player->GetAllHousings())
{
WorldPackets::Housing::HousingSvcsGetPlayerHousesInfoResponse::HouseInfoData info;
info.HouseGuid = housing->GetHouseGuid();
info.NeighborhoodGuid = housing->GetNeighborhoodGuid();
info.PlotIndex = housing->GetPlotIndex();
info.Level = static_cast<uint8>(housing->GetLevel());
WorldPackets::Housing::JamCurrentHouseInfo info;
// Sniff-verified: OwnerGuid=HouseGUID, SecondaryOwnerGuid=PlotGUID, PlotGuid=NeighborhoodGUID
info.OwnerGuid = housing->GetHouseGuid();
info.SecondaryOwnerGuid = housing->GetPlotGuid();
info.PlotGuid = housing->GetNeighborhoodGuid();
info.Flags = housing->GetPlotIndex();
info.HouseTypeId = 32;
// Set creation timestamp if available (sniff shows StatusFlags=0x80 with uint64 timestamp)
if (housing->GetCreateTime())
{
info.StatusFlags = 0x80;
info.HouseId = static_cast<uint64>(housing->GetCreateTime());
}
response.Houses.push_back(info);
}
SendPacket(response.Write());
@@ -1220,10 +1267,9 @@ void WorldSession::HandleHousingSvcsStartTutorial(WorldPackets::Housing::Housing
return;
// Step 1: Find or create a tutorial neighborhood for the player's faction.
// The tutorial assigns the player to a system-generated neighborhood so they
// have a place to buy a plot and acquire a house.
Neighborhood* neighborhood = sNeighborhoodMgr.FindOrCreateTutorialNeighborhood(
player->GetGUID(), player->GetTeam());
// The tutorial only needs a neighborhood to exist so the map instance can be
// created. It does NOT grant membership ? that happens when the player buys a plot.
Neighborhood* neighborhood = sNeighborhoodMgr.FindOrCreatePublicNeighborhood(player->GetTeam());
if (neighborhood)
{
@@ -1252,15 +1298,31 @@ void WorldSession::HandleHousingSvcsStartTutorial(WorldPackets::Housing::Housing
// Step 2: Auto-accept the "My First Home" quest (91863) so the player can
// progress through the tutorial by interacting with the steward NPC.
// Skip if already completed (account-wide warband quest) or already in quest log.
static constexpr uint32 QUEST_MY_FIRST_HOME = 91863;
if (Quest const* quest = sObjectMgr->GetQuestTemplate(QUEST_MY_FIRST_HOME))
{
if (player->CanAddQuest(quest, true))
QuestStatus status = player->GetQuestStatus(QUEST_MY_FIRST_HOME);
if (status == QUEST_STATUS_NONE)
{
player->AddQuestAndCheckCompletion(quest, nullptr);
TC_LOG_INFO("housing", "CMSG_HOUSING_SVCS_START_TUTORIAL: Auto-accepted quest {} for player {}",
// Quest not in log and not yet rewarded ? safe to add
if (player->CanAddQuest(quest, true))
{
player->AddQuestAndCheckCompletion(quest, nullptr);
TC_LOG_INFO("housing", "CMSG_HOUSING_SVCS_START_TUTORIAL: Auto-accepted quest {} for player {}",
QUEST_MY_FIRST_HOME, player->GetGUID().ToString());
}
}
else if (status == QUEST_STATUS_REWARDED)
{
TC_LOG_DEBUG("housing", "CMSG_HOUSING_SVCS_START_TUTORIAL: Quest {} already completed (warband) for player {}, skipping",
QUEST_MY_FIRST_HOME, player->GetGUID().ToString());
}
else
{
TC_LOG_DEBUG("housing", "CMSG_HOUSING_SVCS_START_TUTORIAL: Quest {} already in log (status {}) for player {}, skipping",
QUEST_MY_FIRST_HOME, uint32(status), player->GetGUID().ToString());
}
}
// Step 3: Teleport the player to the housing neighborhood via faction-specific spell.
@@ -1468,14 +1530,51 @@ void WorldSession::HandleHousingSvcsGetBnetFriendNeighborhoods(WorldPackets::Hou
if (!player)
return;
// BNet friend neighborhood lookup requires BNet social integration
// For now, acknowledge the request
PlayerSocial* social = player->GetSocial();
if (!social)
{
WorldPackets::Housing::HousingSvcsGetBnetFriendNeighborhoodsResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
SendPacket(response.Write());
return;
}
// Build response by iterating all neighborhoods and checking if any plot owner
// is on the requesting player's friend list. Each player can own at most one
// house per faction (2 houses max), so results are naturally bounded.
WorldPackets::Housing::HousingSvcsGetBnetFriendNeighborhoodsResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
std::vector<Neighborhood*> allNeighborhoods = sNeighborhoodMgr.GetAllNeighborhoods();
for (Neighborhood const* neighborhood : allNeighborhoods)
{
for (auto const& plot : neighborhood->GetPlots())
{
if (!plot.IsOccupied() || plot.OwnerGuid.IsEmpty())
continue;
// Check if the plot owner is a friend of the requesting player
if (!social->HasFriend(plot.OwnerGuid))
continue;
// Resolve the friend's character name
std::string friendName;
if (!sCharacterCache->GetCharacterNameByGuid(plot.OwnerGuid, friendName))
continue;
WorldPackets::Housing::HousingSvcsGetBnetFriendNeighborhoodsResponse::BnetFriendNeighborhoodData data;
data.NeighborhoodGuid = neighborhood->GetGuid();
data.FriendName = std::move(friendName);
data.MapID = neighborhood->GetNeighborhoodMapID();
response.Neighborhoods.push_back(std::move(data));
break; // Only list each neighborhood once per friend check
}
}
SendPacket(response.Write());
TC_LOG_INFO("housing", "CMSG_HOUSING_SVCS_GET_BNET_FRIEND_NEIGHBORHOODS BnetAccountGuid: {}",
housingSvcsGetBnetFriendNeighborhoods.BnetAccountGuid.ToString());
TC_LOG_INFO("housing", "CMSG_HOUSING_SVCS_GET_BNET_FRIEND_NEIGHBORHOODS BnetAccountGuid: {}, FriendNeighborhoods: {}",
housingSvcsGetBnetFriendNeighborhoods.BnetAccountGuid.ToString(), uint32(response.Neighborhoods.size()));
}
void WorldSession::HandleHousingSvcsDeleteAllNeighborhoodInvites(WorldPackets::Housing::HousingSvcsDeleteAllNeighborhoodInvites const& /*housingSvcsDeleteAllNeighborhoodInvites*/)
@@ -1512,22 +1611,16 @@ void WorldSession::HandleHousingHouseStatus(WorldPackets::Housing::HousingHouseS
if (housing)
{
response.HouseGuid = housing->GetHouseGuid();
response.OwnerBNetGuid = GetBattlenetAccountGUID();
response.OwnerPlayerGuid = player->GetGUID();
response.HouseStatus = 1; // Active
response.PlotIndex = housing->GetPlotIndex();
response.StatusFlags = 0;
response.HouseTemplateGuid = ObjectGuid::Create<HighGuid::Housing>(3, 0, 7, 0);
response.PlotGuid = housing->GetPlotGuid();
response.Status = 0;
}
// No house: all fields stay at defaults (empty GUIDs, HouseStatus=0, PlotIndex=0xFF).
// Neighborhood context is provided separately via SMSG_HOUSING_GET_CURRENT_HOUSE_INFO_RESPONSE.
// No house: all fields stay at defaults (empty GUIDs, Status=0).
SendPacket(response.Write());
TC_LOG_INFO("housing", ">>> CMSG_HOUSING_HOUSE_STATUS received");
TC_LOG_INFO("housing", "<<< SMSG_HOUSING_HOUSE_STATUS_RESPONSE sent (HouseStatus: {}, PlotIndex: {}, StatusFlags: {}, HouseGuid: {}, "
"OwnerBNetGuid: {}, OwnerPlayerGuid: {})",
response.HouseStatus, response.PlotIndex, response.StatusFlags,
response.HouseGuid.ToString(), response.OwnerBNetGuid.ToString(),
response.OwnerPlayerGuid.ToString());
TC_LOG_INFO("housing", "<<< SMSG_HOUSING_HOUSE_STATUS_RESPONSE sent (HouseGuid: {}, PlotGuid: {}, Status: {})",
response.HouseGuid.ToString(), response.PlotGuid.ToString(), response.Status);
}
void WorldSession::HandleHousingGetPlayerPermissions(WorldPackets::Housing::HousingGetPlayerPermissions const& housingGetPlayerPermissions)
@@ -1552,7 +1645,9 @@ void WorldSession::HandleHousingGetPlayerPermissions(WorldPackets::Housing::Hous
if (targetGuid == player->GetGUID())
{
// House owner gets full permissions
response.PermissionFlags = 0xFFFF;
// Sniff-verified: owner permissions are 0xE0 (bits 5,6,7)
response.ResultCode = 0;
response.PermissionFlags = 0xE0;
}
else
{
@@ -1590,17 +1685,19 @@ void WorldSession::HandleHousingGetPlayerPermissions(WorldPackets::Housing::Hous
if (flags & HOUSE_SETTING_PLOT_ACCESS_PARTY)
permissions |= HOUSE_SETTING_PLOT_ACCESS_PARTY;
response.PermissionFlags = static_cast<uint16>(permissions);
response.ResultCode = 0; // Success
response.PermissionFlags = static_cast<uint8>(permissions & 0xFF);
}
}
else
{
response.ResultCode = 0;
response.PermissionFlags = 0;
}
SendPacket(response.Write());
TC_LOG_INFO("housing", "<<< SMSG_HOUSING_GET_PLAYER_PERMISSIONS_RESPONSE sent (HouseGuid: {}, PermissionFlags: 0x{:04X})",
response.HouseGuid.ToString(), response.PermissionFlags);
TC_LOG_INFO("housing", "<<< SMSG_HOUSING_GET_PLAYER_PERMISSIONS_RESPONSE sent (HouseGuid: {}, ResultCode: {}, PermissionFlags: 0x{:02X})",
response.HouseGuid.ToString(), response.ResultCode, response.PermissionFlags);
}
void WorldSession::HandleHousingGetCurrentHouseInfo(WorldPackets::Housing::HousingGetCurrentHouseInfo const& housingGetCurrentHouseInfo)
@@ -1616,25 +1713,26 @@ void WorldSession::HandleHousingGetCurrentHouseInfo(WorldPackets::Housing::Housi
WorldPackets::Housing::HousingGetCurrentHouseInfoResponse response;
if (housing)
{
response.HouseGuid = housing->GetHouseGuid();
response.OwnerPlayerGuid = player->GetGUID();
response.NeighborhoodGuid = housing->GetNeighborhoodGuid();
response.PlotIndex = housing->GetPlotIndex();
response.HouseProperties = housing->GetSettingsFlags() & 0xFF; // Packed property bits
response.HouseLevel = static_cast<uint8>(housing->GetLevel());
// Sniff-verified: JamCurrentHouseInfo fields carry HouseGuid, PlotGuid, NeighborhoodGuid
response.HouseInfo.OwnerGuid = housing->GetHouseGuid();
response.HouseInfo.SecondaryOwnerGuid = housing->GetPlotGuid();
response.HouseInfo.PlotGuid = housing->GetNeighborhoodGuid();
response.HouseInfo.Flags = housing->GetPlotIndex();
response.HouseInfo.HouseTypeId = 32; // sniff value: 0x20 = default house type
response.HouseInfo.StatusFlags = 0;
}
else if (HousingMap* housingMap = dynamic_cast<HousingMap*>(player->GetMap()))
{
// No house, but on a housing map ? provide the neighborhood GUID so
// the client knows which neighborhood it's viewing (enables purchase UI).
// Do NOT set OwnerPlayerGuid ? the player doesn't own anything yet.
// No house, but on a housing map ? provide the neighborhood GUID
// in the PlotGuid field (3rd GUID) so the client knows the context.
if (Neighborhood* neighborhood = housingMap->GetNeighborhood())
response.NeighborhoodGuid = neighborhood->GetGuid();
response.HouseInfo.PlotGuid = neighborhood->GetGuid();
}
response.ResponseFlags = 0;
SendPacket(response.Write());
TC_LOG_INFO("housing", "<<< SMSG_HOUSING_GET_CURRENT_HOUSE_INFO_RESPONSE sent (HasHouse: {}, HouseGuid: {}, NeighborhoodGuid: {})",
housing ? "yes" : "no", response.HouseGuid.ToString(), response.NeighborhoodGuid.ToString());
TC_LOG_INFO("housing", "<<< SMSG_HOUSING_GET_CURRENT_HOUSE_INFO_RESPONSE sent (HasHouse: {}, HouseGuid: {})",
housing ? "yes" : "no", response.HouseInfo.OwnerGuid.ToString());
}
void WorldSession::HandleHousingResetKioskMode(WorldPackets::Housing::HousingResetKioskMode const& /*housingResetKioskMode*/)
@@ -1666,21 +1764,31 @@ void WorldSession::HandleQueryNeighborhoodInfo(WorldPackets::Housing::QueryNeigh
return;
WorldPackets::Housing::QueryNeighborhoodNameResponse response;
response.NeighborhoodGuid = queryNeighborhoodInfo.NeighborhoodGuid;
Neighborhood const* neighborhood = sNeighborhoodMgr.ResolveNeighborhood(queryNeighborhoodInfo.NeighborhoodGuid, player);
if (neighborhood)
{
// Use the canonical neighborhood GUID, not the client's (which may be a GO GUID or empty)
response.NeighborhoodGuid = neighborhood->GetGuid();
response.Allow = true;
response.Name = neighborhood->GetName();
}
else
{
response.NeighborhoodGuid = queryNeighborhoodInfo.NeighborhoodGuid;
response.Allow = false;
}
SendPacket(response.Write());
WorldPacket const* namePkt = response.Write();
SendPacket(namePkt);
TC_LOG_INFO("housing", "CMSG_QUERY_NEIGHBORHOOD_INFO NeighborhoodGuid: {}, Found: {}",
queryNeighborhoodInfo.NeighborhoodGuid.ToString(), response.Allow);
TC_LOG_ERROR("housing", "=== SMSG_QUERY_NEIGHBORHOOD_NAME_RESPONSE (0x460012) ===\n"
" Allow={}, Name='{}' (len={})\n"
" NeighborhoodGuid: {} ({})\n"
" Packet size={} bytes, hex:\n {}",
response.Allow, response.Name, response.Name.size(),
response.NeighborhoodGuid.ToString(), GuidHex(response.NeighborhoodGuid),
namePkt->size(), HexDumpPacket(namePkt));
}
void WorldSession::HandleInvitePlayerToNeighborhood(WorldPackets::Housing::InvitePlayerToNeighborhood const& invitePlayerToNeighborhood)
@@ -1693,7 +1801,7 @@ void WorldSession::HandleInvitePlayerToNeighborhood(WorldPackets::Housing::Invit
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodInviteResidentResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
return;
}
@@ -1701,7 +1809,7 @@ void WorldSession::HandleInvitePlayerToNeighborhood(WorldPackets::Housing::Invit
if (!neighborhood->IsManager(player->GetGUID()) && !neighborhood->IsOwner(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodInviteResidentResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_GENERIC_FAILURE);
response.Result = static_cast<uint8>(HOUSING_RESULT_GENERIC_FAILURE);
SendPacket(response.Write());
return;
}
@@ -1709,8 +1817,7 @@ void WorldSession::HandleInvitePlayerToNeighborhood(WorldPackets::Housing::Invit
HousingResult result = neighborhood->InviteResident(player->GetGUID(), invitePlayerToNeighborhood.PlayerGuid);
WorldPackets::Neighborhood::NeighborhoodInviteResidentResponse response;
response.Result = static_cast<uint32>(result);
response.NeighborhoodGuid = invitePlayerToNeighborhood.NeighborhoodGuid;
response.Result = static_cast<uint8>(result);
response.InviteeGuid = invitePlayerToNeighborhood.PlayerGuid;
SendPacket(response.Write());
@@ -1721,8 +1828,6 @@ void WorldSession::HandleInvitePlayerToNeighborhood(WorldPackets::Housing::Invit
{
WorldPackets::Neighborhood::NeighborhoodInviteNotification notification;
notification.NeighborhoodGuid = invitePlayerToNeighborhood.NeighborhoodGuid;
notification.InviterGuid = player->GetGUID();
notification.NeighborhoodName = neighborhood->GetName();
invitee->SendDirectMessage(notification.Write());
}
}
@@ -1762,3 +1867,39 @@ void WorldSession::HandleGuildGetOthersOwnedHouses(WorldPackets::Housing::GuildG
TC_LOG_INFO("housing", "CMSG_GUILD_GET_OTHERS_OWNED_HOUSES PlayerGuid: {}, FoundNeighborhoods: {}",
guildGetOthersOwnedHouses.PlayerGuid.ToString(), uint32(neighborhoods.size()));
}
// ============================================================
// Photo Sharing Authorization
// ============================================================
void WorldSession::HandleHousingPhotoSharingCompleteAuthorization(WorldPackets::Housing::HousingPhotoSharingCompleteAuthorization const& /*packet*/)
{
Player* player = GetPlayer();
if (!player)
return;
// Photo sharing authorization grants permission for the player's house photos
// to be shared with other players through the housing social features.
WorldPackets::Housing::HousingPhotoSharingAuthorizationResult response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "CMSG_HOUSING_PHOTO_SHARING_COMPLETE_AUTHORIZATION Player: {}",
player->GetGUID().ToString());
}
void WorldSession::HandleHousingPhotoSharingClearAuthorization(WorldPackets::Housing::HousingPhotoSharingClearAuthorization const& /*packet*/)
{
Player* player = GetPlayer();
if (!player)
return;
// Clears any existing photo sharing authorization for the player's house,
// revoking permission for photos to be shared publicly.
WorldPackets::Housing::HousingPhotoSharingAuthorizationClearedResult response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "CMSG_HOUSING_PHOTO_SHARING_CLEAR_AUTHORIZATION Player: {}",
player->GetGUID().ToString());
}
+317 -134
View File
@@ -34,9 +34,38 @@
#include "Player.h"
#include "World.h"
// ============================================================
// Neighborhood Charter System
// ============================================================
namespace
{
std::string HexDumpPacket(WorldPacket const* packet, size_t maxBytes = 128)
{
if (!packet || packet->size() == 0)
return "(empty)";
size_t len = std::min(packet->size(), maxBytes);
std::string result;
result.reserve(len * 3 + 32);
uint8 const* raw = packet->data();
for (size_t i = 0; i < len; ++i)
{
if (i > 0 && i % 32 == 0)
result += "\n ";
else if (i > 0)
result += ' ';
result += fmt::format("{:02X}", raw[i]);
}
if (len < packet->size())
result += fmt::format(" ...({} more)", packet->size() - len);
return result;
}
std::string GuidHex(ObjectGuid const& guid)
{
return fmt::format("lo={:016X} hi={:016X}", guid.GetRawValue(0), guid.GetRawValue(1));
}
}
// ============================================================
// Neighborhood Charter System
// ============================================================
void WorldSession::HandleNeighborhoodCharterOpenConfirmationUI(WorldPackets::Neighborhood::NeighborhoodCharterOpenConfirmationUI const& /*neighborhoodCharterOpenConfirmationUI*/)
{
@@ -50,7 +79,7 @@ void WorldSession::HandleNeighborhoodCharterOpenConfirmationUI(WorldPackets::Nei
// Client requests to open the charter creation confirmation UI
// The client handles UI display; server acknowledges readiness
WorldPackets::Neighborhood::NeighborhoodCharterOpenConfirmationUIResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
response.Result = static_cast<uint8>(HOUSING_RESULT_SUCCESS);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "Sent NeighborhoodCharterOpenConfirmationUIResponse (SUCCESS) to player {}",
@@ -71,7 +100,7 @@ void WorldSession::HandleNeighborhoodCharterCreate(WorldPackets::Neighborhood::N
if (neighborhoodCharterCreate.Name.empty() || neighborhoodCharterCreate.Name.length() > HOUSING_MAX_NAME_LENGTH)
{
WorldPackets::Neighborhood::NeighborhoodCharterUpdateResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_INVALID_NEIGHBORHOOD_NAME);
response.Result = static_cast<uint8>(HOUSING_RESULT_INVALID_NEIGHBORHOOD_NAME);
SendPacket(response.Write());
TC_LOG_INFO("housing", "HandleNeighborhoodCharterCreate: Invalid name length for player {}",
@@ -96,7 +125,7 @@ void WorldSession::HandleNeighborhoodCharterCreate(WorldPackets::Neighborhood::N
CharacterDatabase.CommitTransaction(trans);
WorldPackets::Neighborhood::NeighborhoodCharterUpdateResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
response.Result = static_cast<uint8>(HOUSING_RESULT_SUCCESS);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "Player {} created neighborhood charter '{}' (ID: {}, MapID: {})",
@@ -118,7 +147,7 @@ void WorldSession::HandleNeighborhoodCharterEdit(WorldPackets::Neighborhood::Nei
if (neighborhoodCharterEdit.Name.empty() || neighborhoodCharterEdit.Name.length() > HOUSING_MAX_NAME_LENGTH)
{
WorldPackets::Neighborhood::NeighborhoodCharterUpdateResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_INVALID_NEIGHBORHOOD_NAME);
response.Result = static_cast<uint8>(HOUSING_RESULT_INVALID_NEIGHBORHOOD_NAME);
SendPacket(response.Write());
TC_LOG_INFO("housing", "HandleNeighborhoodCharterEdit: Invalid name length for player {}",
@@ -144,7 +173,7 @@ void WorldSession::HandleNeighborhoodCharterEdit(WorldPackets::Neighborhood::Nei
CharacterDatabase.CommitTransaction(trans);
WorldPackets::Neighborhood::NeighborhoodCharterUpdateResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
response.Result = static_cast<uint8>(HOUSING_RESULT_SUCCESS);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "Player {} edited neighborhood charter '{}' (ID: {}, MapID: {})",
@@ -171,7 +200,7 @@ void WorldSession::HandleNeighborhoodCharterFinalize(WorldPackets::Neighborhood:
if (!charterResult)
{
WorldPackets::Neighborhood::NeighborhoodCharterUpdateResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_GENERIC_FAILURE);
response.Result = static_cast<uint8>(HOUSING_RESULT_GENERIC_FAILURE);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodCharterFinalize: No charter found for player {}",
@@ -187,7 +216,7 @@ void WorldSession::HandleNeighborhoodCharterFinalize(WorldPackets::Neighborhood:
if (!charter.LoadFromDB(charterResult, sigResult))
{
WorldPackets::Neighborhood::NeighborhoodCharterUpdateResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_DB_ERROR);
response.Result = static_cast<uint8>(HOUSING_RESULT_DB_ERROR);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodCharterFinalize: Failed to load charter {} from DB",
@@ -199,7 +228,7 @@ void WorldSession::HandleNeighborhoodCharterFinalize(WorldPackets::Neighborhood:
if (!charter.HasEnoughSignatures())
{
WorldPackets::Neighborhood::NeighborhoodCharterUpdateResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_MORE_SIGNATURES_NEEDED);
response.Result = static_cast<uint8>(HOUSING_RESULT_MORE_SIGNATURES_NEEDED);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodCharterFinalize: Charter {} has only {}/{} signatures",
@@ -223,7 +252,7 @@ void WorldSession::HandleNeighborhoodCharterFinalize(WorldPackets::Neighborhood:
CharacterDatabase.CommitTransaction(trans);
WorldPackets::Neighborhood::NeighborhoodCharterUpdateResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
response.Result = static_cast<uint8>(HOUSING_RESULT_SUCCESS);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "Player {} finalized charter '{}', created neighborhood {}",
@@ -233,7 +262,7 @@ void WorldSession::HandleNeighborhoodCharterFinalize(WorldPackets::Neighborhood:
else
{
WorldPackets::Neighborhood::NeighborhoodCharterUpdateResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_DB_ERROR);
response.Result = static_cast<uint8>(HOUSING_RESULT_DB_ERROR);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "Player {} failed to finalize charter '{}' - neighborhood creation failed",
@@ -261,7 +290,7 @@ void WorldSession::HandleNeighborhoodCharterAddSignature(WorldPackets::Neighborh
if (!charterResult)
{
WorldPackets::Neighborhood::NeighborhoodCharterAddSignatureResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_GENERIC_FAILURE);
response.Result = static_cast<uint8>(HOUSING_RESULT_GENERIC_FAILURE);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodCharterAddSignature: Charter {} not found in DB",
@@ -277,7 +306,7 @@ void WorldSession::HandleNeighborhoodCharterAddSignature(WorldPackets::Neighborh
if (!charter.LoadFromDB(charterResult, sigResult))
{
WorldPackets::Neighborhood::NeighborhoodCharterAddSignatureResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_DB_ERROR);
response.Result = static_cast<uint8>(HOUSING_RESULT_DB_ERROR);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodCharterAddSignature: Failed to load charter {} from DB",
@@ -289,7 +318,7 @@ void WorldSession::HandleNeighborhoodCharterAddSignature(WorldPackets::Neighborh
if (!charter.AddSignature(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodCharterAddSignatureResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_GENERIC_FAILURE);
response.Result = static_cast<uint8>(HOUSING_RESULT_GENERIC_FAILURE);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodCharterAddSignature: Player {} could not sign charter {}",
@@ -298,7 +327,7 @@ void WorldSession::HandleNeighborhoodCharterAddSignature(WorldPackets::Neighborh
}
WorldPackets::Neighborhood::NeighborhoodCharterAddSignatureResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
response.Result = static_cast<uint8>(HOUSING_RESULT_SUCCESS);
response.CharterGuid = neighborhoodCharterAddSignature.CharterGuid;
response.SignerGuid = player->GetGUID();
SendPacket(response.Write());
@@ -336,7 +365,7 @@ void WorldSession::HandleNeighborhoodCharterSendSignatureRequest(WorldPackets::N
// Acknowledge to the requester that the signature request was sent
WorldPackets::Neighborhood::NeighborhoodCharterAddSignatureResponse ackResponse;
ackResponse.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
ackResponse.Result = static_cast<uint8>(HOUSING_RESULT_SUCCESS);
SendPacket(ackResponse.Write());
TC_LOG_DEBUG("housing", "Player {} requested signature from {} for charter {}",
@@ -357,7 +386,7 @@ void WorldSession::HandleNeighborhoodUpdateName(WorldPackets::Neighborhood::Neig
if (!housing)
{
WorldPackets::Neighborhood::NeighborhoodUpdateNameResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_HOUSE_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_HOUSE_NOT_FOUND);
SendPacket(response.Write());
return;
}
@@ -371,7 +400,7 @@ void WorldSession::HandleNeighborhoodUpdateName(WorldPackets::Neighborhood::Neig
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodUpdateNameResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodUpdateName: Neighborhood {} not found",
@@ -383,7 +412,7 @@ void WorldSession::HandleNeighborhoodUpdateName(WorldPackets::Neighborhood::Neig
if (!neighborhood->IsOwner(player->GetGUID()) && !neighborhood->IsManager(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodUpdateNameResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_PERMISSION_DENIED);
response.Result = static_cast<uint8>(HOUSING_RESULT_PERMISSION_DENIED);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodUpdateName: Player {} lacks permission for neighborhood {}",
@@ -395,7 +424,7 @@ void WorldSession::HandleNeighborhoodUpdateName(WorldPackets::Neighborhood::Neig
if (neighborhoodUpdateName.NewName.empty() || neighborhoodUpdateName.NewName.length() > HOUSING_MAX_NAME_LENGTH)
{
WorldPackets::Neighborhood::NeighborhoodUpdateNameResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_INVALID_NEIGHBORHOOD_NAME);
response.Result = static_cast<uint8>(HOUSING_RESULT_INVALID_NEIGHBORHOOD_NAME);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodUpdateName: Invalid name length");
@@ -414,15 +443,13 @@ void WorldSession::HandleNeighborhoodUpdateName(WorldPackets::Neighborhood::Neig
memberPlayer->SendDirectMessage(invalidate.Write());
WorldPackets::Neighborhood::NeighborhoodUpdateNameNotification nameNotification;
nameNotification.NeighborhoodGuid = neighborhoodGuid;
nameNotification.NewName = neighborhoodUpdateName.NewName;
memberPlayer->SendDirectMessage(nameNotification.Write());
}
}
WorldPackets::Neighborhood::NeighborhoodUpdateNameResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
response.NeighborhoodGuid = neighborhoodGuid;
response.Result = static_cast<uint8>(HOUSING_RESULT_SUCCESS);
SendPacket(response.Write());
// Send guild rename notification if player is in a guild
@@ -452,7 +479,7 @@ void WorldSession::HandleNeighborhoodSetPublicFlag(WorldPackets::Neighborhood::N
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodUpdateNameResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodSetPublicFlag: Neighborhood {} not found",
@@ -464,7 +491,7 @@ void WorldSession::HandleNeighborhoodSetPublicFlag(WorldPackets::Neighborhood::N
if (!neighborhood->IsOwner(player->GetGUID()) && !neighborhood->IsManager(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodUpdateNameResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_PERMISSION_DENIED);
response.Result = static_cast<uint8>(HOUSING_RESULT_PERMISSION_DENIED);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodSetPublicFlag: Player {} lacks permission for neighborhood {}",
@@ -475,7 +502,7 @@ void WorldSession::HandleNeighborhoodSetPublicFlag(WorldPackets::Neighborhood::N
neighborhood->SetPublic(neighborhoodSetPublicFlag.IsPublic);
WorldPackets::Neighborhood::NeighborhoodUpdateNameResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
response.Result = static_cast<uint8>(HOUSING_RESULT_SUCCESS);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "Neighborhood {} set to {} by player {}",
@@ -494,7 +521,7 @@ void WorldSession::HandleNeighborhoodAddSecondaryOwner(WorldPackets::Neighborhoo
if (!housing)
{
WorldPackets::Neighborhood::NeighborhoodAddSecondaryOwnerResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_HOUSE_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_HOUSE_NOT_FOUND);
SendPacket(response.Write());
return;
}
@@ -508,7 +535,7 @@ void WorldSession::HandleNeighborhoodAddSecondaryOwner(WorldPackets::Neighborhoo
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodAddSecondaryOwnerResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodAddSecondaryOwner: Neighborhood {} not found",
@@ -520,7 +547,7 @@ void WorldSession::HandleNeighborhoodAddSecondaryOwner(WorldPackets::Neighborhoo
if (!neighborhood->IsOwner(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodAddSecondaryOwnerResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_PERMISSION_DENIED);
response.Result = static_cast<uint8>(HOUSING_RESULT_PERMISSION_DENIED);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodAddSecondaryOwner: Player {} is not owner of neighborhood {}",
@@ -531,9 +558,8 @@ void WorldSession::HandleNeighborhoodAddSecondaryOwner(WorldPackets::Neighborhoo
HousingResult result = neighborhood->AddManager(neighborhoodAddSecondaryOwner.PlayerGuid);
WorldPackets::Neighborhood::NeighborhoodAddSecondaryOwnerResponse response;
response.Result = static_cast<uint32>(result);
response.NeighborhoodGuid = neighborhoodGuid;
response.PlayerGuid = neighborhoodAddSecondaryOwner.PlayerGuid;
response.Result = static_cast<uint8>(result);
SendPacket(response.Write());
// Broadcast roster update and refresh manager mirror data for all online members
@@ -582,7 +608,7 @@ void WorldSession::HandleNeighborhoodRemoveSecondaryOwner(WorldPackets::Neighbor
if (!housing)
{
WorldPackets::Neighborhood::NeighborhoodRemoveSecondaryOwnerResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_HOUSE_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_HOUSE_NOT_FOUND);
SendPacket(response.Write());
return;
}
@@ -596,7 +622,7 @@ void WorldSession::HandleNeighborhoodRemoveSecondaryOwner(WorldPackets::Neighbor
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodRemoveSecondaryOwnerResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodRemoveSecondaryOwner: Neighborhood {} not found",
@@ -608,7 +634,7 @@ void WorldSession::HandleNeighborhoodRemoveSecondaryOwner(WorldPackets::Neighbor
if (!neighborhood->IsOwner(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodRemoveSecondaryOwnerResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_PERMISSION_DENIED);
response.Result = static_cast<uint8>(HOUSING_RESULT_PERMISSION_DENIED);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodRemoveSecondaryOwner: Player {} is not owner of neighborhood {}",
@@ -619,9 +645,8 @@ void WorldSession::HandleNeighborhoodRemoveSecondaryOwner(WorldPackets::Neighbor
HousingResult result = neighborhood->RemoveManager(neighborhoodRemoveSecondaryOwner.PlayerGuid);
WorldPackets::Neighborhood::NeighborhoodRemoveSecondaryOwnerResponse response;
response.Result = static_cast<uint32>(result);
response.NeighborhoodGuid = neighborhoodGuid;
response.PlayerGuid = neighborhoodRemoveSecondaryOwner.PlayerGuid;
response.Result = static_cast<uint8>(result);
SendPacket(response.Write());
// Broadcast roster update and refresh manager mirror data for all online members
@@ -670,7 +695,7 @@ void WorldSession::HandleNeighborhoodInviteResident(WorldPackets::Neighborhood::
if (!housing)
{
WorldPackets::Neighborhood::NeighborhoodInviteResidentResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_HOUSE_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_HOUSE_NOT_FOUND);
SendPacket(response.Write());
return;
}
@@ -684,7 +709,7 @@ void WorldSession::HandleNeighborhoodInviteResident(WorldPackets::Neighborhood::
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodInviteResidentResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodInviteResident: Neighborhood {} not found",
@@ -696,7 +721,7 @@ void WorldSession::HandleNeighborhoodInviteResident(WorldPackets::Neighborhood::
if (!neighborhood->IsOwner(player->GetGUID()) && !neighborhood->IsManager(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodInviteResidentResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_PERMISSION_DENIED);
response.Result = static_cast<uint8>(HOUSING_RESULT_PERMISSION_DENIED);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodInviteResident: Player {} lacks permission for neighborhood {}",
@@ -707,8 +732,7 @@ void WorldSession::HandleNeighborhoodInviteResident(WorldPackets::Neighborhood::
HousingResult result = neighborhood->InviteResident(player->GetGUID(), neighborhoodInviteResident.PlayerGuid);
WorldPackets::Neighborhood::NeighborhoodInviteResidentResponse response;
response.Result = static_cast<uint32>(result);
response.NeighborhoodGuid = neighborhoodGuid;
response.Result = static_cast<uint8>(result);
response.InviteeGuid = neighborhoodInviteResident.PlayerGuid;
SendPacket(response.Write());
@@ -719,8 +743,6 @@ void WorldSession::HandleNeighborhoodInviteResident(WorldPackets::Neighborhood::
{
WorldPackets::Neighborhood::NeighborhoodInviteNotification notification;
notification.NeighborhoodGuid = neighborhoodGuid;
notification.InviterGuid = player->GetGUID();
notification.NeighborhoodName = neighborhood->GetName();
invitee->SendDirectMessage(notification.Write());
}
}
@@ -740,7 +762,7 @@ void WorldSession::HandleNeighborhoodCancelInvitation(WorldPackets::Neighborhood
if (!housing)
{
WorldPackets::Neighborhood::NeighborhoodCancelInvitationResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_HOUSE_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_HOUSE_NOT_FOUND);
SendPacket(response.Write());
return;
}
@@ -754,7 +776,7 @@ void WorldSession::HandleNeighborhoodCancelInvitation(WorldPackets::Neighborhood
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodCancelInvitationResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodCancelInvitation: Neighborhood {} not found",
@@ -766,7 +788,7 @@ void WorldSession::HandleNeighborhoodCancelInvitation(WorldPackets::Neighborhood
if (!neighborhood->IsOwner(player->GetGUID()) && !neighborhood->IsManager(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodCancelInvitationResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_PERMISSION_DENIED);
response.Result = static_cast<uint8>(HOUSING_RESULT_PERMISSION_DENIED);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodCancelInvitation: Player {} lacks permission for neighborhood {}",
@@ -777,8 +799,7 @@ void WorldSession::HandleNeighborhoodCancelInvitation(WorldPackets::Neighborhood
HousingResult result = neighborhood->CancelInvitation(neighborhoodCancelInvitation.InviteeGuid);
WorldPackets::Neighborhood::NeighborhoodCancelInvitationResponse response;
response.Result = static_cast<uint32>(result);
response.NeighborhoodGuid = neighborhoodGuid;
response.Result = static_cast<uint8>(result);
response.InviteeGuid = neighborhoodCancelInvitation.InviteeGuid;
SendPacket(response.Write());
@@ -800,7 +821,7 @@ void WorldSession::HandleNeighborhoodPlayerDeclineInvite(WorldPackets::Neighborh
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodDeclineInvitationResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodPlayerDeclineInvite: Neighborhood {} not found",
@@ -811,7 +832,7 @@ void WorldSession::HandleNeighborhoodPlayerDeclineInvite(WorldPackets::Neighborh
HousingResult result = neighborhood->DeclineInvitation(player->GetGUID());
WorldPackets::Neighborhood::NeighborhoodDeclineInvitationResponse response;
response.Result = static_cast<uint32>(result);
response.Result = static_cast<uint8>(result);
response.NeighborhoodGuid = neighborhoodPlayerDeclineInvite.NeighborhoodGuid;
SendPacket(response.Write());
@@ -848,15 +869,14 @@ void WorldSession::HandleNeighborhoodPlayerGetInvite(WorldPackets::Neighborhood:
WorldPackets::Neighborhood::NeighborhoodPlayerGetInviteResponse response;
if (foundNeighborhood && foundInvite)
{
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
response.NeighborhoodGuid = foundNeighborhood->GetGuid();
response.InviterGuid = foundInvite->InviterGuid;
response.InviteTime = foundInvite->InviteTime;
response.NeighborhoodName = foundNeighborhood->GetName();
response.Result = static_cast<uint8>(HOUSING_RESULT_SUCCESS);
response.Entry.Timestamp = foundInvite->InviteTime;
response.Entry.PlayerGuid = foundInvite->InviterGuid;
response.Entry.HouseGuid = foundNeighborhood->GetGuid();
}
else
{
response.Result = static_cast<uint32>(HOUSING_RESULT_GENERIC_FAILURE);
response.Result = static_cast<uint8>(HOUSING_RESULT_GENERIC_FAILURE);
}
SendPacket(response.Write());
@@ -878,7 +898,7 @@ void WorldSession::HandleNeighborhoodGetInvites(WorldPackets::Neighborhood::Neig
if (!housing)
{
WorldPackets::Neighborhood::NeighborhoodGetInvitesResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_HOUSE_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_HOUSE_NOT_FOUND);
SendPacket(response.Write());
return;
}
@@ -888,7 +908,7 @@ void WorldSession::HandleNeighborhoodGetInvites(WorldPackets::Neighborhood::Neig
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodGetInvitesResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodGetInvites: Neighborhood {} not found",
@@ -900,7 +920,7 @@ void WorldSession::HandleNeighborhoodGetInvites(WorldPackets::Neighborhood::Neig
if (!neighborhood->IsOwner(player->GetGUID()) && !neighborhood->IsManager(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodGetInvitesResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_PERMISSION_DENIED);
response.Result = static_cast<uint8>(HOUSING_RESULT_PERMISSION_DENIED);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodGetInvites: Player {} lacks permission for neighborhood {}",
@@ -911,15 +931,15 @@ void WorldSession::HandleNeighborhoodGetInvites(WorldPackets::Neighborhood::Neig
std::vector<Neighborhood::PendingInvite> const& invites = neighborhood->GetPendingInvites();
WorldPackets::Neighborhood::NeighborhoodGetInvitesResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
response.Result = static_cast<uint8>(HOUSING_RESULT_SUCCESS);
response.Invites.reserve(invites.size());
for (auto const& invite : invites)
{
WorldPackets::Neighborhood::NeighborhoodGetInvitesResponse::InviteData data;
data.InviteeGuid = invite.InviteeGuid;
data.InviterGuid = invite.InviterGuid;
data.InviteTime = invite.InviteTime;
response.Invites.push_back(data);
WorldPackets::Housing::JamNeighborhoodRosterEntry entry;
entry.Timestamp = invite.InviteTime;
entry.PlayerGuid = invite.InviteeGuid;
entry.HouseGuid = ObjectGuid::Empty; // invitees don't have houses yet
response.Invites.push_back(entry);
}
SendPacket(response.Write());
@@ -933,14 +953,15 @@ void WorldSession::HandleNeighborhoodBuyHouse(WorldPackets::Neighborhood::Neighb
if (!player)
return;
TC_LOG_INFO("housing", "CMSG_NEIGHBORHOOD_BUY_HOUSE NeighborhoodGuid: {}, PlotIndex: {}",
neighborhoodBuyHouse.NeighborhoodGuid.ToString(), neighborhoodBuyHouse.PlotIndex);
TC_LOG_INFO("housing", "CMSG_NEIGHBORHOOD_BUY_HOUSE NeighborhoodGuid: {}, RawPlotIndex: {}, PlotGuid: {}",
neighborhoodBuyHouse.NeighborhoodGuid.ToString(), neighborhoodBuyHouse.PlotIndex,
neighborhoodBuyHouse.PlotGuid.ToString());
Neighborhood* neighborhood = sNeighborhoodMgr.ResolveNeighborhood(neighborhoodBuyHouse.NeighborhoodGuid, player);
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodBuyHouseResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodBuyHouse: Neighborhood {} not found",
@@ -948,23 +969,49 @@ void WorldSession::HandleNeighborhoodBuyHouse(WorldPackets::Neighborhood::Neighb
return;
}
// Must be a member of the neighborhood
// Resolve the actual DB2 PlotIndex from the cornerstone GO entry.
// The client may send a different value in the PlotIndex field (array index vs DB2 PlotIndex),
// so we always resolve from the GO GUID which is reliable.
uint32 cornerstoneGoEntry = neighborhoodBuyHouse.NeighborhoodGuid.GetEntry();
uint32 neighborhoodMapId = neighborhood->GetNeighborhoodMapID();
NeighborhoodPlotData const* plotData = sHousingMgr.GetPlotByCornerstoneEntry(neighborhoodMapId, cornerstoneGoEntry);
if (!plotData)
{
TC_LOG_ERROR("housing", "HandleNeighborhoodBuyHouse: No plot found for CornerstoneGO entry {} on map {}",
cornerstoneGoEntry, neighborhoodMapId);
WorldPackets::Neighborhood::NeighborhoodBuyHouseResponse response;
response.Result = static_cast<uint8>(HOUSING_RESULT_GENERIC_FAILURE);
SendPacket(response.Write());
return;
}
uint8 resolvedPlotIndex = static_cast<uint8>(plotData->PlotIndex);
TC_LOG_INFO("housing", "HandleNeighborhoodBuyHouse: Resolved CornerstoneGO {} -> PlotIndex {} (raw was {})",
cornerstoneGoEntry, resolvedPlotIndex, neighborhoodBuyHouse.PlotIndex);
// Auto-join neighborhood if not already a member ? buying a plot implies joining
if (!neighborhood->IsMember(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodBuyHouseResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
HousingResult joinResult = neighborhood->AddResident(player->GetGUID());
if (joinResult != HOUSING_RESULT_SUCCESS)
{
WorldPackets::Neighborhood::NeighborhoodBuyHouseResponse response;
response.Result = static_cast<uint8>(joinResult);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodBuyHouse: Player {} is not a member of neighborhood {}",
player->GetGUID().ToString(), neighborhoodBuyHouse.NeighborhoodGuid.ToString());
return;
TC_LOG_DEBUG("housing", "HandleNeighborhoodBuyHouse: Failed to add player {} to neighborhood {} (result {})",
player->GetGUID().ToString(), neighborhoodBuyHouse.NeighborhoodGuid.ToString(), static_cast<uint32>(joinResult));
return;
}
TC_LOG_INFO("housing", "HandleNeighborhoodBuyHouse: Auto-added player {} as resident of neighborhood '{}'",
player->GetGUID().ToString(), neighborhood->GetName());
}
// Must not already own a house in this neighborhood
if (player->GetHousingForNeighborhood(neighborhoodBuyHouse.NeighborhoodGuid))
{
WorldPackets::Neighborhood::NeighborhoodBuyHouseResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_INVALID_HOUSE);
response.Result = static_cast<uint8>(HOUSING_RESULT_INVALID_HOUSE);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodBuyHouse: Player {} already has a house in neighborhood {}",
@@ -972,15 +1019,15 @@ void WorldSession::HandleNeighborhoodBuyHouse(WorldPackets::Neighborhood::Neighb
return;
}
HousingResult result = neighborhood->PurchasePlot(player->GetGUID(), neighborhoodBuyHouse.PlotIndex);
HousingResult result = neighborhood->PurchasePlot(player->GetGUID(), resolvedPlotIndex);
if (result == HOUSING_RESULT_SUCCESS)
{
player->CreateHousing(neighborhoodBuyHouse.NeighborhoodGuid, neighborhoodBuyHouse.PlotIndex);
player->CreateHousing(neighborhoodBuyHouse.NeighborhoodGuid, resolvedPlotIndex);
// Update the PlotInfo with the newly created HouseGuid and Battle.net account GUID
if (Housing const* housing = player->GetHousing())
{
neighborhood->UpdatePlotHouseInfo(neighborhoodBuyHouse.PlotIndex,
neighborhood->UpdatePlotHouseInfo(resolvedPlotIndex,
housing->GetHouseGuid(), GetBattlenetAccountGUID());
}
@@ -988,14 +1035,67 @@ void WorldSession::HandleNeighborhoodBuyHouse(WorldPackets::Neighborhood::Neighb
static constexpr uint32 NPC_KILL_CREDIT_BUY_HOME = 248858;
player->KilledMonsterCredit(NPC_KILL_CREDIT_BUY_HOME);
// Retail sequence: FirstTimeDecorAcquisition ? BuyHouseResponse ? LevelFavor updates
// 1. Send starter decor acquisition notifications (sniff: 7-8 packets before buy response)
std::vector<uint32> starterDecorIds = sHousingMgr.GetStarterDecorIds();
for (uint32 decorId : starterDecorIds)
{
WorldPackets::Housing::HousingFirstTimeDecorAcquisition decorAcq;
decorAcq.DecorEntryID = decorId;
SendPacket(decorAcq.Write());
}
TC_LOG_DEBUG("housing", "HandleNeighborhoodBuyHouse: Sent {} FirstTimeDecorAcquisition packets",
uint32(starterDecorIds.size()));
// 2. Build buy response with JamCurrentHouseInfo matching retail sniff format
WorldPackets::Neighborhood::NeighborhoodBuyHouseResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
response.NeighborhoodGuid = neighborhoodBuyHouse.NeighborhoodGuid;
response.PlotIndex = neighborhoodBuyHouse.PlotIndex;
response.Result = static_cast<uint8>(HOUSING_RESULT_SUCCESS);
if (Housing const* h = player->GetHousing())
response.HouseGuid = h->GetHouseGuid();
{
// Sniff-verified: OwnerGuid=HouseGUID, SecondaryOwnerGuid=PlotGUID, PlotGuid=NeighborhoodGUID
response.HouseInfo.OwnerGuid = h->GetHouseGuid();
response.HouseInfo.SecondaryOwnerGuid = h->GetPlotGuid();
response.HouseInfo.PlotGuid = neighborhood->GetGuid();
response.HouseInfo.Flags = resolvedPlotIndex;
response.HouseInfo.HouseTypeId = 32; // sniff value: 0x20
response.HouseInfo.StatusFlags = 0;
}
SendPacket(response.Write());
// 3. Send level/favor updates (sniff: always 2 packets after buy response)
if (Housing const* h = player->GetHousing())
{
// Packet 1: Initial level assignment (prev=-1/-1, new=level 1, next level cost=910)
{
WorldPackets::Housing::HousingSvcsUpdateHousesLevelFavor levelFavor;
levelFavor.Type = 0;
levelFavor.PreviousFavor = -1;
levelFavor.PreviousLevel = -1;
levelFavor.NewLevel = 1;
levelFavor.Field4 = 0;
levelFavor.HouseGuid = h->GetHouseGuid();
levelFavor.PreviousLevelId = -1;
levelFavor.NextLevelFavorCost = 910; // sniff value: 0x038E
levelFavor.Flags = 0x8000;
SendPacket(levelFavor.Write());
}
// Packet 2: Favor state (prev favor=910, level=1, no next level info)
{
WorldPackets::Housing::HousingSvcsUpdateHousesLevelFavor levelFavor;
levelFavor.Type = 0;
levelFavor.PreviousFavor = 910;
levelFavor.PreviousLevel = 1;
levelFavor.NewLevel = 1;
levelFavor.Field4 = 0;
levelFavor.HouseGuid = h->GetHouseGuid();
levelFavor.PreviousLevelId = -1;
levelFavor.NextLevelFavorCost = -1;
levelFavor.Flags = 0x8000;
SendPacket(levelFavor.Write());
}
}
// Broadcast roster update to other neighborhood members
for (auto const& member : neighborhood->GetMembers())
{
@@ -1004,7 +1104,7 @@ void WorldSession::HandleNeighborhoodBuyHouse(WorldPackets::Neighborhood::Neighb
if (Player* memberPlayer = ObjectAccessor::FindPlayer(member.PlayerGuid))
{
WorldPackets::Neighborhood::NeighborhoodRosterResidentUpdate rosterUpdate;
rosterUpdate.Residents.push_back({ player->GetGUID(), uint16(neighborhoodBuyHouse.PlotIndex) });
rosterUpdate.Residents.push_back({ player->GetGUID(), uint16(resolvedPlotIndex) });
memberPlayer->SendDirectMessage(rosterUpdate.Write());
}
}
@@ -1022,11 +1122,11 @@ void WorldSession::HandleNeighborhoodBuyHouse(WorldPackets::Neighborhood::Neighb
// Mark the plot Cornerstone as owned (GOState 1 = READY)
if (HousingMap* housingMap = dynamic_cast<HousingMap*>(player->GetMap()))
housingMap->SetPlotOwnershipState(neighborhoodBuyHouse.PlotIndex, true);
housingMap->SetPlotOwnershipState(resolvedPlotIndex, true);
TC_LOG_DEBUG("housing", "Player {} purchased plot {} in neighborhood {}",
player->GetGUID().ToString(), neighborhoodBuyHouse.PlotIndex,
neighborhoodBuyHouse.NeighborhoodGuid.ToString());
TC_LOG_DEBUG("housing", "Player {} purchased plot {} in neighborhood '{}'",
player->GetGUID().ToString(), resolvedPlotIndex,
neighborhood->GetName());
// Check if neighborhoods need expansion after plot purchase
sNeighborhoodMgr.CheckAndExpandNeighborhoods();
@@ -1034,7 +1134,7 @@ void WorldSession::HandleNeighborhoodBuyHouse(WorldPackets::Neighborhood::Neighb
else
{
WorldPackets::Neighborhood::NeighborhoodBuyHouseResponse response;
response.Result = static_cast<uint32>(result);
response.Result = static_cast<uint8>(result);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodBuyHouse: PurchasePlot result: {} for player {}",
@@ -1055,7 +1155,7 @@ void WorldSession::HandleNeighborhoodMoveHouse(WorldPackets::Neighborhood::Neigh
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodMoveHouseResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodMoveHouse: Neighborhood {} not found",
@@ -1079,9 +1179,15 @@ void WorldSession::HandleNeighborhoodMoveHouse(WorldPackets::Neighborhood::Neigh
HousingResult result = neighborhood->MoveHouse(player->GetGUID(), targetPlotIndex);
WorldPackets::Neighborhood::NeighborhoodMoveHouseResponse response;
response.Result = static_cast<uint32>(result);
response.NeighborhoodGuid = neighborhoodMoveHouse.NeighborhoodGuid;
response.NewPlotIndex = targetPlotIndex;
response.Result = static_cast<uint8>(result);
if (result == HOUSING_RESULT_SUCCESS)
{
response.HouseInfo.OwnerGuid = player->GetGUID();
response.HouseInfo.PlotGuid = neighborhoodMoveHouse.PlotGuid;
if (Housing const* housing = player->GetHousing())
response.HouseInfo.HouseTypeId = 0; // default house type
}
response.MoveTransactionGuid = ObjectGuid::Empty; // transaction tracking GUID
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "MoveHouse result: {} to plot {} in neighborhood {}",
@@ -1101,12 +1207,12 @@ void WorldSession::HandleNeighborhoodOpenCornerstoneUI(WorldPackets::Neighborhoo
Neighborhood* neighborhood = sNeighborhoodMgr.ResolveNeighborhood(neighborhoodOpenCornerstoneUI.NeighborhoodGuid, player);
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodOpenCornerstoneUIResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodOpenCornerstoneUI: Neighborhood {} not found",
neighborhoodOpenCornerstoneUI.NeighborhoodGuid.ToString());
// No result code in this packet ? just send with PurchaseStatus=0, client won't show buy button
WorldPackets::Neighborhood::NeighborhoodOpenCornerstoneUIResponse response;
response.PlotIndex = neighborhoodOpenCornerstoneUI.PlotIndex;
SendPacket(response.Write());
return;
}
@@ -1119,7 +1225,7 @@ void WorldSession::HandleNeighborhoodOpenCornerstoneUI(WorldPackets::Neighborhoo
for (NeighborhoodPlotData const* plot : plots)
{
if (plot->PlotIndex == neighborhoodOpenCornerstoneUI.PlotIndex)
if (plot->PlotIndex == static_cast<int32>(neighborhoodOpenCornerstoneUI.PlotIndex))
{
plotCost = plot->Cost;
plotFound = true;
@@ -1129,26 +1235,65 @@ void WorldSession::HandleNeighborhoodOpenCornerstoneUI(WorldPackets::Neighborhoo
if (!plotFound)
{
WorldPackets::Neighborhood::NeighborhoodOpenCornerstoneUIResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_RPC_FAILURE);
SendPacket(response.Write());
TC_LOG_ERROR("housing", "HandleNeighborhoodOpenCornerstoneUI: PlotIndex {} not found in neighborhood map {}",
neighborhoodOpenCornerstoneUI.PlotIndex, neighborhoodMapId);
WorldPackets::Neighborhood::NeighborhoodOpenCornerstoneUIResponse response;
response.PlotIndex = neighborhoodOpenCornerstoneUI.PlotIndex;
response.NeighborhoodName = neighborhood->GetName();
SendPacket(response.Write());
return;
}
WorldPackets::Neighborhood::NeighborhoodOpenCornerstoneUIResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
response.NeighborhoodGuid = neighborhoodOpenCornerstoneUI.NeighborhoodGuid;
response.PlotGuid = ObjectGuid::Empty; // Unclaimed plot ? no owner
response.PlotIndex = static_cast<uint8>(neighborhoodOpenCornerstoneUI.PlotIndex);
response.Cost = plotCost;
response.NeighborhoodName = neighborhood->GetName();
SendPacket(response.Write());
// Pre-send neighborhood name response to populate the JamCliNeighborhoodName
// DataCache. Flag +574 in the display function checks whether the TLS
// NeighborhoodGuid is resolved in the DataCache. Sending this immediately
// before the cornerstone response ensures the cache entry exists.
{
WorldPackets::Housing::QueryNeighborhoodNameResponse nameResp;
nameResp.NeighborhoodGuid = neighborhood->GetGuid();
nameResp.Allow = true;
nameResp.Name = neighborhood->GetName();
SendPacket(nameResp.Write());
}
TC_LOG_DEBUG("housing", "HandleNeighborhoodOpenCornerstoneUI: Player {} opened cornerstone UI for plot {} (cost: {}) in neighborhood '{}'",
player->GetGUID().ToString(), neighborhoodOpenCornerstoneUI.PlotIndex, plotCost, neighborhood->GetName());
// Build cornerstone UI response ? wire format verified against retail 12.0.1 build 65940
WorldPackets::Neighborhood::NeighborhoodOpenCornerstoneUIResponse response;
response.PlotIndex = neighborhoodOpenCornerstoneUI.PlotIndex;
// PlotOwnerGuid (?Buffer+40): Controls flag +573. Empty triggers zero-check path
// which clears +573. Non-Housing GUIDs (Player type 2) succeed entity name lookup.
response.PlotOwnerGuid = ObjectGuid::Empty;
// NeighborhoodGuid (?Buffer+56): Controls flag +574 via JamCliNeighborhoodName DataCache.
// Client looks up neighborhood name in the cache using this GUID as the key.
// Must match the GUID in the pre-sent QueryNeighborhoodNameResponse above,
// otherwise the cache lookup fails and the name shows as <?>.
response.NeighborhoodGuid = neighborhood->GetGuid();
response.Cost = plotCost;
// PurchaseStatus (?Buffer+80): HousingResult enum value
// Value 73 = PlotReserved error code, which shows "Plot already reserved"
// Value 0 = Success (no error), allows the purchase UI to render normally
response.PurchaseStatus = 0;
response.CornerstoneGuid = ObjectGuid::Empty;
// CanPurchase bit is 0 in ALL retail packets (both owned and unclaimed purchasable)
// The actual "can buy" signal is PurchaseStatus==73. Setting CanPurchase=true
// causes client to show "Plot already reserved" message.
response.CanPurchase = false;
response.NeighborhoodName = neighborhood->GetName();
WorldPacket const* pkt = response.Write();
SendPacket(pkt);
TC_LOG_ERROR("housing", "=== SMSG_NEIGHBORHOOD_OPEN_CORNERSTONE_UI_RESPONSE (0x5C000A) ===\n"
" PlotIndex={}, Cost={}, PurchaseStatus={}, CanPurchase={}, IsPlotOwned={}\n"
" PlotOwnerGuid: {} ({})\n"
" NeighborhoodGuid: {} ({})\n"
" CornerstoneGuid: {} ({})\n"
" NeighborhoodName='{}' (len={})\n"
" Packet size={} bytes, hex:\n {}",
response.PlotIndex, response.Cost, uint32(response.PurchaseStatus), response.CanPurchase, response.IsPlotOwned,
response.PlotOwnerGuid.ToString(), GuidHex(response.PlotOwnerGuid),
response.NeighborhoodGuid.ToString(), GuidHex(response.NeighborhoodGuid),
response.CornerstoneGuid.ToString(), GuidHex(response.CornerstoneGuid),
response.NeighborhoodName, response.NeighborhoodName.size(),
pkt->size(), HexDumpPacket(pkt));
}
void WorldSession::HandleNeighborhoodOfferOwnership(WorldPackets::Neighborhood::NeighborhoodOfferOwnership const& neighborhoodOfferOwnership)
@@ -1161,7 +1306,7 @@ void WorldSession::HandleNeighborhoodOfferOwnership(WorldPackets::Neighborhood::
if (!housing)
{
WorldPackets::Neighborhood::NeighborhoodOfferOwnershipResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_HOUSE_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_HOUSE_NOT_FOUND);
SendPacket(response.Write());
return;
}
@@ -1175,7 +1320,7 @@ void WorldSession::HandleNeighborhoodOfferOwnership(WorldPackets::Neighborhood::
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodOfferOwnershipResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodOfferOwnership: Neighborhood {} not found",
@@ -1187,7 +1332,7 @@ void WorldSession::HandleNeighborhoodOfferOwnership(WorldPackets::Neighborhood::
if (!neighborhood->IsOwner(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodOfferOwnershipResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_PERMISSION_DENIED);
response.Result = static_cast<uint8>(HOUSING_RESULT_PERMISSION_DENIED);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodOfferOwnership: Player {} is not owner of neighborhood {}",
@@ -1198,9 +1343,7 @@ void WorldSession::HandleNeighborhoodOfferOwnership(WorldPackets::Neighborhood::
HousingResult result = neighborhood->TransferOwnership(neighborhoodOfferOwnership.NewOwnerGuid);
WorldPackets::Neighborhood::NeighborhoodOfferOwnershipResponse response;
response.Result = static_cast<uint32>(result);
response.NeighborhoodGuid = neighborhoodGuid;
response.NewOwnerGuid = neighborhoodOfferOwnership.NewOwnerGuid;
response.Result = static_cast<uint8>(result);
SendPacket(response.Write());
// Notify the new owner and broadcast to all members
@@ -1247,7 +1390,7 @@ void WorldSession::HandleNeighborhoodGetRoster(WorldPackets::Neighborhood::Neigh
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodGetRosterResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodGetRoster: Neighborhood {} not found",
@@ -1281,15 +1424,35 @@ void WorldSession::HandleNeighborhoodGetRoster(WorldPackets::Neighborhood::Neigh
data.PlayerGuid = member.PlayerGuid;
data.PlotIndex = member.PlotIndex;
data.JoinTime = member.JoinTime;
data.IsOnline = ObjectAccessor::FindPlayer(member.PlayerGuid) != nullptr;
if (member.PlotIndex != INVALID_PLOT_INDEX)
if (Neighborhood::PlotInfo const* plotInfo = neighborhood->GetPlotInfo(member.PlotIndex))
data.HouseGuid = plotInfo->HouseGuid;
response.Members.push_back(data);
}
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "Neighborhood {} roster: {} members sent",
neighborhoodGetRoster.NeighborhoodGuid.ToString(), uint32(members.size()));
// Pre-send neighborhood name response to populate JamCliNeighborhoodName DataCache.
// The roster UI resolves the neighborhood name via GroupNeighborhoodGuid cache lookup.
{
WorldPackets::Housing::QueryNeighborhoodNameResponse nameResp;
nameResp.NeighborhoodGuid = neighborhood->GetGuid();
nameResp.Allow = true;
nameResp.Name = neighborhood->GetName();
SendPacket(nameResp.Write());
}
WorldPacket const* rosterPkt = response.Write();
SendPacket(rosterPkt);
TC_LOG_ERROR("housing", "=== SMSG_NEIGHBORHOOD_GET_ROSTER_RESPONSE (0x5C0012) [handler] ===\n"
" Result={}, Members={}, NeighborhoodName='{}'\n"
" GroupNeighborhoodGuid: {} ({})\n"
" GroupOwnerGuid: {} ({})\n"
" Packet size={} bytes, hex:\n {}",
uint32(response.Result), response.Members.size(), response.NeighborhoodName,
response.GroupNeighborhoodGuid.ToString(), GuidHex(response.GroupNeighborhoodGuid),
response.GroupOwnerGuid.ToString(), GuidHex(response.GroupOwnerGuid),
rosterPkt->size(), HexDumpPacket(rosterPkt, 256));
}
void WorldSession::HandleNeighborhoodEvictPlot(WorldPackets::Neighborhood::NeighborhoodEvictPlot const& neighborhoodEvictPlot)
@@ -1305,7 +1468,7 @@ void WorldSession::HandleNeighborhoodEvictPlot(WorldPackets::Neighborhood::Neigh
if (!neighborhood)
{
WorldPackets::Neighborhood::NeighborhoodEvictPlotResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodEvictPlot: Neighborhood {} not found",
@@ -1317,7 +1480,7 @@ void WorldSession::HandleNeighborhoodEvictPlot(WorldPackets::Neighborhood::Neigh
if (!neighborhood->IsOwner(player->GetGUID()) && !neighborhood->IsManager(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodEvictPlotResponse response;
response.Result = static_cast<uint32>(HOUSING_RESULT_PERMISSION_DENIED);
response.Result = static_cast<uint8>(HOUSING_RESULT_PERMISSION_DENIED);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodEvictPlot: Player {} lacks permission for neighborhood {}",
@@ -1337,9 +1500,8 @@ void WorldSession::HandleNeighborhoodEvictPlot(WorldPackets::Neighborhood::Neigh
HousingResult result = neighborhood->EvictPlayer(evictedPlayerGuid);
WorldPackets::Neighborhood::NeighborhoodEvictPlotResponse response;
response.Result = static_cast<uint32>(result);
response.Result = static_cast<uint8>(result);
response.NeighborhoodGuid = neighborhoodEvictPlot.NeighborhoodGuid;
response.PlotGuid = plotGuid;
SendPacket(response.Write());
// Send eviction notice to the evicted player and broadcast roster update
@@ -1354,6 +1516,7 @@ void WorldSession::HandleNeighborhoodEvictPlot(WorldPackets::Neighborhood::Neigh
if (Player* evictedPlayer = ObjectAccessor::FindPlayer(evictedPlayerGuid))
{
WorldPackets::Neighborhood::NeighborhoodEvictPlotNotice notice;
notice.PlotId = neighborhoodEvictPlot.PlotIndex;
notice.NeighborhoodGuid = neighborhoodEvictPlot.NeighborhoodGuid;
notice.PlotGuid = plotGuid;
evictedPlayer->SendDirectMessage(notice.Write());
@@ -1404,13 +1567,23 @@ void WorldSession::HandleGetAvailableInitiativeRequest(WorldPackets::Neighborhoo
if (!player)
return;
TC_LOG_DEBUG("housing", "CMSG_GET_AVAILABLE_INITIATIVE_REQUEST NeighborhoodGuid: {}",
getAvailableInitiativeRequest.NeighborhoodGuid.ToString());
Neighborhood* neighborhood = sNeighborhoodMgr.ResolveNeighborhood(getAvailableInitiativeRequest.NeighborhoodGuid, player);
if (!neighborhood)
{
WorldPackets::Housing::GetPlayerInitiativeInfoResult response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
return;
}
// Respond with empty initiative list for now
// Initiative info is driven by DB2 data (NeighborhoodInitiative.db2) which the client
// already has. This response confirms the server acknowledges the initiative query.
WorldPackets::Housing::GetPlayerInitiativeInfoResult response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "CMSG_GET_AVAILABLE_INITIATIVE_REQUEST NeighborhoodGuid: {}, Player: {}",
getAvailableInitiativeRequest.NeighborhoodGuid.ToString(), player->GetGUID().ToString());
}
void WorldSession::HandleGetInitiativeActivityLogRequest(WorldPackets::Neighborhood::GetInitiativeActivityLogRequest const& getInitiativeActivityLogRequest)
@@ -1419,11 +1592,21 @@ void WorldSession::HandleGetInitiativeActivityLogRequest(WorldPackets::Neighborh
if (!player)
return;
TC_LOG_DEBUG("housing", "CMSG_GET_INITIATIVE_ACTIVITY_LOG_REQUEST NeighborhoodGuid: {}",
getInitiativeActivityLogRequest.NeighborhoodGuid.ToString());
Neighborhood* neighborhood = sNeighborhoodMgr.ResolveNeighborhood(getInitiativeActivityLogRequest.NeighborhoodGuid, player);
if (!neighborhood)
{
WorldPackets::Housing::GetInitiativeActivityLogResult response;
response.Result = static_cast<uint32>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
return;
}
// Respond with empty activity log
// Activity log entries are tracked per-neighborhood. The response confirms the query
// and the client uses cached initiative data from DB2 to display the log.
WorldPackets::Housing::GetInitiativeActivityLogResult response;
response.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "CMSG_GET_INITIATIVE_ACTIVITY_LOG_REQUEST NeighborhoodGuid: {}, Player: {}",
getInitiativeActivityLogRequest.NeighborhoodGuid.ToString(), player->GetGUID().ToString());
}
+31 -2
View File
@@ -34,12 +34,34 @@
#include "Transport.h"
#include "World.h"
#include "Neighborhood.h"
#include "NeighborhoodMgr.h"
void WorldSession::BuildNameQueryData(ObjectGuid guid, WorldPackets::Query::NameCacheLookupResult& lookupData)
{
Player* player = ObjectAccessor::FindConnectedPlayer(guid);
lookupData.Player = guid;
// Housing GUIDs (HighGuid::Housing, type 55) are resolved via HouseData
// instead of the player name cache. The client's NameCacheLookupResult
// structure includes an Optional<HouseLookupData> field for this purpose.
if (guid.GetHigh() == HighGuid::Housing)
{
Neighborhood const* neighborhood = sNeighborhoodMgr.GetNeighborhood(guid);
if (neighborhood && !neighborhood->GetName().empty())
{
lookupData.Result = RESPONSE_SUCCESS;
lookupData.HouseData.emplace();
lookupData.HouseData->Guid = guid;
// GetName() returns const std::string& with stable lifetime
lookupData.HouseData->Name = neighborhood->GetName();
}
else
lookupData.Result = RESPONSE_FAILURE;
return;
}
Player* player = ObjectAccessor::FindConnectedPlayer(guid);
lookupData.Data.emplace();
if (lookupData.Data->Initialize(guid, player))
lookupData.Result = RESPONSE_SUCCESS; // name known
@@ -51,7 +73,14 @@ void WorldSession::HandleQueryPlayerNames(WorldPackets::Query::QueryPlayerNames&
{
WorldPackets::Query::QueryPlayerNamesResponse response;
for (ObjectGuid guid : queryPlayerNames.Players)
{
// Log Housing GUID queries for debugging neighborhood name display
if (guid.GetHigh() == HighGuid::Housing)
TC_LOG_ERROR("housing", "CMSG_QUERY_PLAYER_NAMES: Client queried Housing GUID {} ? resolving via HouseData",
guid.ToString());
BuildNameQueryData(guid, response.Players.emplace_back());
}
SendPacket(response.Write());
}
@@ -211,6 +211,13 @@ void WorldSession::HandleGameObjectUseOpcode(WorldPackets::GameObject::GameObjUs
obj->Use(GetPlayer());
}
else
{
// Debug: Log failed GO interaction attempt for housing diagnostics
TC_LOG_DEBUG("housing", "HandleGameObjectUseOpcode: GetGameObjectIfCanInteractWith returned null "
"for guid={} player={}", packet.Guid.ToString(), GetPlayer()->GetGUID().ToString());
}
}
void WorldSession::HandleGameobjectReportUse(WorldPackets::GameObject::GameObjReportUse& packet)
+38 -6
View File
@@ -19,6 +19,7 @@
#include "Account.h"
#include "DatabaseEnv.h"
#include "DB2Stores.h"
#include "GameTime.h"
#include "HousingMgr.h"
#include "HousingPackets.h"
#include "Log.h"
@@ -59,6 +60,7 @@ bool Housing::LoadFromDB(PreparedQueryResult housing, PreparedQueryResult decor,
_exteriorLocked = fields[6].GetUInt8() != 0;
_houseSize = fields[7].GetUInt8();
_houseType = fields[8].GetUInt32();
_createTime = fields[9].GetUInt32();
// Load placed decor
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13
@@ -326,6 +328,7 @@ HousingResult Housing::Create(ObjectGuid neighborhoodGuid, uint8 plotIndex)
_exteriorLocked = false;
_houseSize = HOUSING_FIXTURE_SIZE_SMALL;
_houseType = 0;
_createTime = static_cast<uint32>(GameTime::GetGameTime());
// Generate a new house guid using the owner's low guid as a base
_houseGuid = ObjectGuid::Create<HighGuid::Housing>(/*subType*/ 3, /*arg1*/ sRealmList->GetCurrentRealmId().Realm, /*arg2*/ 7, _owner->GetGUID().GetCounter());
@@ -337,6 +340,16 @@ HousingResult Housing::Create(ObjectGuid neighborhoodGuid, uint8 plotIndex)
return HOUSING_RESULT_SUCCESS;
}
ObjectGuid Housing::GetPlotGuid() const
{
// Deterministic PlotGUID: subType=2 encodes neighborhood + plot index
return ObjectGuid::Create<HighGuid::Housing>(
/*subType*/ 2,
/*arg1*/ sRealmList->GetCurrentRealmId().Realm,
/*arg2*/ _plotIndex,
_neighborhoodGuid.GetCounter());
}
void Housing::Delete()
{
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
@@ -1184,9 +1197,15 @@ void Housing::AddLevel(uint32 amount)
if (_owner && _owner->GetSession())
{
WorldPackets::Housing::HousingSvcsUpdateHousesLevelFavor levelUpdate;
levelUpdate.Type = 0;
levelUpdate.PreviousFavor = static_cast<int32>(_level > 1 ? _favor : -1);
levelUpdate.PreviousLevel = static_cast<int32>(_level - amount);
levelUpdate.NewLevel = static_cast<int32>(_level);
levelUpdate.Field4 = 0;
levelUpdate.HouseGuid = _houseGuid;
levelUpdate.Level = _level;
levelUpdate.Favor = _favor64;
levelUpdate.PreviousLevelId = -1;
levelUpdate.NextLevelFavorCost = -1;
levelUpdate.Flags = 0x8000;
_owner->SendDirectMessage(levelUpdate.Write());
}
}
@@ -1205,9 +1224,15 @@ void Housing::AddFavor(uint64 amount)
if (_owner && _owner->GetSession())
{
WorldPackets::Housing::HousingSvcsUpdateHousesLevelFavor favorUpdate;
favorUpdate.Type = 0;
favorUpdate.PreviousFavor = static_cast<int32>(_favor);
favorUpdate.PreviousLevel = static_cast<int32>(_level);
favorUpdate.NewLevel = static_cast<int32>(_level);
favorUpdate.Field4 = 0;
favorUpdate.HouseGuid = _houseGuid;
favorUpdate.Level = _level;
favorUpdate.Favor = _favor64;
favorUpdate.PreviousLevelId = -1;
favorUpdate.NextLevelFavorCost = -1;
favorUpdate.Flags = 0x8000;
_owner->SendDirectMessage(favorUpdate.Write());
}
}
@@ -1219,6 +1244,7 @@ void Housing::OnQuestCompleted(uint32 questId)
uint32 nextLevelQuestId = sHousingMgr.GetQuestForLevel(_level + 1);
if (nextLevelQuestId > 0 && nextLevelQuestId == questId)
{
uint32 previousLevel = _level;
_level++;
TC_LOG_DEBUG("housing", "Housing::OnQuestCompleted: Player {} house leveled up to {} (quest {}) in house {}",
_owner->GetName(), _level, questId, _houseGuid.ToString());
@@ -1229,9 +1255,15 @@ void Housing::OnQuestCompleted(uint32 questId)
if (_owner && _owner->GetSession())
{
WorldPackets::Housing::HousingSvcsUpdateHousesLevelFavor levelUpdate;
levelUpdate.Type = 0;
levelUpdate.PreviousFavor = static_cast<int32>(_favor);
levelUpdate.PreviousLevel = static_cast<int32>(previousLevel);
levelUpdate.NewLevel = static_cast<int32>(_level);
levelUpdate.Field4 = 0;
levelUpdate.HouseGuid = _houseGuid;
levelUpdate.Level = _level;
levelUpdate.Favor = _favor64;
levelUpdate.PreviousLevelId = -1;
levelUpdate.NextLevelFavorCost = -1;
levelUpdate.Flags = 0x8000;
_owner->SendDirectMessage(levelUpdate.Write());
}
}
+3
View File
@@ -92,7 +92,9 @@ public:
Player* GetOwner() const { return _owner; }
ObjectGuid GetHouseGuid() const { return _houseGuid; }
ObjectGuid GetNeighborhoodGuid() const { return _neighborhoodGuid; }
ObjectGuid GetPlotGuid() const;
uint8 GetPlotIndex() const { return _plotIndex; }
uint32 GetCreateTime() const { return _createTime; }
uint32 GetLevel() const { return _level; }
uint32 GetFavor() const { return _favor; }
uint32 GetSettingsFlags() const { return _settingsFlags; }
@@ -197,6 +199,7 @@ private:
bool _exteriorLocked = false;
uint8 _houseSize = HOUSING_FIXTURE_SIZE_SMALL;
uint32 _houseType = 0;
uint32 _createTime = 0;
// WeightCost-based budget tracking
uint32 _interiorDecorWeightUsed = 0;
+21 -16
View File
@@ -94,7 +94,7 @@ void HousingMap::SpawnPlotGameObjects()
LoadGrid(x, y);
// Retail always uses CornerstoneGameObjectID (457142) for ALL plots.
// Ownership state is communicated via GOState: 0 (ACTIVE) = ForSale, 1 (READY) = Owned.
// Ownership state via GOState: 0 (ACTIVE) = Owned/Claimed, 1 (READY) = ForSale sign.
Neighborhood::PlotInfo const* plotInfo = _neighborhood->GetPlotInfo(static_cast<uint8>(plot->PlotIndex));
uint32 goEntry = static_cast<uint32>(plot->CornerstoneGameObjectID);
bool isOwned = plotInfo && !plotInfo->OwnerGuid.IsEmpty();
@@ -115,8 +115,8 @@ void HousingMap::SpawnPlotGameObjects()
float rotZ = plot->CornerstoneRotation[2];
QuaternionData rot = QuaternionData::fromEulerAnglesZYX(rotZ, plot->CornerstoneRotation[1], plot->CornerstoneRotation[0]);
// Retail sniff: ForSale = GOState 0 (ACTIVE), Owned = GOState 1 (READY)
GOState plotState = isOwned ? GO_STATE_READY : GO_STATE_ACTIVE;
// GOState 0 (ACTIVE) = Owned/Claimed cornerstone, GOState 1 (READY) = ForSale sign
GOState plotState = isOwned ? GO_STATE_ACTIVE : GO_STATE_READY;
Position pos(x, y, z, rotZ);
GameObject* go = GameObject::CreateGameObject(goEntry, this, pos, rot, 255, plotState);
@@ -233,8 +233,8 @@ void HousingMap::SetPlotOwnershipState(uint8 plotIndex, bool owned)
return;
// Toggle GOState on the existing Cornerstone GO.
// Retail sniff: GOState 0 (ACTIVE) = ForSale, GOState 1 (READY) = Owned.
GOState newState = owned ? GO_STATE_READY : GO_STATE_ACTIVE;
// GOState 0 (ACTIVE) = Owned/Claimed cornerstone, GOState 1 (READY) = ForSale sign
GOState newState = owned ? GO_STATE_ACTIVE : GO_STATE_READY;
auto itr = _plotGameObjects.find(plotIndex);
if (itr != _plotGameObjects.end())
@@ -304,9 +304,9 @@ bool HousingMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/)
}
// Do NOT auto-add the player as a neighborhood member here.
// Membership is granted when the player buys a plot or is invited.
// Auto-adding causes the client to resolve neighborhoodOwnerType as
// Self instead of None, which prevents the "For Sale" Cornerstone UI.
// Membership is granted when the player buys a plot or is invited.
// Auto-adding causes the client to resolve neighborhoodOwnerType as
// Self instead of None, which prevents the "For Sale" Cornerstone UI.
// Track player housing if they own a house in this neighborhood
Housing* housing = player->GetHousingForNeighborhood(_neighborhood->GetGuid());
@@ -319,17 +319,22 @@ bool HousingMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/)
// Send neighborhood context so the client can call SetViewingNeighborhood()
// and enable Cornerstone purchase UI interaction
WorldPackets::Housing::HousingGetCurrentHouseInfoResponse houseInfo;
houseInfo.NeighborhoodGuid = _neighborhood->GetGuid();
if (housing)
{
houseInfo.HouseGuid = housing->GetHouseGuid();
houseInfo.OwnerPlayerGuid = player->GetGUID();
houseInfo.PlotIndex = housing->GetPlotIndex();
houseInfo.HouseProperties = housing->GetSettingsFlags() & 0xFF;
houseInfo.HouseLevel = static_cast<uint8>(housing->GetLevel());
// Sniff-verified: OwnerGuid=HouseGUID, SecondaryOwnerGuid=PlotGUID, PlotGuid=NeighborhoodGUID
houseInfo.HouseInfo.OwnerGuid = housing->GetHouseGuid();
houseInfo.HouseInfo.SecondaryOwnerGuid = housing->GetPlotGuid();
houseInfo.HouseInfo.PlotGuid = housing->GetNeighborhoodGuid();
houseInfo.HouseInfo.Flags = housing->GetPlotIndex();
houseInfo.HouseInfo.HouseTypeId = 32;
houseInfo.HouseInfo.StatusFlags = 0;
}
// No house: OwnerPlayerGuid stays empty. Only NeighborhoodGuid is set
// so the client knows which neighborhood it's viewing.
else if (_neighborhood)
{
// No house ? provide neighborhood GUID in PlotGuid field for purchase UI context
houseInfo.HouseInfo.PlotGuid = _neighborhood->GetGuid();
}
houseInfo.ResponseFlags = 0;
player->SendDirectMessage(houseInfo.Write());
TC_LOG_DEBUG("housing", "HousingMap::AddPlayerToMap: Sent neighborhood context to player {} (neighborhood='{}', hasHouse={})",
+27
View File
@@ -382,6 +382,19 @@ std::vector<NeighborhoodPlotData const*> HousingMgr::GetPlotsForMap(uint32 neigh
return {};
}
NeighborhoodPlotData const* HousingMgr::GetPlotByCornerstoneEntry(uint32 neighborhoodMapId, uint32 cornerstoneGoEntry) const
{
auto itr = _plotsByMap.find(neighborhoodMapId);
if (itr == _plotsByMap.end())
return nullptr;
for (NeighborhoodPlotData const* plot : itr->second)
if (static_cast<uint32>(plot->CornerstoneGameObjectID) == cornerstoneGoEntry)
return plot;
return nullptr;
}
std::string HousingMgr::GenerateNeighborhoodName(uint32 neighborhoodMapId) const
{
auto itr = _nameGenByMap.find(neighborhoodMapId);
@@ -487,6 +500,20 @@ uint32 HousingMgr::GetRoomWeightCost(uint32 roomEntryId) const
return 1;
}
std::vector<uint32> HousingMgr::GetStarterDecorIds() const
{
std::vector<uint32> result;
for (auto const& [id, decor] : _houseDecorStore)
{
if (decor.StartingQuantity > 0)
{
for (int32 i = 0; i < decor.StartingQuantity; ++i)
result.push_back(id);
}
}
return result;
}
HousingResult HousingMgr::ValidateDecorPlacement(uint32 decorId, Position const& pos, uint32 houseLevel) const
{
HouseDecorData const* decorEntry = GetHouseDecorData(decorId);
+5
View File
@@ -239,6 +239,8 @@ public:
// Neighborhood plot lookups
std::vector<NeighborhoodPlotData const*> GetPlotsForMap(uint32 neighborhoodMapId) const;
// Find a plot by its cornerstone GO entry within a specific neighborhood map
NeighborhoodPlotData const* GetPlotByCornerstoneEntry(uint32 neighborhoodMapId, uint32 cornerstoneGoEntry) const;
// Check if a world MapID corresponds to a neighborhood map
bool IsNeighborhoodWorldMap(uint32 mapId) const;
@@ -265,6 +267,9 @@ public:
uint32 GetRoomDoorCount(uint32 roomEntryId) const;
std::vector<RoomDoorInfo> const* GetRoomDoors(uint32 roomWmoDataId) const;
// Starter decor (items granted on first house purchase)
std::vector<uint32> GetStarterDecorIds() const;
// Validation
HousingResult ValidateDecorPlacement(uint32 decorId, Position const& pos, uint32 houseLevel) const;
+4 -1
View File
@@ -42,7 +42,10 @@ bool Neighborhood::LoadFromDB(PreparedQueryResult neighborhood, PreparedQueryRes
// _guid is already set in constructor
_name = fields[1].GetString();
_neighborhoodMapID = fields[2].GetUInt32();
_ownerGuid = ObjectGuid::Create<HighGuid::Player>(fields[3].GetUInt64());
{
uint64 ownerCounter = fields[3].GetUInt64();
_ownerGuid = ownerCounter ? ObjectGuid::Create<HighGuid::Player>(ownerCounter) : ObjectGuid::Empty;
}
_factionRestriction = fields[4].GetInt32();
_isPublic = fields[5].GetBool();
_createTime = fields[6].GetUInt32();
+48 -44
View File
@@ -271,6 +271,15 @@ Neighborhood* NeighborhoodMgr::GetNeighborhoodByOwner(ObjectGuid ownerGuid)
return nullptr;
}
std::vector<Neighborhood*> NeighborhoodMgr::GetAllNeighborhoods() const
{
std::vector<Neighborhood*> result;
result.reserve(_neighborhoods.size());
for (auto const& [guid, neighborhood] : _neighborhoods)
result.push_back(neighborhood.get());
return result;
}
std::vector<Neighborhood*> NeighborhoodMgr::GetPublicNeighborhoods() const
{
std::vector<Neighborhood*> result;
@@ -293,6 +302,23 @@ std::vector<Neighborhood*> NeighborhoodMgr::GetNeighborhoodsForPlayer(ObjectGuid
return result;
}
std::vector<Neighborhood*> NeighborhoodMgr::GetNeighborhoodsByBnetAccount(ObjectGuid bnetAccountGuid) const
{
std::vector<Neighborhood*> result;
for (auto const& [guid, neighborhood] : _neighborhoods)
{
for (auto const& plot : neighborhood->GetPlots())
{
if (plot.IsOccupied() && plot.OwnerBnetGuid == bnetAccountGuid)
{
result.push_back(neighborhood.get());
break; // Only add each neighborhood once
}
}
}
return result;
}
std::string NeighborhoodMgr::GetNeighborhoodName(ObjectGuid neighborhoodGuid) const
{
Neighborhood const* neighborhood = GetNeighborhood(neighborhoodGuid);
@@ -312,25 +338,14 @@ Neighborhood* NeighborhoodMgr::FindNeighborhoodWithPendingInvite(ObjectGuid play
return nullptr;
}
Neighborhood* NeighborhoodMgr::FindOrCreateTutorialNeighborhood(ObjectGuid playerGuid, uint32 teamId)
Neighborhood* NeighborhoodMgr::FindOrCreatePublicNeighborhood(uint32 teamId)
{
// Check if player already belongs to a neighborhood
std::vector<Neighborhood*> existing = GetNeighborhoodsForPlayer(playerGuid);
if (!existing.empty())
{
TC_LOG_DEBUG("housing", "FindOrCreateTutorialNeighborhood: Player {} already in neighborhood '{}'",
playerGuid.ToString(), existing[0]->GetName());
return existing[0];
}
// Determine the correct NeighborhoodMapID for the player's faction
// NeighborhoodMap flags: bit 0 = Alliance, bit 1 = Horde, bit 2 = CanSystemGenerate
// Determine the correct NeighborhoodMapID for the faction
uint32 targetMapId = 0;
int32 factionRestriction = NEIGHBORHOOD_FACTION_NONE;
for (auto const& [id, data] : sHousingMgr.GetAllNeighborhoodMapData())
{
int32 flags = data.UiMapID; // UiMapID stores DB2 FactionRestriction/Flags field
int32 flags = data.UiMapID;
bool isAlliance = (flags & 0x1) != 0;
bool isHorde = (flags & 0x2) != 0;
bool canSystemGenerate = (flags & 0x4) != 0;
@@ -338,51 +353,40 @@ Neighborhood* NeighborhoodMgr::FindOrCreateTutorialNeighborhood(ObjectGuid playe
if (!canSystemGenerate)
continue;
if (teamId == ALLIANCE && isAlliance)
if ((teamId == ALLIANCE && isAlliance) || (teamId == HORDE && isHorde))
{
targetMapId = id;
factionRestriction = NEIGHBORHOOD_FACTION_ALLIANCE;
break;
}
else if (teamId == HORDE && isHorde)
{
targetMapId = id;
factionRestriction = NEIGHBORHOOD_FACTION_HORDE;
break;
}
}
if (targetMapId == 0)
{
TC_LOG_ERROR("housing", "FindOrCreateTutorialNeighborhood: No system-generatable NeighborhoodMap found for team {}",
teamId);
TC_LOG_ERROR("housing", "FindOrCreatePublicNeighborhood: No system-generatable NeighborhoodMap found for team {}", teamId);
return nullptr;
}
// Look for an existing public neighborhood on the target map with available plots
// 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.
// Force-run it now as a fallback, then retry.
TC_LOG_WARN("housing", "FindOrCreatePublicNeighborhood: No public neighborhood for map {}, running EnsurePublicNeighborhoods", targetMapId);
EnsurePublicNeighborhoods();
return FindPublicNeighborhoodForMap(targetMapId);
}
Neighborhood* NeighborhoodMgr::FindPublicNeighborhoodForMap(uint32 neighborhoodMapId) const
{
for (auto const& [guid, neighborhood] : _neighborhoods)
{
if (neighborhood->GetNeighborhoodMapID() == targetMapId && neighborhood->IsPublic())
{
// Ensure the player is a member (they may not be if they didn't create this neighborhood)
neighborhood->AddResident(playerGuid);
TC_LOG_DEBUG("housing", "FindOrCreateTutorialNeighborhood: Found existing public neighborhood '{}' for player {}",
neighborhood->GetName(), playerGuid.ToString());
if (neighborhood->GetNeighborhoodMapID() == neighborhoodMapId && neighborhood->IsPublic())
return neighborhood.get();
}
}
// Create a new public, system-generated neighborhood with the player as owner
std::string name = sHousingMgr.GenerateNeighborhoodName(targetMapId);
Neighborhood* neighborhood = CreateNeighborhood(playerGuid, name, targetMapId, factionRestriction, /*isPublic*/ true);
if (neighborhood)
{
TC_LOG_INFO("housing", "FindOrCreateTutorialNeighborhood: Created tutorial neighborhood '{}' (map {}) for player {}",
name, targetMapId, playerGuid.ToString());
}
return neighborhood;
return nullptr;
}
void NeighborhoodMgr::EnsurePublicNeighborhoods()
+7 -2
View File
@@ -54,13 +54,18 @@ public:
// Queries
Neighborhood* GetNeighborhoodByOwner(ObjectGuid ownerGuid);
std::vector<Neighborhood*> GetAllNeighborhoods() const;
std::vector<Neighborhood*> GetPublicNeighborhoods() const;
std::vector<Neighborhood*> GetNeighborhoodsForPlayer(ObjectGuid playerGuid) const;
std::vector<Neighborhood*> GetNeighborhoodsByBnetAccount(ObjectGuid bnetAccountGuid) const;
std::string GetNeighborhoodName(ObjectGuid neighborhoodGuid) const;
Neighborhood* FindNeighborhoodWithPendingInvite(ObjectGuid playerGuid);
// Tutorial support
Neighborhood* FindOrCreateTutorialNeighborhood(ObjectGuid playerGuid, uint32 teamId);
// Find or create a public neighborhood for a faction (no membership changes)
Neighborhood* FindOrCreatePublicNeighborhood(uint32 teamId);
// Find a public neighborhood on the given map (for visitors, no membership change)
Neighborhood* FindPublicNeighborhoodForMap(uint32 neighborhoodMapId) const;
// Expansion
void CheckAndExpandNeighborhoods();
+6 -4
View File
@@ -288,12 +288,14 @@ Map* MapManager::CreateMap(uint32 mapId, Player* player, Optional<uint32> lfgDun
}
}
// Auto-assign if not already a member
// If the player isn't a member of any neighborhood on this map,
// find an existing public neighborhood for map rendering only.
// Do NOT auto-add the player as a member ? membership is only
// granted through the tutorial flow, buying a plot, or being invited.
if (!neighborhood)
{
TC_LOG_DEBUG("housing", "MapManager::CreateMap: No existing membership, auto-assigning tutorial neighborhood");
neighborhood = sNeighborhoodMgr.FindOrCreateTutorialNeighborhood(
player->GetGUID(), player->GetTeam());
TC_LOG_DEBUG("housing", "MapManager::CreateMap: No existing membership, finding public neighborhood for viewing");
neighborhood = sNeighborhoodMgr.FindPublicNeighborhoodForMap(neighborhoodMapId);
}
if (!neighborhood)
+141 -90
View File
@@ -355,18 +355,20 @@ namespace WorldPackets::Housing
WorldPacket const* QueryNeighborhoodNameResponse::Write()
{
// Wire format verified against retail 12.0.1 build 65940 (opcode 0x460012)
// Retail: [PackedGUID] [0x80] [uint8 len] [string bytes]
// Byte after GUID: bit 7 = Allow, bits 6-0 = zero padding (NOT a length field)
// The 7-bit field was incorrectly used as SizedString::BitsSize<7> before,
// causing the client to read NameLen bytes before the uint8 prefix, shifting everything.
_worldPacket << NeighborhoodGuid;
_worldPacket << Bits<1>(Allow);
_worldPacket.FlushBits();
if (Allow)
{
_worldPacket << SizedString::BitsSize<7>(Name);
_worldPacket.FlushBits();
_worldPacket << SizedString::Data(Name);
_worldPacket << uint8(Name.size());
_worldPacket.WriteString(Name);
}
else
_worldPacket.FlushBits();
return &_worldPacket;
}
@@ -669,17 +671,15 @@ namespace WorldPackets::Housing
return &_worldPacket;
}
static void WriteJamCurrentHouseInfo(WorldPacket& packet, JamCurrentHouseInfo const& info);
WorldPacket const* HousingSvcsGetPlayerHousesInfoResponse::Write()
{
// Sniff-verified: uint32 Count + uint8 Unknown + JamCurrentHouseInfo per house
_worldPacket << uint32(Houses.size());
_worldPacket << uint8(Unknown);
for (auto const& house : Houses)
{
_worldPacket << house.HouseGuid;
_worldPacket << house.NeighborhoodGuid;
_worldPacket << uint8(house.PlotIndex);
_worldPacket << uint8(house.Level);
}
WriteJamCurrentHouseInfo(_worldPacket, house);
return &_worldPacket;
}
@@ -708,9 +708,16 @@ namespace WorldPackets::Housing
WorldPacket const* HousingSvcsUpdateHousesLevelFavor::Write()
{
// Sniff-verified (36 bytes): uint8 + 4x int32 + PackedGUID + 2x int32 + uint16
_worldPacket << uint8(Type);
_worldPacket << int32(PreviousFavor);
_worldPacket << int32(PreviousLevel);
_worldPacket << int32(NewLevel);
_worldPacket << int32(Field4);
_worldPacket << HouseGuid;
_worldPacket << uint32(Level);
_worldPacket << uint64(Favor);
_worldPacket << int32(PreviousLevelId);
_worldPacket << int32(NextLevelFavorCost);
_worldPacket << uint16(Flags);
return &_worldPacket;
}
@@ -870,26 +877,46 @@ namespace WorldPackets::Housing
// Housing General SMSG Responses (0x55xxxx)
// ============================================================
// Helper: Write JamCurrentHouseInfo struct to packet (IDA sub_7FF6F6E0A170)
static void WriteJamCurrentHouseInfo(WorldPacket& packet, JamCurrentHouseInfo const& info)
{
packet << info.OwnerGuid;
packet << info.SecondaryOwnerGuid;
packet << info.PlotGuid;
packet << uint8(info.Flags);
packet << uint32(info.HouseTypeId);
uint8 statusFlags = info.StatusFlags;
if (info.HouseId)
statusFlags |= 0x80;
packet << uint8(statusFlags);
if (info.HouseId)
packet << uint64(*info.HouseId);
}
// Helper: Write JamNeighborhoodRosterEntry struct to packet (IDA sub_7FF6F6E0A460)
static void WriteJamNeighborhoodRosterEntry(WorldPacket& packet, JamNeighborhoodRosterEntry const& entry)
{
packet << uint64(entry.Timestamp);
packet << entry.PlayerGuid;
packet << entry.HouseGuid;
packet << uint64(entry.ExtraData);
}
WorldPacket const* HousingHouseStatusResponse::Write()
{
// Sniff-verified (0x550000): 3x PackedGUID + uint32 Status
_worldPacket << HouseGuid;
_worldPacket << OwnerBNetGuid;
_worldPacket << OwnerPlayerGuid;
_worldPacket << uint16(HouseStatus);
_worldPacket << uint8(PlotIndex);
_worldPacket << uint8(StatusFlags);
_worldPacket << HouseTemplateGuid;
_worldPacket << PlotGuid;
_worldPacket << uint32(Status);
return &_worldPacket;
}
WorldPacket const* HousingGetCurrentHouseInfoResponse::Write()
{
_worldPacket << HouseGuid;
_worldPacket << OwnerPlayerGuid;
_worldPacket << NeighborhoodGuid;
_worldPacket << uint8(PlotIndex);
_worldPacket << uint8(HouseProperties);
_worldPacket << uint8(HouseLevel);
_worldPacket << uint32(Reserved);
// IDA 12.0 verified (0x550001): JamCurrentHouseInfo + uint8 ResponseFlags
WriteJamCurrentHouseInfo(_worldPacket, HouseInfo);
_worldPacket << uint8(ResponseFlags);
return &_worldPacket;
}
@@ -905,14 +932,17 @@ namespace WorldPackets::Housing
WorldPacket const* HousingGetPlayerPermissionsResponse::Write()
{
// IDA 12.0 verified (0x550006): PackedGUID + uint8 ResultCode + uint8 Permissions
_worldPacket << HouseGuid;
_worldPacket << uint16(PermissionFlags);
_worldPacket << uint8(ResultCode);
_worldPacket << uint8(PermissionFlags);
return &_worldPacket;
}
WorldPacket const* HousingResetKioskModeResponse::Write()
{
_worldPacket << uint32(Result);
// IDA 12.0 verified (0x550007): single uint8
_worldPacket << uint8(Result);
return &_worldPacket;
}
@@ -979,6 +1009,18 @@ namespace WorldPackets::Housing
return &_worldPacket;
}
WorldPacket const* HousingPhotoSharingAuthorizationResult::Write()
{
_worldPacket << uint32(Result);
return &_worldPacket;
}
WorldPacket const* HousingPhotoSharingAuthorizationClearedResult::Write()
{
_worldPacket << uint32(Result);
return &_worldPacket;
}
} // namespace WorldPackets::Housing
// ============================================================
@@ -1171,136 +1213,144 @@ namespace WorldPackets::Neighborhood
WorldPacket const* NeighborhoodUpdateNameResponse::Write()
{
_worldPacket << uint32(Result);
_worldPacket << NeighborhoodGuid;
// IDA 12.0 verified (0x5C0003): single uint8
_worldPacket << uint8(Result);
return &_worldPacket;
}
WorldPacket const* NeighborhoodUpdateNameNotification::Write()
{
_worldPacket << NeighborhoodGuid;
_worldPacket << SizedString::BitsSize<7>(NewName);
_worldPacket.FlushBits();
_worldPacket << SizedString::Data(NewName);
// IDA 12.0 verified (0x5C0004): uint8(nameLen) + bytes[nameLen]
uint8 nameLen = NewName.empty() ? 0 : static_cast<uint8>(NewName.size() + 1);
_worldPacket << uint8(nameLen);
if (nameLen > 0)
_worldPacket.append(reinterpret_cast<uint8 const*>(NewName.c_str()), nameLen);
return &_worldPacket;
}
WorldPacket const* NeighborhoodAddSecondaryOwnerResponse::Write()
{
_worldPacket << uint32(Result);
_worldPacket << NeighborhoodGuid;
// IDA 12.0 verified (0x5C0006): PackedGUID + uint8 Result
_worldPacket << PlayerGuid;
_worldPacket << uint8(Result);
return &_worldPacket;
}
WorldPacket const* NeighborhoodRemoveSecondaryOwnerResponse::Write()
{
_worldPacket << uint32(Result);
_worldPacket << NeighborhoodGuid;
// IDA 12.0 verified (0x5C0007): PackedGUID + uint8 Result
_worldPacket << PlayerGuid;
_worldPacket << uint8(Result);
return &_worldPacket;
}
WorldPacket const* NeighborhoodBuyHouseResponse::Write()
{
_worldPacket << uint32(Result);
_worldPacket << HouseGuid;
_worldPacket << NeighborhoodGuid;
_worldPacket << uint8(PlotIndex);
// IDA 12.0 verified (0x5C0008): JamCurrentHouseInfo + uint8 Result
Housing::WriteJamCurrentHouseInfo(_worldPacket, HouseInfo);
_worldPacket << uint8(Result);
return &_worldPacket;
}
WorldPacket const* NeighborhoodMoveHouseResponse::Write()
{
_worldPacket << uint32(Result);
_worldPacket << NeighborhoodGuid;
_worldPacket << uint8(NewPlotIndex);
// IDA 12.0 verified (0x5C0009): JamCurrentHouseInfo + PackedGUID + uint8 Result
Housing::WriteJamCurrentHouseInfo(_worldPacket, HouseInfo);
_worldPacket << MoveTransactionGuid;
_worldPacket << uint8(Result);
return &_worldPacket;
}
WorldPacket const* NeighborhoodOpenCornerstoneUIResponse::Write()
{
// Wire format verified against IDA client deserializer sub_7FF6F6E3E200 (12.0, opcode 0x5C000A)
_worldPacket << uint32(Result);
_worldPacket << NeighborhoodGuid; // PackedGUID ? neighborhood identity
_worldPacket << PlotGuid; // PackedGUID ? plot owner (empty = unclaimed "For Sale")
_worldPacket << uint64(Cost);
_worldPacket << uint8(PlotIndex);
// Wire format verified against retail 12.0.1 build 65940 packet captures (Alliance + Horde)
// IDA deserializer sub_7FF6F6E3E200: uint32?+32, GUID?+40, GUID?+56, uint64?+72, uint8?+80, GUID?+128
// Fixed fields
_worldPacket << uint32(PlotIndex); // Echoed from CMSG (NOT a result code)
_worldPacket << PlotOwnerGuid; // ?+40: Player GUID when owned, Empty when unclaimed
_worldPacket << NeighborhoodGuid; // ?+56: Housing GUID when owned, Empty when unclaimed
_worldPacket << uint64(Cost); // ?+72: Purchase price
_worldPacket << uint8(PurchaseStatus); // ?+80: 73=purchasable, 0=not. Client checks ==73
_worldPacket << CornerstoneGuid; // ?+128: Cornerstone game object
// Length-prefixed string: uint32 byteCount (including null terminator) + bytes
// Client reader sub_7FF6F97E62B0 skips read when length <= 1
uint32 nameLen = NeighborhoodName.empty() ? 0 : static_cast<uint32>(NeighborhoodName.size() + 1);
_worldPacket << uint32(nameLen);
if (nameLen > 0)
_worldPacket.append(reinterpret_cast<uint8 const*>(NeighborhoodName.c_str()), nameLen);
// Bit-packed section: 1 bool + 8-bit nameLen + 6 bools = 15 bits = 2 bytes
// Retail uses NUL-terminated CString: NameLen includes the NUL byte
_worldPacket << Bits<1>(IsPlotOwned);
_worldPacket << SizedCString::BitsSize<8>(NeighborhoodName);
_worldPacket << OptionalInit(AlternatePrice);
_worldPacket << Bits<1>(CanPurchase);
_worldPacket.WriteBit(false); // HasOptionalStruct ? not yet implemented
_worldPacket << Bits<1>(HasResidents);
_worldPacket << OptionalInit(StatusValue);
_worldPacket << Bits<1>(IsInitiative);
_worldPacket.FlushBits();
// Variable-length data (order matches client deserialization)
// Optional struct data would go here if HasOptionalStruct was set
_worldPacket << SizedCString::Data(NeighborhoodName);
if (AlternatePrice)
_worldPacket << uint64(*AlternatePrice);
if (StatusValue)
_worldPacket << uint32(*StatusValue);
_worldPacket << uint8(0); // OptionalFieldBits ? no optional sub-structs
return &_worldPacket;
}
WorldPacket const* NeighborhoodInviteResidentResponse::Write()
{
_worldPacket << uint32(Result);
_worldPacket << NeighborhoodGuid;
// IDA 12.0 verified (0x5C000B): uint8 Result + PackedGUID
_worldPacket << uint8(Result);
_worldPacket << InviteeGuid;
return &_worldPacket;
}
WorldPacket const* NeighborhoodCancelInvitationResponse::Write()
{
_worldPacket << uint32(Result);
_worldPacket << NeighborhoodGuid;
// IDA 12.0 verified (0x5C000C): uint8 Result + PackedGUID
_worldPacket << uint8(Result);
_worldPacket << InviteeGuid;
return &_worldPacket;
}
WorldPacket const* NeighborhoodDeclineInvitationResponse::Write()
{
_worldPacket << uint32(Result);
// IDA 12.0 verified (0x5C000D): uint8 Result + PackedGUID
_worldPacket << uint8(Result);
_worldPacket << NeighborhoodGuid;
return &_worldPacket;
}
WorldPacket const* NeighborhoodPlayerGetInviteResponse::Write()
{
_worldPacket << uint32(Result);
_worldPacket << NeighborhoodGuid;
_worldPacket << InviterGuid;
_worldPacket << uint32(InviteTime);
_worldPacket << SizedString::BitsSize<7>(NeighborhoodName);
_worldPacket.FlushBits();
_worldPacket << SizedString::Data(NeighborhoodName);
// IDA 12.0 verified (0x5C000E): uint8 Result + JamNeighborhoodRosterEntry(48 bytes)
_worldPacket << uint8(Result);
Housing::WriteJamNeighborhoodRosterEntry(_worldPacket, Entry);
return &_worldPacket;
}
WorldPacket const* NeighborhoodGetInvitesResponse::Write()
{
_worldPacket << uint32(Result);
// IDA 12.0 verified (0x5C000F): uint8 Result + uint32 Count + RosterEntry[Count]
_worldPacket << uint8(Result);
_worldPacket << uint32(Invites.size());
for (auto const& invite : Invites)
{
_worldPacket << invite.InviteeGuid;
_worldPacket << invite.InviterGuid;
_worldPacket << uint32(invite.InviteTime);
}
Housing::WriteJamNeighborhoodRosterEntry(_worldPacket, invite);
return &_worldPacket;
}
WorldPacket const* NeighborhoodInviteNotification::Write()
{
// IDA 12.0 verified (0x5C0010): single PackedGUID
_worldPacket << NeighborhoodGuid;
_worldPacket << InviterGuid;
_worldPacket << SizedString::BitsSize<7>(NeighborhoodName);
_worldPacket.FlushBits();
_worldPacket << SizedString::Data(NeighborhoodName);
return &_worldPacket;
}
WorldPacket const* NeighborhoodOfferOwnershipResponse::Write()
{
_worldPacket << uint32(Result);
_worldPacket << NeighborhoodGuid;
_worldPacket << NewOwnerGuid;
// IDA 12.0 verified (0x5C0011): single uint8 Result
_worldPacket << uint8(Result);
return &_worldPacket;
}
@@ -1361,7 +1411,8 @@ namespace WorldPackets::Neighborhood
{
_worldPacket << member.PlayerGuid; // PackedGUID
_worldPacket << uint8(0); // Status field 1
_worldPacket << uint8(0); // Status field 2 (only bit 7 used by client)
// Status field 2: bit 7 (0x80) = online flag, checked by client UI for roster display
_worldPacket << uint8(member.IsOnline ? 0x80 : 0x00);
}
// Step 6: Optional trailing GUID (skipped since MainFlags.bit7 = 0)
@@ -1382,24 +1433,24 @@ namespace WorldPackets::Neighborhood
WorldPacket const* NeighborhoodInviteNameLookupResult::Write()
{
_worldPacket << uint32(Result);
// IDA 12.0 verified (0x5C0014): uint8 Result + PackedGUID
_worldPacket << uint8(Result);
_worldPacket << PlayerGuid;
_worldPacket << SizedString::BitsSize<7>(PlayerName);
_worldPacket.FlushBits();
_worldPacket << SizedString::Data(PlayerName);
return &_worldPacket;
}
WorldPacket const* NeighborhoodEvictPlotResponse::Write()
{
_worldPacket << uint32(Result);
// IDA 12.0 verified (0x5C0015): uint8 Result + PackedGUID
_worldPacket << uint8(Result);
_worldPacket << NeighborhoodGuid;
_worldPacket << PlotGuid;
return &_worldPacket;
}
WorldPacket const* NeighborhoodEvictPlotNotice::Write()
{
// IDA 12.0 verified (0x5C0016): uint32 + PackedGUID + PackedGUID
_worldPacket << uint32(PlotId);
_worldPacket << NeighborhoodGuid;
_worldPacket << PlotGuid;
return &_worldPacket;
+145 -82
View File
@@ -27,6 +27,33 @@
namespace WorldPackets::Housing
{
// ============================================================
// Shared JAM Structs (verified against IDA 12.0 deserializers)
// ============================================================
// JamCurrentHouseInfo ? sub_7FF6F6E0A170 (80 bytes, used by 0x550001, 0x5C0008, 0x5C0009)
// Wire order: PackedGUID + PackedGUID + PackedGUID + uint8 + uint32 + uint8(bit7=has_optional) + [optional uint64]
struct JamCurrentHouseInfo
{
ObjectGuid OwnerGuid;
ObjectGuid SecondaryOwnerGuid;
ObjectGuid PlotGuid;
uint8 Flags = 0;
uint32 HouseTypeId = 0;
uint8 StatusFlags = 0; // bit 7 = has optional HouseId
Optional<uint64> HouseId;
};
// JamNeighborhoodRosterEntry ? sub_7FF6F6E0A460 (48 bytes, used by 0x5C000E, 0x5C000F)
// Wire order: uint64 + PackedGUID + PackedGUID + uint64
struct JamNeighborhoodRosterEntry
{
uint64 Timestamp = 0;
ObjectGuid PlayerGuid;
ObjectGuid HouseGuid;
uint64 ExtraData = 0;
};
// ============================================================
// House Exterior System (0x2Exxxx)
// ============================================================
@@ -590,6 +617,26 @@ namespace WorldPackets::Housing
Optional<ObjectGuid> PlayerGuid;
};
// ============================================================
// Photo Sharing Authorization (0x40019x)
// ============================================================
class HousingPhotoSharingCompleteAuthorization final : public ClientPacket
{
public:
explicit HousingPhotoSharingCompleteAuthorization(WorldPacket&& packet) : ClientPacket(CMSG_HOUSING_PHOTO_SHARING_COMPLETE_AUTHORIZATION, std::move(packet)) {}
void Read() override {}
};
class HousingPhotoSharingClearAuthorization final : public ClientPacket
{
public:
explicit HousingPhotoSharingClearAuthorization(WorldPacket&& packet) : ClientPacket(CMSG_HOUSING_PHOTO_SHARING_CLEAR_AUTHORIZATION, std::move(packet)) {}
void Read() override {}
};
// ============================================================
// Other Housing CMSG
// ============================================================
@@ -1029,16 +1076,9 @@ namespace WorldPackets::Housing
HousingSvcsGetPlayerHousesInfoResponse() : ServerPacket(SMSG_HOUSING_SVCS_GET_PLAYER_HOUSES_INFO_RESPONSE) {}
WorldPacket const* Write() override;
// Wire format (sniff-confirmed, 5 bytes minimum):
// uint32 HouseCount + uint8 Unknown
struct HouseInfoData
{
ObjectGuid HouseGuid;
ObjectGuid NeighborhoodGuid;
uint8 PlotIndex = 0;
uint8 Level = 0;
};
std::vector<HouseInfoData> Houses;
// Wire format (sniff-verified): uint32 Count + uint8 Unknown + JamCurrentHouseInfo per house
// JamCurrentHouseInfo fields mapped to: HouseGuid, PlotGuid, NeighborhoodGuid, PlotIndex, HouseType, StatusFlags+Timestamp
std::vector<JamCurrentHouseInfo> Houses;
uint8 Unknown = 0;
};
@@ -1072,9 +1112,17 @@ namespace WorldPackets::Housing
public:
HousingSvcsUpdateHousesLevelFavor() : ServerPacket(SMSG_HOUSING_SVCS_UPDATE_HOUSES_LEVEL_FAVOR) {}
WorldPacket const* Write() override;
// Sniff-verified (36 bytes): uint8 + 4x int32 + PackedGUID + 2x int32 + uint16
uint8 Type = 0;
int32 PreviousFavor = -1;
int32 PreviousLevel = -1;
int32 NewLevel = 1;
int32 Field4 = 0;
ObjectGuid HouseGuid;
uint32 Level = 0;
uint64 Favor = 0;
int32 PreviousLevelId = -1;
int32 NextLevelFavorCost = -1;
uint16 Flags = 0x8000;
};
class HousingSvcsGuildAddHouseNotification final : public ServerPacket
@@ -1259,15 +1307,12 @@ namespace WorldPackets::Housing
HousingHouseStatusResponse() : ServerPacket(SMSG_HOUSING_HOUSE_STATUS_RESPONSE) {}
WorldPacket const* Write() override;
// Wire format (sniff-confirmed, 30 bytes):
// PackedGUID HouseGUID + PackedGUID OwnerBNetGUID + PackedGUID OwnerPlayerGUID
// + uint16 HouseStatus + uint8 PlotIndex + uint8 StatusFlags
// Wire format (sniff-verified, 0x550000):
// PackedGUID HouseGuid + PackedGUID HouseTemplateGuid + PackedGUID PlotGuid + uint32 Status
ObjectGuid HouseGuid;
ObjectGuid OwnerBNetGuid;
ObjectGuid OwnerPlayerGuid;
uint16 HouseStatus = 0; // 0 = no house, 1 = active
uint8 PlotIndex = 0xFF; // INVALID_PLOT_INDEX
uint8 StatusFlags = 0;
ObjectGuid HouseTemplateGuid;
ObjectGuid PlotGuid;
uint32 Status = 0;
};
class HousingGetCurrentHouseInfoResponse final : public ServerPacket
@@ -1276,16 +1321,10 @@ namespace WorldPackets::Housing
HousingGetCurrentHouseInfoResponse() : ServerPacket(SMSG_HOUSING_GET_CURRENT_HOUSE_INFO_RESPONSE) {}
WorldPacket const* Write() override;
// Wire format (sniff-confirmed, 33 bytes):
// PackedGUID HouseGUID + PackedGUID OwnerPlayerGUID + PackedGUID NeighborhoodGUID
// + uint8 PlotIndex + uint8 HouseProperties + uint8 HouseLevel + uint32 Reserved
ObjectGuid HouseGuid;
ObjectGuid OwnerPlayerGuid;
ObjectGuid NeighborhoodGuid;
uint8 PlotIndex = 0;
uint8 HouseProperties = 0;
uint8 HouseLevel = 0;
uint32 Reserved = 0;
// Wire format (IDA 12.0 verified, 0x550001):
// JamCurrentHouseInfo + uint8 ResponseFlags
JamCurrentHouseInfo HouseInfo;
uint8 ResponseFlags = 0;
};
class HousingExportHouseResponse final : public ServerPacket
@@ -1304,10 +1343,11 @@ namespace WorldPackets::Housing
HousingGetPlayerPermissionsResponse() : ServerPacket(SMSG_HOUSING_GET_PLAYER_PERMISSIONS_RESPONSE) {}
WorldPacket const* Write() override;
// Wire format (sniff-confirmed, 12 bytes):
// PackedGUID HouseGUID + uint16 PermissionFlags
// Wire format (IDA 12.0 verified, 0x550006):
// PackedGUID + uint8 ResultCode + uint8 Permissions(bits 5,6,7)
ObjectGuid HouseGuid;
uint16 PermissionFlags = 0;
uint8 ResultCode = 0;
uint8 PermissionFlags = 0; // bit7=houseEditingPermitted, bit6=plotEntryPermitted, bit5=houseEntryPermitted
};
class HousingResetKioskModeResponse final : public ServerPacket
@@ -1315,7 +1355,7 @@ namespace WorldPackets::Housing
public:
HousingResetKioskModeResponse() : ServerPacket(SMSG_HOUSING_RESET_KIOSK_MODE_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
uint8 Result = 0; // IDA 12.0 verified (0x550007): single uint8
};
// ============================================================
@@ -1397,6 +1437,26 @@ namespace WorldPackets::Housing
WorldPacket const* Write() override;
uint32 Result = 0;
};
// ============================================================
// Photo Sharing SMSG Responses (0x42037x)
// ============================================================
class HousingPhotoSharingAuthorizationResult final : public ServerPacket
{
public:
HousingPhotoSharingAuthorizationResult() : ServerPacket(SMSG_HOUSING_PHOTO_SHARING_AUTHORIZATION_RESULT) {}
WorldPacket const* Write() override;
uint32 Result = 0;
};
class HousingPhotoSharingAuthorizationClearedResult final : public ServerPacket
{
public:
HousingPhotoSharingAuthorizationClearedResult() : ServerPacket(SMSG_HOUSING_PHOTO_SHARING_AUTHORIZATION_CLEARED_RESULT) {}
WorldPacket const* Write() override;
uint32 Result = 0;
};
}
namespace WorldPackets::Neighborhood
@@ -1717,8 +1777,7 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodUpdateNameResponse() : ServerPacket(SMSG_NEIGHBORHOOD_UPDATE_NAME_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
ObjectGuid NeighborhoodGuid;
uint8 Result = 0; // IDA 12.0 verified (0x5C0003): single uint8
};
class NeighborhoodUpdateNameNotification final : public ServerPacket
@@ -1726,7 +1785,7 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodUpdateNameNotification() : ServerPacket(SMSG_NEIGHBORHOOD_UPDATE_NAME_NOTIFICATION) {}
WorldPacket const* Write() override;
ObjectGuid NeighborhoodGuid;
// IDA 12.0 verified (0x5C0004): uint8(nameLen) + bytes[nameLen]
std::string NewName;
};
@@ -1735,9 +1794,9 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodAddSecondaryOwnerResponse() : ServerPacket(SMSG_NEIGHBORHOOD_ADD_SECONDARY_OWNER_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
ObjectGuid NeighborhoodGuid;
// IDA 12.0 verified (0x5C0006): PackedGUID + uint8 Result
ObjectGuid PlayerGuid;
uint8 Result = 0;
};
class NeighborhoodRemoveSecondaryOwnerResponse final : public ServerPacket
@@ -1745,9 +1804,9 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodRemoveSecondaryOwnerResponse() : ServerPacket(SMSG_NEIGHBORHOOD_REMOVE_SECONDARY_OWNER_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
ObjectGuid NeighborhoodGuid;
// IDA 12.0 verified (0x5C0007): PackedGUID + uint8 Result
ObjectGuid PlayerGuid;
uint8 Result = 0;
};
class NeighborhoodBuyHouseResponse final : public ServerPacket
@@ -1755,10 +1814,9 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodBuyHouseResponse() : ServerPacket(SMSG_NEIGHBORHOOD_BUY_HOUSE_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
ObjectGuid HouseGuid;
ObjectGuid NeighborhoodGuid;
uint8 PlotIndex = 0;
// IDA 12.0 verified (0x5C0008): JamCurrentHouseInfo + uint8 Result
Housing::JamCurrentHouseInfo HouseInfo;
uint8 Result = 0;
};
class NeighborhoodMoveHouseResponse final : public ServerPacket
@@ -1766,9 +1824,10 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodMoveHouseResponse() : ServerPacket(SMSG_NEIGHBORHOOD_MOVE_HOUSE_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
ObjectGuid NeighborhoodGuid;
uint8 NewPlotIndex = 0;
// IDA 12.0 verified (0x5C0009): JamCurrentHouseInfo + PackedGUID + uint8 Result
Housing::JamCurrentHouseInfo HouseInfo;
ObjectGuid MoveTransactionGuid;
uint8 Result = 0;
};
class NeighborhoodOpenCornerstoneUIResponse final : public ServerPacket
@@ -1776,12 +1835,22 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodOpenCornerstoneUIResponse() : ServerPacket(SMSG_NEIGHBORHOOD_OPEN_CORNERSTONE_UI_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
ObjectGuid NeighborhoodGuid;
ObjectGuid PlotGuid; // Plot owner GUID (empty for unclaimed "For Sale" plots)
uint64 Cost = 0;
uint8 PlotIndex = 0;
std::string NeighborhoodName; // Neighborhood name displayed in Cornerstone UI
// Wire format verified against retail 12.0.1 build 65940 packet captures
// IDA deserializer sub_7FF6F6E3E200: uint32?+32, GUID?+40, GUID?+56, uint64?+72, uint8?+80, GUID?+128
uint32 PlotIndex = 0; // Echoed from CMSG (NOT a result code)
ObjectGuid PlotOwnerGuid; // ?Buffer+40: Player GUID when owned, Empty when unclaimed
ObjectGuid NeighborhoodGuid; // ?Buffer+56: Housing GUID when owned, Empty when unclaimed
uint64 Cost = 0; // ?Buffer+72: Purchase price (0 if owned or free)
uint8 PurchaseStatus = 0; // ?Buffer+80: 73 (0x49) = purchasable, 0 = not. Client checks ==73
ObjectGuid CornerstoneGuid; // ?Buffer+128: Cornerstone game object GUID
bool IsPlotOwned = false; // Whether this plot has an owner
bool CanPurchase = false; // Whether the player can purchase this plot
bool HasResidents = false; // Whether the plot has residents
bool IsInitiative = false; // Initiative-related flag
Optional<uint64> AlternatePrice; // Alternate/discounted price
Optional<uint32> StatusValue; // Additional status value
std::string NeighborhoodName; // NUL-terminated CString in wire format
};
class NeighborhoodInviteResidentResponse final : public ServerPacket
@@ -1789,8 +1858,8 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodInviteResidentResponse() : ServerPacket(SMSG_NEIGHBORHOOD_INVITE_RESIDENT_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
ObjectGuid NeighborhoodGuid;
// IDA 12.0 verified (0x5C000B): uint8 Result + PackedGUID
uint8 Result = 0;
ObjectGuid InviteeGuid;
};
@@ -1799,8 +1868,8 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodCancelInvitationResponse() : ServerPacket(SMSG_NEIGHBORHOOD_CANCEL_INVITATION_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
ObjectGuid NeighborhoodGuid;
// IDA 12.0 verified (0x5C000C): uint8 Result + PackedGUID
uint8 Result = 0;
ObjectGuid InviteeGuid;
};
@@ -1809,7 +1878,8 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodDeclineInvitationResponse() : ServerPacket(SMSG_NEIGHBORHOOD_DECLINE_INVITATION_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
// IDA 12.0 verified (0x5C000D): uint8 Result + PackedGUID
uint8 Result = 0;
ObjectGuid NeighborhoodGuid;
};
@@ -1818,11 +1888,9 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodPlayerGetInviteResponse() : ServerPacket(SMSG_NEIGHBORHOOD_PLAYER_GET_INVITE_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
ObjectGuid NeighborhoodGuid;
ObjectGuid InviterGuid;
std::string NeighborhoodName;
uint32 InviteTime = 0;
// IDA 12.0 verified (0x5C000E): uint8 Result + JamNeighborhoodRosterEntry(48 bytes)
uint8 Result = 0;
Housing::JamNeighborhoodRosterEntry Entry;
};
class NeighborhoodGetInvitesResponse final : public ServerPacket
@@ -1830,15 +1898,9 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodGetInvitesResponse() : ServerPacket(SMSG_NEIGHBORHOOD_GET_INVITES_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
struct InviteData
{
ObjectGuid InviteeGuid;
ObjectGuid InviterGuid;
uint32 InviteTime = 0;
};
std::vector<InviteData> Invites;
// IDA 12.0 verified (0x5C000F): uint8 Result + uint32 Count + JamNeighborhoodRosterEntry[Count]
uint8 Result = 0;
std::vector<Housing::JamNeighborhoodRosterEntry> Invites;
};
class NeighborhoodInviteNotification final : public ServerPacket
@@ -1846,9 +1908,8 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodInviteNotification() : ServerPacket(SMSG_NEIGHBORHOOD_INVITE_NOTIFICATION) {}
WorldPacket const* Write() override;
// IDA 12.0 verified (0x5C0010): single PackedGUID
ObjectGuid NeighborhoodGuid;
ObjectGuid InviterGuid;
std::string NeighborhoodName;
};
class NeighborhoodOfferOwnershipResponse final : public ServerPacket
@@ -1856,9 +1917,8 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodOfferOwnershipResponse() : ServerPacket(SMSG_NEIGHBORHOOD_OFFER_OWNERSHIP_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
ObjectGuid NeighborhoodGuid;
ObjectGuid NewOwnerGuid;
// IDA 12.0 verified (0x5C0011): single uint8 Result
uint8 Result = 0;
};
class NeighborhoodGetRosterResponse final : public ServerPacket
@@ -1875,6 +1935,7 @@ namespace WorldPackets::Neighborhood
ObjectGuid BnetAccountGuid; // Usually empty
uint8 PlotIndex = 0xFF; // INVALID_PLOT_INDEX
uint32 JoinTime = 0;
bool IsOnline = false; // Controls status byte 2 bit 7 in roster UI
};
std::vector<RosterMemberData> Members;
@@ -1904,8 +1965,8 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodInviteNameLookupResult() : ServerPacket(SMSG_NEIGHBORHOOD_INVITE_NAME_LOOKUP_RESULT) {}
WorldPacket const* Write() override;
uint32 Result = 0;
std::string PlayerName;
// IDA 12.0 verified (0x5C0014): uint8 Result + PackedGUID
uint8 Result = 0;
ObjectGuid PlayerGuid;
};
@@ -1914,9 +1975,9 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodEvictPlotResponse() : ServerPacket(SMSG_NEIGHBORHOOD_EVICT_PLOT_RESPONSE) {}
WorldPacket const* Write() override;
uint32 Result = 0;
// IDA 12.0 verified (0x5C0015): uint8 Result + PackedGUID
uint8 Result = 0;
ObjectGuid NeighborhoodGuid;
ObjectGuid PlotGuid;
};
class NeighborhoodEvictPlotNotice final : public ServerPacket
@@ -1924,6 +1985,8 @@ namespace WorldPackets::Neighborhood
public:
NeighborhoodEvictPlotNotice() : ServerPacket(SMSG_NEIGHBORHOOD_EVICT_PLOT_NOTICE) {}
WorldPacket const* Write() override;
// IDA 12.0 verified (0x5C0016): uint32 + PackedGUID + PackedGUID
uint32 PlotId = 0;
ObjectGuid NeighborhoodGuid;
ObjectGuid PlotGuid;
};
+2 -2
View File
@@ -584,8 +584,8 @@ void OpcodeTable::InitializeClientOpcodes()
DEFINE_HANDLER(CMSG_HOUSING_GET_CURRENT_HOUSE_INFO, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleHousingGetCurrentHouseInfo);
DEFINE_HANDLER(CMSG_HOUSING_GET_PLAYER_PERMISSIONS, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleHousingGetPlayerPermissions);
DEFINE_HANDLER(CMSG_HOUSING_HOUSE_STATUS, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleHousingHouseStatus);
DEFINE_HANDLER(CMSG_HOUSING_PHOTO_SHARING_CLEAR_AUTHORIZATION, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_HOUSING_PHOTO_SHARING_COMPLETE_AUTHORIZATION, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_HOUSING_PHOTO_SHARING_CLEAR_AUTHORIZATION, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleHousingPhotoSharingClearAuthorization);
DEFINE_HANDLER(CMSG_HOUSING_PHOTO_SHARING_COMPLETE_AUTHORIZATION, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleHousingPhotoSharingCompleteAuthorization);
DEFINE_HANDLER(CMSG_HOUSING_RESET_KIOSK_MODE, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleHousingResetKioskMode);
DEFINE_HANDLER(CMSG_HOUSING_ROOM_ADD, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleHousingRoomAdd);
DEFINE_HANDLER(CMSG_HOUSING_ROOM_APPLY_COMPONENT_MATERIALS, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleHousingRoomApplyComponentMaterials);
+6
View File
@@ -487,6 +487,8 @@ namespace WorldPackets
class InvitePlayerToNeighborhood;
class GuildGetOthersOwnedHouses;
class HouseExteriorLock;
class HousingPhotoSharingCompleteAuthorization;
class HousingPhotoSharingClearAuthorization;
class HousingFixtureSetHouseSize;
class HousingFixtureSetHouseType;
}
@@ -1621,6 +1623,10 @@ class TC_GAME_API WorldSession
void HandleHousingSvcsGetBnetFriendNeighborhoods(WorldPackets::Housing::HousingSvcsGetBnetFriendNeighborhoods const& housingSvcsGetBnetFriendNeighborhoods);
void HandleHousingSvcsDeleteAllNeighborhoodInvites(WorldPackets::Housing::HousingSvcsDeleteAllNeighborhoodInvites const& housingSvcsDeleteAllNeighborhoodInvites);
// Housing - Photo Sharing
void HandleHousingPhotoSharingCompleteAuthorization(WorldPackets::Housing::HousingPhotoSharingCompleteAuthorization const& packet);
void HandleHousingPhotoSharingClearAuthorization(WorldPackets::Housing::HousingPhotoSharingClearAuthorization const& packet);
// Housing - Misc
void HandleHousingHouseStatus(WorldPackets::Housing::HousingHouseStatus const& housingHouseStatus);
void HandleHousingGetCurrentHouseInfo(WorldPackets::Housing::HousingGetCurrentHouseInfo const& housingGetCurrentHouseInfo);
@@ -0,0 +1,60 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ScriptMgr.h"
#include "GameObject.h"
#include "Log.h"
#include "Player.h"
#include "SpellScript.h"
enum HousingCornerstoneSpells
{
SPELL_TRIGGER_CONVO_UNOWNED_PLOT = 1266097
};
// 1266097 - [DNT] Trigger Convo for Unowned Plot
// Cast by Cornerstone GO (entry 457142, type UILink) when a player clicks it.
// The SMSG_NPC_INTERACTION_OPEN_RESULT with CornerstoneInteraction (type 70) is
// already sent by the UILink Use() handler before this spell fires.
// This dummy effect provides server-side validation and logging.
class spell_housing_trigger_convo_unowned_plot : public SpellScript
{
bool Validate(SpellInfo const* /*spellInfo*/) override
{
return true;
}
void HandleDummy(SpellEffIndex /*effIndex*/) const
{
Player* caster = GetCaster()->ToPlayer();
if (!caster)
return;
TC_LOG_DEBUG("housing", "spell_housing_trigger_convo_unowned_plot: Spell {} fired for player {} ({})",
GetSpellInfo()->Id, caster->GetName(), caster->GetGUID().ToString());
}
void Register() override
{
OnEffectHit += SpellEffectFn(spell_housing_trigger_convo_unowned_plot::HandleDummy, EFFECT_0, SPELL_EFFECT_DUMMY);
}
};
void AddSC_housing_spell_scripts()
{
RegisterSpellScript(spell_housing_trigger_convo_unowned_plot);
}
@@ -33,6 +33,7 @@ void AddSC_warrior_spell_scripts();
void AddSC_quest_spell_scripts();
void AddSC_item_spell_scripts();
void AddSC_azerite_item_spell_scripts();
void AddSC_housing_spell_scripts();
// The name of this function should match:
// void Add${NameOfDirectory}Scripts()
@@ -55,4 +56,5 @@ void AddSpellsScripts()
AddSC_quest_spell_scripts();
AddSC_item_spell_scripts();
AddSC_azerite_item_spell_scripts();
AddSC_housing_spell_scripts();
}
@@ -25,27 +25,30 @@
enum HousingTutorialData
{
// Quest: "My First Home" (91863) kill credit NPCs
NPC_KILL_CREDIT_GREET_STEWARD = 249851,
NPC_KILL_CREDIT_ASK_STEWARD = 248857,
NPC_KILL_CREDIT_GREET_STEWARD = 249851,
NPC_KILL_CREDIT_ASK_STEWARD = 248857,
// Gossip actions
GOSSIP_ACTION_ASK_TO_JOIN = 1001,
GOSSIP_ACTION_ASK_TO_JOIN = 1001,
};
// Lyssabel Dawnpetal (233063) / Tocho (233708) — Housing tutorial steward NPCs.
// Lyssabel Dawnpetal (233063) / Tocho (233708) ? Housing tutorial steward NPCs.
// When the player interacts with the steward during the "My First Home" quest (91863),
// the gossip grants quest kill credits for greeting the steward and asking them to join.
struct npc_housing_steward : public CreatureAI
{
npc_housing_steward(Creature* creature) : CreatureAI(creature) { }
npc_housing_steward(Creature* creature) : CreatureAI(creature) {}
void UpdateAI(uint32 /*diff*/) override { }
void UpdateAI(uint32 /*diff*/) override {}
bool OnGossipHello(Player* player) override
{
// Grant "Greet the steward" kill credit (quest objective 0)
// Grant "Greet the steward" kill credit (quest objective 0: MONSTER 249851)
player->KilledMonsterCredit(NPC_KILL_CREDIT_GREET_STEWARD);
// Satisfy "Talk to Lyssabel/Tocho" objective (quest objective 1/2: TALKTO with NPC entry)
player->TalkedToCreature(me->GetEntry(), me->GetGUID());
// Show gossip menu with option to ask the steward to join the neighborhood
InitGossipMenuFor(player, 0);
if (me->IsQuestGiver())
@@ -55,8 +58,8 @@ struct npc_housing_steward : public CreatureAI
GOSSIP_SENDER_MAIN, GOSSIP_ACTION_ASK_TO_JOIN);
SendGossipMenuFor(player, DEFAULT_GOSSIP_MESSAGE, me->GetGUID());
TC_LOG_DEBUG("housing", "npc_housing_steward: Player {} greeted steward {} (kill credit {})",
player->GetGUID().ToString(), me->GetEntry(), NPC_KILL_CREDIT_GREET_STEWARD);
TC_LOG_DEBUG("housing", "npc_housing_steward: Player {} greeted steward {} (kill credit {}, talkto {})",
player->GetGUID().ToString(), me->GetEntry(), NPC_KILL_CREDIT_GREET_STEWARD, me->GetEntry());
return true;
}