Housing: Fix initiative packet parsing, opcode collisions, and tutorial CVar timing

- Fix CMSG_INITIATIVE_REPORT_PROGRESS ByteBufferException: packet only
  contains a packed NeighborhoodGuid (sniff-verified 7 bytes), not three
  extra uint32 fields. Handler now just sends initiative info back.

- Fix 4 SMSG initiative opcode collisions (0x420369-0x42036C) with
  existing opcodes (CATALOG_SHOP_OBTAIN_LICENSE, MIRROR_VARS,
  SET_INSTANCE_LEAVER, UNSET_INSTANCE_LEAVER). Reassigned to
  0x420380-0x420383.

- Fix tutorial mode still blocking editor: CVar injection in
  Player::LoadFromDB happened after the client already fetched account
  data during auth. Added SendAccountDataTimes() after SetAccountData()
  to force the client to re-fetch the updated GLOBAL_CONFIG_CACHE with
  housingTutorialsEnabled=0 and closedInfoFramesAccountWide bits.

- Reset FHousingStorage_C populated flag on every edit mode entry so
  the Account VALUES_UPDATE always carries the full storage map.

- Remove FHousingDecorActor_C fragment from decor MeshObjects (sniff
  analysis confirmed fragment 28 is not present on any retail entity).

- Use DROP+CREATE instead of CREATE IF NOT EXISTS in initiative SQL
  to prevent stale schema from persisting silently.
This commit is contained in:
luis
2026-03-10 19:54:41 -03:00
parent 1d444ee8f7
commit b299277770
8 changed files with 33 additions and 39 deletions
@@ -181,13 +181,18 @@ void MeshObject::InitHousingDecorData(ObjectGuid decorGuid, ObjectGuid houseGuid
m_entityFragments.Add(WowCS::EntityFragment::FHousingDecor_C, IsInWorld(),
WowCS::GetRawFragmentData(m_housingDecorData));
// Retail sniff-verified: decor MeshObjects have exactly these fragments:
// [CGObject(2), FMeshObjectData_C(19), FHousingDecor_C(20),
// FMirroredPositionData_C(31), Tag_MeshObject(221)]
// FHousingDecorActor_C (28) is NOT present on any retail entity.
// Retail sniff: HasDecor movement block flag is NEVER set (always False).
// The room entity GUID is already in the FHousingDecor_C fragment's AttachParentGUID field.
// Do NOT set m_updateFlag.Decor here — it adds an extra movement block field
// Do NOT set m_updateFlag.Decor here ? it adds an extra movement block field
// that the client does not expect.
_decorRoomEntityGUID = roomEntityGuid;
TC_LOG_DEBUG("housing", "MeshObject::InitHousingDecorData: guid={} decorGuid={} houseGuid={} flags={} roomEntity={} (Decor flag=ON)",
TC_LOG_DEBUG("housing", "MeshObject::InitHousingDecorData: guid={} decorGuid={} houseGuid={} flags={} roomEntity={} (FHousingDecor_C ON)",
GetGUID().ToString(), decorGuid.ToString(), houseGuid.ToString(), flags, roomEntityGuid.ToString());
}
@@ -256,7 +261,7 @@ void MeshObject::InitHousingRoomData(ObjectGuid houseGuid, int32 houseRoomID,
// Retail sniff: HasRoom movement block flag is NEVER set (always False).
// The house GUID is already in the FHousingRoom_C fragment data.
// Do NOT set m_updateFlag.Room here — it adds an extra movement block field
// Do NOT set m_updateFlag.Room here ? it adds an extra movement block field
// that the client does not expect.
_roomHouseGUID = houseGuid;
@@ -295,7 +300,7 @@ void MeshObject::InitHousingRoomComponentData(ObjectGuid roomGuid,
}
// Set Geobox (axis-aligned bounding box) on MeshObjectData.
// Sniff-verified: ALL retail room component meshes have Geobox (-35,-30,-1.01)→(35,30,125.01).
// Sniff-verified: ALL retail room component meshes have Geobox (-35,-30,-1.01)?(35,30,125.01).
// The client uses this box for its OutsidePlotBounds collision check.
{
auto meshData = m_values.ModifyValue(&MeshObject::m_meshObjectData);
@@ -323,7 +328,7 @@ void MeshObject::InitHousingRoomComponentData(ObjectGuid roomGuid,
TC_LOG_ERROR("housing", "MeshObject::InitHousingRoomComponentData: guid={} roomGuid={} "
"compOptionID={} compID={} compType={} themeID={} "
"geobox=({:.2f},{:.2f},{:.2f})→({:.2f},{:.2f},{:.2f})",
"geobox=({:.2f},{:.2f},{:.2f})?({:.2f},{:.2f},{:.2f})",
GetGUID().ToString(), roomGuid.ToString(),
roomComponentOptionID, roomComponentID, roomComponentType, houseThemeID,
geoboxMinX, geoboxMinY, geoboxMinZ, geoboxMaxX, geoboxMaxY, geoboxMaxZ);
@@ -18925,6 +18925,10 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
if (configModified)
{
GetSession()->SetAccountData(GLOBAL_CONFIG_CACHE, GameTime::GetGameTime(), configData);
// Re-send account data timestamps so the client detects the newer timestamp
// and re-fetches GLOBAL_CONFIG_CACHE. Without this, the client uses the stale
// data it fetched during auth (before LoadFromDB modified it).
GetSession()->SendAccountDataTimes(GetGUID(), GLOBAL_CACHE_MASK);
TC_LOG_DEBUG("housing", "Player::LoadFromDB: Injected housing tutorial CVars into GLOBAL_CONFIG_CACHE for account {}",
GetSession()->GetAccountId());
}
+5 -1
View File
@@ -496,10 +496,14 @@ void WorldSession::HandleHousingDecorSetEditMode(WorldPackets::Housing::HousingD
player->SetUnitFlag2(UNIT_FLAG2_NO_ACTIONS);
player->ReplaceAllSilencedSchoolMask(SPELL_SCHOOL_MASK_ALL);
// 4. Populate FHousingStorage_C on the Account entity if not done yet.
// 4. Populate FHousingStorage_C on the Account entity.
// The client correlates MeshObject FHousingDecor_C.DecorGUID with entries in
// FHousingStorage_C to build its placed decor list for the targeting system.
// Without this, the client has no decor to target and selection is impossible.
// Reset the populated flag so storage entries are re-pushed on every edit mode
// entry ? the client may clear its decor list when exiting editor mode, so we
// must ensure the Account VALUES_UPDATE always carries the full storage map.
housing->ResetStoragePopulated();
housing->PopulateCatalogStorageEntries();
// 4b. Refresh budget values on the HousingPlayerHouseEntity so the client
@@ -2016,8 +2016,8 @@ void WorldSession::HandleInitiativeReportProgress(WorldPackets::Neighborhood::In
if (!player)
return;
TC_LOG_DEBUG("housing", "CMSG_INITIATIVE_REPORT_PROGRESS NeighborhoodGuid: {} InitiativeID: {} TaskID: {} Delta: {} Player: {}",
packet.NeighborhoodGuid.ToString(), packet.InitiativeID, packet.TaskID, packet.ProgressDelta, player->GetGUID().ToString());
TC_LOG_DEBUG("housing", "CMSG_INITIATIVE_REPORT_PROGRESS NeighborhoodGuid: {} Player: {}",
packet.NeighborhoodGuid.ToString(), player->GetGUID().ToString());
Neighborhood* neighborhood = sNeighborhoodMgr.ResolveNeighborhood(packet.NeighborhoodGuid, player);
if (!neighborhood)
@@ -2030,20 +2030,7 @@ void WorldSession::HandleInitiativeReportProgress(WorldPackets::Neighborhood::In
uint64 nhGuid = neighborhood->GetGuid().GetCounter();
// Validate that the initiative and task exist
ActiveInitiative* active = sInitiativeManager.GetActiveInitiative(nhGuid);
if (!active || active->InitiativeID != packet.InitiativeID)
{
WorldPackets::Housing::GetPlayerInitiativeInfoResult response;
response.Result = static_cast<uint8>(HOUSING_RESULT_GENERIC_FAILURE);
SendPacket(response.Write());
return;
}
// Apply progress and send updated task info
if (packet.ProgressDelta > 0)
sInitiativeManager.UpdateTaskProgress(nhGuid, packet.InitiativeID, packet.TaskID, packet.ProgressDelta, player);
// Client is requesting initiative info for this neighborhood ? send current state
sInitiativeManager.SendPlayerInitiativeInfo(this, nhGuid);
}
+5 -4
View File
@@ -87,7 +87,7 @@ public:
explicit Housing(Player* owner);
// Global DB ID generators — must be called once during server startup
// Global DB ID generators ? must be called once during server startup
// before any Housing objects are loaded, to prevent cross-player ID collisions.
static void InitializeDbIdGenerators();
@@ -121,7 +121,7 @@ public:
void SetInInterior(bool interior) { _isInInterior = interior; }
bool IsInInterior() const { return _isInInterior; }
// Decor operations — StartPlacingNewDecor creates a pending placement, PlaceDecorWithGuid commits it
// Decor operations ? StartPlacingNewDecor creates a pending placement, PlaceDecorWithGuid commits it
ObjectGuid StartPlacingNewDecor(uint32 catalogEntryId, HousingResult& result);
uint32 GetPendingPlacementEntryId(ObjectGuid decorGuid) const;
void CancelPendingPlacement(ObjectGuid decorGuid);
@@ -221,9 +221,10 @@ public:
// Direct access to placed decor map (for GO spawning)
std::unordered_map<ObjectGuid, PlacedDecor> const& GetPlacedDecorMap() const { return _placedDecor; }
bool IsStoragePopulated() const { return _storagePopulated; }
void ResetStoragePopulated() { _storagePopulated = false; }
// Populate ALL decor entries (placed + catalog) into the Account entity's FHousingStorage_C.
// Called on-demand by REQUEST_STORAGE handler. Retail does NOT populate storage at login —
// Called on-demand by REQUEST_STORAGE handler. Retail does NOT populate storage at login ?
// FHousingStorage_C is only sent when the player enters edit mode or requests storage.
void PopulateCatalogStorageEntries();
@@ -263,7 +264,7 @@ private:
float _houseFacing = 0.0f;
ObjectGuid _cosmeticOwnerGuid; // Display owner for guild housing
bool _hasCustomPosition = false;
bool _storagePopulated = false; // True after PopulateCatalogStorageEntries() — gates Account entity updates
bool _storagePopulated = false; // True after PopulateCatalogStorageEntries() ? gates Account entity updates
bool _photoSharingAuthorized = false; // Per-session photo sharing authorization state
// WeightCost-based budget tracking
@@ -2781,12 +2781,8 @@ namespace WorldPackets::Neighborhood
void InitiativeReportProgress::Read()
{
_worldPacket >> NeighborhoodGuid;
_worldPacket >> InitiativeID;
_worldPacket >> TaskID;
_worldPacket >> ProgressDelta;
TC_LOG_DEBUG("network.opcode", "CMSG_INITIATIVE_REPORT_PROGRESS NeighborhoodGuid: {} InitiativeID: {} TaskID: {} ProgressDelta: {}",
NeighborhoodGuid.ToString(), InitiativeID, TaskID, ProgressDelta);
TC_LOG_DEBUG("network.opcode", "CMSG_INITIATIVE_REPORT_PROGRESS NeighborhoodGuid: {}",
NeighborhoodGuid.ToString());
}
void GetInitiativeClaimRewardRequest::Read()
@@ -2864,9 +2864,6 @@ namespace WorldPackets::Neighborhood
InitiativeReportProgress(WorldPacket&& packet) : ClientPacket(CMSG_INITIATIVE_REPORT_PROGRESS, std::move(packet)) {}
void Read() override;
ObjectGuid NeighborhoodGuid;
uint32 InitiativeID = 0;
uint32 TaskID = 0;
uint32 ProgressDelta = 0;
};
class GetInitiativeClaimRewardRequest final : public ClientPacket
+4 -4
View File
@@ -1776,14 +1776,14 @@ enum OpcodeServer : uint32
SMSG_HOUSING_SVC_REQUEST_PLAYER_RELOAD_DATA = 0x540020,
SMSG_INITIALIZE_FACTIONS = 0x4201CC,
SMSG_INITIAL_SETUP = 0x420014,
SMSG_INITIATIVE_CHEST_RESULT = 0x420369,
SMSG_INITIATIVE_CHEST_RESULT = 0x420380,
SMSG_INITIATIVE_COMPLETE = 0x420363,
SMSG_INITIATIVE_MILESTONE_UPDATE = 0x42036A,
SMSG_INITIATIVE_POINTS_UPDATE = 0x42036B,
SMSG_INITIATIVE_MILESTONE_UPDATE = 0x420381,
SMSG_INITIATIVE_POINTS_UPDATE = 0x420382,
SMSG_INITIATIVE_REWARD_AVAILABLE = 0x420368,
SMSG_INITIATIVE_SERVICE_STATUS = 0x420361,
SMSG_INITIATIVE_TASK_COMPLETE = 0x420362,
SMSG_INITIATIVE_TRACKED_UPDATED = 0x42036C,
SMSG_INITIATIVE_TRACKED_UPDATED = 0x420383,
SMSG_INITIATIVE_UPDATE_STATUS = 0x42036D,
SMSG_INIT_WORLD_STATES = 0x4201EE,
SMSG_INSPECT_RESULT = 0x4200D9,