Housing: Fix entity GUID mismatch, initiative wire format, and packet error fields

Fix Housing/4 NeighborhoodMirrorEntity GUID using battlenetAccountId instead
of the neighborhood's actual DB low GUID. The client matches entity GUIDs
against NeighborhoodGUID references in JamCliHouse packets — a mismatch
causes the client to fail to associate plot data with the correct entity.
Added ResetGuid() to correct the GUID in Player::LoadFromDB before the
entity is added to the world.

Fix initiative SMSG_GET_PLAYER_INITIATIVE_INFO_RESULT: duration now converts
DB2 days to seconds (×86400), progress scales from 0.0–1.0 to 0–1000 wire
format, and the packet is sent proactively on service status check so the
client's isLoaded flag gets set.

Revert incorrect HasError replacements on 14+ non-initiative response packets
back to their proper Result field assignments. Remove fake initiative SQL
hotfix data that overwrote real DB2 records.
This commit is contained in:
luis
2026-03-12 07:49:28 -03:00
parent be453341aa
commit 0cb961d47d
23 changed files with 10737 additions and 10452 deletions
@@ -0,0 +1,11 @@
DROP TABLE IF EXISTS `initiative_cycle`;
CREATE TABLE `initiative_cycle` (
`ID` int unsigned NOT NULL DEFAULT '0',
`RewardGroupID` int NOT NULL DEFAULT '0',
`CycleIndex` int NOT NULL DEFAULT '0',
`StartDay` int NOT NULL DEFAULT '0',
`Duration` int NOT NULL DEFAULT '0',
`InitiativeID` int NOT NULL DEFAULT '0',
`VerifiedBuild` int NOT NULL DEFAULT '0',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -2393,7 +2393,7 @@ void HotfixDatabaseConnection::DoPrepareStatements()
// InitiativeCycle.db2
PrepareStatement(HOTFIX_SEL_INITIATIVE_CYCLE, "SELECT ID, InitiativeID, CycleIndex, StartDay, Duration, Flags FROM initiative_cycle WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
PrepareStatement(HOTFIX_SEL_INITIATIVE_CYCLE, "SELECT ID, RewardGroupID, CycleIndex, StartDay, Duration, InitiativeID FROM initiative_cycle WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
PREPARE_MAX_ID_STMT(HOTFIX_SEL_INITIATIVE_CYCLE, "SELECT MAX(ID) + 1 FROM initiative_cycle", CONNECTION_SYNCH);
// InitiativeCyclePriority.db2
+2 -2
View File
@@ -7825,11 +7825,11 @@ struct HouseDecorLoadInfo
static constexpr DB2FieldMeta Fields[6] =
{
{.IsSigned = false, .Type = FT_INT, .Name = "ID" },
{.IsSigned = true, .Type = FT_INT, .Name = "InitiativeID" },
{.IsSigned = true, .Type = FT_INT, .Name = "RewardGroupID" },
{.IsSigned = true, .Type = FT_INT, .Name = "CycleIndex" },
{.IsSigned = true, .Type = FT_INT, .Name = "StartDay" },
{.IsSigned = true, .Type = FT_INT, .Name = "Duration" },
{.IsSigned = true, .Type = FT_INT, .Name = "Flags" },
{.IsSigned = true, .Type = FT_INT, .Name = "InitiativeID" },
};
static constexpr DB2LoadInfo Instance{ Fields, 6, &InitiativeCycleMeta::Instance, HOTFIX_SEL_INITIATIVE_CYCLE };
+2 -2
View File
@@ -5527,11 +5527,11 @@ struct ExteriorComponentXGroupEntry
struct InitiativeCycleEntry
{
uint32 ID;
int32 InitiativeID;
int32 RewardGroupID; // Unknown FK (values 600-2607), not NeighborhoodInitiative
int32 CycleIndex;
int32 StartDay;
int32 Duration;
int32 Flags;
int32 InitiativeID; // FK -> NeighborhoodInitiative.ID (was mislabeled "Flags")
};
struct InitiativeCyclePriorityEntry
+1 -1
View File
@@ -97,7 +97,7 @@ namespace Battlenet
// (e.g., FHousingStorage_C populated by PopulateCatalogStorageEntries) are included.
BuildUpdateChangesMask();
BaseEntity::SendUpdateToPlayer(player);
ClearUpdateMask(false);
ClearUpdateMask(true);
}
void Account::SetHousingDecorStorageEntry(ObjectGuid decorGuid, ObjectGuid houseGuid, uint8 sourceType, std::string sourceValue)
@@ -81,7 +81,7 @@ void HousingNeighborhoodMirrorEntity::SendUpdateToPlayer(Player* player)
{
BuildUpdateChangesMask();
BaseEntity::SendUpdateToPlayer(player);
ClearUpdateMask(false);
ClearUpdateMask(true);
}
void HousingNeighborhoodMirrorEntity::SetName(std::string const& name)
@@ -36,6 +36,9 @@ public:
void SendUpdateToPlayer(Player* player);
// Reset the entity GUID (must be called before AddToWorld)
void ResetGuid(ObjectGuid newGuid) { _Create(newGuid); }
// Neighborhood mirror setters
void SetName(std::string const& name);
void SetOwnerGUID(ObjectGuid ownerGuid);
@@ -81,17 +81,7 @@ void HousingPlayerHouseEntity::SendUpdateToPlayer(Player* player)
{
BuildUpdateChangesMask();
BaseEntity::SendUpdateToPlayer(player);
ClearUpdateMask(false);
}
void HousingPlayerHouseEntity::SetHouseType(uint32 houseType)
{
SetUpdateFieldValue(m_values.ModifyValue(&HousingPlayerHouseEntity::m_housingPlayerHouseData).ModifyValue(&UF::HousingPlayerHouseData::HouseType), houseType);
}
void HousingPlayerHouseEntity::SetHouseSize(uint32 houseSize)
{
SetUpdateFieldValue(m_values.ModifyValue(&HousingPlayerHouseEntity::m_housingPlayerHouseData).ModifyValue(&UF::HousingPlayerHouseData::HouseSize), houseSize);
ClearUpdateMask(true);
}
void HousingPlayerHouseEntity::SetPlotIndex(int32 plotIndex)
@@ -36,9 +36,7 @@ public:
void SendUpdateToPlayer(Player* player);
// Housing UpdateField setters
void SetHouseType(uint32 houseType);
void SetHouseSize(uint32 houseSize);
// Housing UpdateField setters (IDA-verified: HouseType/HouseSize not in this fragment)
void SetPlotIndex(int32 plotIndex);
void SetLevel(uint32 level);
void SetFavor(uint64 favor);
File diff suppressed because it is too large Load Diff
@@ -1946,19 +1946,22 @@ struct HousingRoomComponentMeshData : public IsUpdateFieldStructureTag, public H
void ClearChangesMask();
};
struct HousingPlayerHouseData : public IsUpdateFieldStructureTag, public HasChangesMask<12>
struct HousingPlayerHouseData : public IsUpdateFieldStructureTag, public HasChangesMask<10>
{
// IDA-verified wire order: must match client struct layout (72 bytes, 8 internal + 64 wire).
// Client struct offsets: +8=BnetAccount, +24=PlotIndex(def -1), +28=Level(def 1),
// +32=Favor, +40=Interior, +44=Exterior, +48=Room, +52=Fixture, +56=EntityGUID.
// HOUSE_LEVEL_CHANGED event pushes: level, interior, exterior, room, fixture.
// HouseType and HouseSize do NOT exist in this fragment.
UpdateField<ObjectGuid, 0, 1> BnetAccount;
UpdateField<uint32, 0, 2> HouseType;
UpdateField<uint32, 0, 3> HouseSize;
UpdateField<int32, 0, 4> PlotIndex;
UpdateField<uint32, 0, 5> Level;
UpdateField<uint64, 0, 6> Favor;
UpdateField<uint32, 0, 7> InteriorDecorPlacementBudget;
UpdateField<uint32, 0, 8> ExteriorDecorPlacementBudget;
UpdateField<uint32, 0, 9> ExteriorFixtureBudget;
UpdateField<uint32, 0, 10> RoomPlacementBudget;
UpdateField<ObjectGuid, 0, 11> EntityGUID;
UpdateField<int32, 0, 2> PlotIndex;
UpdateField<uint32, 0, 3> Level;
UpdateField<uint64, 0, 4> Favor;
UpdateField<uint32, 0, 5> InteriorDecorPlacementBudget;
UpdateField<uint32, 0, 6> ExteriorDecorPlacementBudget;
UpdateField<uint32, 0, 7> RoomPlacementBudget;
UpdateField<uint32, 0, 8> ExteriorFixtureBudget;
UpdateField<ObjectGuid, 0, 9> EntityGUID;
using OwnerObject = BaseEntity;
void WriteCreate(EnumFlag<UpdateFieldFlag> fieldVisibilityFlags, ByteBuffer& data, Player const* receiver, BaseEntity const* owner) const;
+92 -6
View File
@@ -19018,12 +19018,31 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
NeighborhoodInitiativeEntry const* initEntry = sNeighborhoodInitiativeStore.LookupEntry(activeInit->InitiativeID);
uint32 cycleID = sInitiativeManager.GetActiveCycleForInitiative(activeInit->InitiativeID);
// Calculate remaining duration from start time + DB2 duration
int64 remainingDuration = 0;
// Calculate remaining duration from start time + DB2 duration.
// Check both NeighborhoodInitiative.Duration and InitiativeCycle.Duration.
// If neither provides a duration, use a 7-day default so the client shows
// the endeavor as active rather than expired (Duration=0 ? hidden).
// Duration from DB2 is in days ? convert to seconds.
// Sniff-verified: RemainingDuration is in seconds (sniff value 972957 ? 11.25 days).
int64 durationSec = 0;
if (initEntry && initEntry->Duration > 0)
durationSec = static_cast<int64>(initEntry->Duration) * 86400;
else if (cycleID)
{
int64 elapsed = static_cast<int64>(GameTime::GetGameTime()) - static_cast<int64>(activeInit->StartTime);
remainingDuration = std::max<int64>(0, static_cast<int64>(initEntry->Duration) - elapsed);
InitiativeCycleEntry const* cycleEntry = sInitiativeCycleStore.LookupEntry(cycleID);
if (cycleEntry && cycleEntry->Duration > 0)
durationSec = static_cast<int64>(cycleEntry->Duration) * 86400;
}
if (durationSec <= 0)
durationSec = 7 * DAY; // 7-day fallback
int64 elapsed = static_cast<int64>(GameTime::GetGameTime()) - static_cast<int64>(activeInit->StartTime);
int64 remainingDuration = durationSec - elapsed;
// If expired, reset start time so the initiative stays active
if (remainingDuration <= 0)
{
activeInit->StartTime = static_cast<uint32>(GameTime::GetGameTime());
remainingDuration = durationSec;
}
// Calculate progress in the 0-1000 scale (sniff: ProgressRequired=1000)
@@ -19082,6 +19101,75 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
}
}
// Pre-populate Housing/4 (NeighborhoodMirrorEntity) and Housing/3 (HousingPlayerHouseEntity)
// BEFORE BuildCreateUpdateBlockForPlayer runs. The CREATE block must include the full Houses
// array so the client sees occupied plots at the correct indices. If we only populate these
// during SendInitialPacketsAfterAddToMap (after CREATE), the client receives an empty Houses
// array in CREATE and a DynamicUpdateField UPDATE that grows the array ? causing it to map
// houses to indices 0,1,2 instead of their real PlotIndex values (e.g. 7,9,47,51).
if (GetSession() && !_housings.empty() && _housings[0] && !_housings[0]->GetNeighborhoodGuid().IsEmpty())
{
Neighborhood const* neighborhood = sNeighborhoodMgr.GetNeighborhood(_housings[0]->GetNeighborhoodGuid());
if (neighborhood)
{
// --- Housing/4: NeighborhoodMirrorEntity ---
// The entity GUID must match the neighborhood's actual GUID so the client
// can associate it with NeighborhoodGUID references in JamCliHouse packets.
// WorldSession creates it with battlenetAccountId as placeholder; fix it here.
HousingNeighborhoodMirrorEntity& mirrorEntity = GetSession()->GetHousingNeighborhoodMirrorEntity();
mirrorEntity.ResetGuid(neighborhood->GetGuid());
mirrorEntity.SetName(neighborhood->GetName());
mirrorEntity.SetOwnerGUID(neighborhood->GetOwnerGuid());
// Add ALL 55 plot entries so Houses[i] = PlotIndex i
mirrorEntity.ClearHouses();
for (auto const& plot : neighborhood->GetPlots())
{
if (plot.IsOccupied() && !plot.HouseGuid.IsEmpty())
mirrorEntity.AddHouse(plot.HouseGuid, plot.OwnerGuid);
else
mirrorEntity.AddHouse(ObjectGuid::Empty, ObjectGuid::Empty);
}
// Add managers
mirrorEntity.ClearManagers();
for (auto const& member : neighborhood->GetMembers())
{
if (member.Role == NEIGHBORHOOD_ROLE_MANAGER || member.Role == NEIGHBORHOOD_ROLE_OWNER)
{
ObjectGuid bnetGuid;
if (Player* managerPlayer = ObjectAccessor::FindPlayer(member.PlayerGuid))
bnetGuid = managerPlayer->GetSession()->GetBattlenetAccountGUID();
mirrorEntity.AddManager(bnetGuid, member.PlayerGuid);
}
}
TC_LOG_DEBUG("housing", "Player::LoadFromDB: Pre-populated Housing/4 mirror entity with {} plots from neighborhood {}",
MAX_NEIGHBORHOOD_PLOTS, neighborhood->GetName());
// --- Housing/3: HousingPlayerHouseEntity ---
Housing* housing = _housings[0].get();
if (housing && !housing->GetHouseGuid().IsEmpty())
{
HousingPlayerHouseEntity& houseEntity = GetSession()->GetHousingPlayerHouseEntity();
houseEntity.SetBnetAccount(GetSession()->GetBattlenetAccountGUID());
houseEntity.SetEntityGUID(housing->GetHouseGuid());
houseEntity.SetPlotIndex(static_cast<int32>(housing->GetPlotIndex()));
houseEntity.SetLevel(housing->GetLevel());
houseEntity.SetFavor(housing->GetFavor64());
houseEntity.SetBudgets(
housing->GetMaxInteriorDecorBudget(),
housing->GetMaxExteriorDecorBudget(),
housing->GetMaxRoomBudget(),
housing->GetMaxFixtureBudget()
);
TC_LOG_DEBUG("housing", "Player::LoadFromDB: Pre-populated Housing/3 house entity: Plot={} Level={} HouseGuid={}",
housing->GetPlotIndex(), housing->GetLevel(), housing->GetHouseGuid().ToString());
}
}
}
//WowCommunity
_InitHonorLevelOnLoadFromDB(fields.honor, fields.honorLevel);
@@ -25742,8 +25830,6 @@ void Player::SendInitialPacketsAfterAddToMap()
HousingPlayerHouseEntity& houseEntity = GetSession()->GetHousingPlayerHouseEntity();
houseEntity.SetBnetAccount(GetSession()->GetBattlenetAccountGUID());
houseEntity.SetEntityGUID(housing->GetHouseGuid());
houseEntity.SetHouseType(housing->GetHouseType());
houseEntity.SetHouseSize(static_cast<uint32>(housing->GetHouseSize()));
houseEntity.SetPlotIndex(static_cast<int32>(housing->GetPlotIndex()));
houseEntity.SetLevel(housing->GetLevel());
houseEntity.SetFavor(housing->GetFavor64());
+86 -5
View File
@@ -551,10 +551,11 @@ void WorldSession::HandleHousingDecorSetEditMode(WorldPackets::Housing::HousingD
updateData.BuildPacket(&updatePacket);
player->SendDirectMessage(&updatePacket);
// Clear change masks after manual send to prevent duplicate sends on next map tick
// Clear change masks AND remove from _updateObjects to prevent duplicate
// VALUES_UPDATE on next map tick (causes "Object update failed" on client).
player->ClearUpdateMask(false);
GetBattlenetAccount().ClearUpdateMask(false);
GetHousingPlayerHouseEntity().ClearUpdateMask(false);
GetBattlenetAccount().ClearUpdateMask(true);
GetHousingPlayerHouseEntity().ClearUpdateMask(true);
}
// Diagnostic: log placed decor GUIDs from Housing vs what's on spawned MeshObjects.
@@ -1053,8 +1054,8 @@ void WorldSession::HandleHousingDecorRequestStorage(WorldPackets::Housing::Housi
updateData.BuildPacket(&updatePacket);
player->SendDirectMessage(&updatePacket);
GetBattlenetAccount().ClearUpdateMask(false);
GetHousingPlayerHouseEntity().ClearUpdateMask(false);
GetBattlenetAccount().ClearUpdateMask(true);
GetHousingPlayerHouseEntity().ClearUpdateMask(true);
}
// 3. Send GET_PLAYER_HOUSES_INFO_RESPONSE
@@ -1215,6 +1216,47 @@ void WorldSession::HandleHousingFixtureSetEditMode(WorldPackets::Housing::Housin
response.Result = static_cast<uint8>(HOUSING_RESULT_SUCCESS);
SendPacket(response.Write());
// Sync entity data so the client receives budget values when switching to customize mode.
// Without this, the client sees 0/0 budgets because the Player entity's EditorMode changed
// but the HousingPlayerHouseEntity/Account entity data was never flushed.
if (housingFixtureSetEditMode.Active)
{
housing->ResetStoragePopulated();
housing->PopulateCatalogStorageEntries();
housing->SyncUpdateFields();
player->BuildUpdateChangesMask();
GetBattlenetAccount().BuildUpdateChangesMask();
GetHousingPlayerHouseEntity().BuildUpdateChangesMask();
UpdateData updateData(player->GetMapId());
WorldPacket updatePacket;
player->BuildValuesUpdateBlockForPlayer(&updateData, player);
if (player->HaveAtClient(&GetBattlenetAccount()))
GetBattlenetAccount().BuildValuesUpdateBlockForPlayer(&updateData, player);
else
{
GetBattlenetAccount().BuildCreateUpdateBlockForPlayer(&updateData, player);
player->m_clientGUIDs.insert(GetBattlenetAccount().GetGUID());
}
if (player->HaveAtClient(&GetHousingPlayerHouseEntity()))
GetHousingPlayerHouseEntity().BuildValuesUpdateBlockForPlayer(&updateData, player);
else
{
GetHousingPlayerHouseEntity().BuildCreateUpdateBlockForPlayer(&updateData, player);
player->m_clientGUIDs.insert(GetHousingPlayerHouseEntity().GetGUID());
}
updateData.BuildPacket(&updatePacket);
player->SendDirectMessage(&updatePacket);
player->ClearUpdateMask(false);
GetBattlenetAccount().ClearUpdateMask(true);
GetHousingPlayerHouseEntity().ClearUpdateMask(true);
}
TC_LOG_INFO("housing", "CMSG_HOUSING_FIXTURE_SET_EDITOR_MODE_ACTIVE Active: {}", housingFixtureSetEditMode.Active);
}
@@ -1576,6 +1618,45 @@ void WorldSession::HandleHousingRoomSetLayoutEditMode(WorldPackets::Housing::Hou
response.Active = housingRoomSetLayoutEditMode.Active;
SendPacket(response.Write());
// Sync entity data so the client receives budget values when switching to layout mode.
if (housingRoomSetLayoutEditMode.Active)
{
housing->ResetStoragePopulated();
housing->PopulateCatalogStorageEntries();
housing->SyncUpdateFields();
player->BuildUpdateChangesMask();
GetBattlenetAccount().BuildUpdateChangesMask();
GetHousingPlayerHouseEntity().BuildUpdateChangesMask();
UpdateData updateData(player->GetMapId());
WorldPacket updatePacket;
player->BuildValuesUpdateBlockForPlayer(&updateData, player);
if (player->HaveAtClient(&GetBattlenetAccount()))
GetBattlenetAccount().BuildValuesUpdateBlockForPlayer(&updateData, player);
else
{
GetBattlenetAccount().BuildCreateUpdateBlockForPlayer(&updateData, player);
player->m_clientGUIDs.insert(GetBattlenetAccount().GetGUID());
}
if (player->HaveAtClient(&GetHousingPlayerHouseEntity()))
GetHousingPlayerHouseEntity().BuildValuesUpdateBlockForPlayer(&updateData, player);
else
{
GetHousingPlayerHouseEntity().BuildCreateUpdateBlockForPlayer(&updateData, player);
player->m_clientGUIDs.insert(GetHousingPlayerHouseEntity().GetGUID());
}
updateData.BuildPacket(&updatePacket);
player->SendDirectMessage(&updatePacket);
player->ClearUpdateMask(false);
GetBattlenetAccount().ClearUpdateMask(true);
GetHousingPlayerHouseEntity().ClearUpdateMask(true);
}
TC_LOG_INFO("housing", "CMSG_HOUSING_ROOM_SET_EDITOR_MODE_ACTIVE Active: {}", housingRoomSetLayoutEditMode.Active);
}
@@ -377,7 +377,7 @@ void WorldSession::HandleNeighborhoodCharterAddSignature(WorldPackets::Neighborh
if (!charter.AddSignature(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodCharterAddSignatureResponse response;
response.Result = static_cast<uint8>(HOUSING_RESULT_GENERIC_FAILURE);
response.Result = static_cast<uint8>(HOUSING_RESULT_DUPLICATE_CHARTER_SIGNATURE);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodCharterAddSignature: Player {} could not sign charter {}",
@@ -1058,7 +1058,7 @@ void WorldSession::HandleNeighborhoodBuyHouse(WorldPackets::Neighborhood::Neighb
if (!player->HasEnoughMoney(HOUSE_PURCHASE_COST_COPPER))
{
WorldPackets::Neighborhood::NeighborhoodBuyHouseResponse response;
response.Result = static_cast<uint8>(HOUSING_RESULT_GENERIC_FAILURE);
response.Result = static_cast<uint8>(HOUSING_RESULT_CANNOT_AFFORD);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodBuyHouse: Player {} cannot afford house (need {} copper, has {})",
@@ -1318,8 +1318,8 @@ void WorldSession::HandleNeighborhoodBuyHouse(WorldPackets::Neighborhood::Neighb
updateData.BuildPacket(&updatePacket);
player->SendDirectMessage(&updatePacket);
GetBattlenetAccount().ClearUpdateMask(false);
GetHousingPlayerHouseEntity().ClearUpdateMask(false);
GetBattlenetAccount().ClearUpdateMask(true);
GetHousingPlayerHouseEntity().ClearUpdateMask(true);
}
TC_LOG_ERROR("housing", "HandleNeighborhoodBuyHouse: Sent proactive STORAGE_RSP + Account + HouseEntity update (CatalogEntries={})",
@@ -1396,7 +1396,7 @@ void WorldSession::HandleNeighborhoodMoveHouse(WorldPackets::Neighborhood::Neigh
if (!player->HasEnoughMoney(HOUSE_MOVE_COST_COPPER))
{
WorldPackets::Neighborhood::NeighborhoodMoveHouseResponse response;
response.Result = static_cast<uint8>(HOUSING_RESULT_GENERIC_FAILURE);
response.Result = static_cast<uint8>(HOUSING_RESULT_CANNOT_AFFORD);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodMoveHouse: Player {} cannot afford move (need {} copper, has {})",
@@ -1710,7 +1710,7 @@ void WorldSession::HandleNeighborhoodGetRoster(WorldPackets::Neighborhood::Neigh
if (!neighborhood->IsMember(player->GetGUID()))
{
WorldPackets::Neighborhood::NeighborhoodGetRosterResponse response;
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.Result = static_cast<uint8>(HOUSING_RESULT_PERMISSION_DENIED);
SendPacket(response.Write());
TC_LOG_DEBUG("housing", "HandleNeighborhoodGetRoster: Player {} is not a member of neighborhood {}",
@@ -1913,7 +1913,23 @@ void WorldSession::HandleNeighborhoodInitiativeServiceStatusCheck(WorldPackets::
TC_LOG_DEBUG("housing", "CMSG_NEIGHBORHOOD_INITIATIVE_SERVICE_STATUS_CHECK for player {}",
player->GetGUID().ToString());
// Send initiative service status
sInitiativeManager.SendInitiativeServiceStatus(this, true);
// Proactively send initiative data ? the client's C_NeighborhoodInitiative singleton
// requires SMSG_GET_PLAYER_INITIATIVE_INFO_RESULT to set its isLoaded flag.
// Without this, GetNeighborhoodInitiativeInfo() returns nil and the endeavor UI is empty.
// Sniff-verified: the live server sends this SMSG proactively (not only in response to
// CMSG_INITIATIVE_UPDATE_ACTIVE_NEIGHBORHOOD which the client rarely/never sends).
if (Housing* housing = player->GetHousing())
{
ObjectGuid nhObjGuid = housing->GetNeighborhoodGuid();
if (!nhObjGuid.IsEmpty())
{
uint64 nhGuid = nhObjGuid.GetCounter();
sInitiativeManager.SendPlayerInitiativeInfo(this, nhObjGuid, nhGuid);
}
}
}
void WorldSession::HandleGetAvailableInitiativeRequest(WorldPackets::Neighborhood::GetAvailableInitiativeRequest const& getAvailableInitiativeRequest)
@@ -1926,13 +1942,14 @@ void WorldSession::HandleGetAvailableInitiativeRequest(WorldPackets::Neighborhoo
if (!neighborhood)
{
WorldPackets::Housing::GetPlayerInitiativeInfoResult response;
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.HasError = true;
SendPacket(response.Write());
return;
}
uint64 nhGuid = neighborhood->GetGuid().GetCounter();
sInitiativeManager.SendPlayerInitiativeInfo(this, nhGuid);
ObjectGuid nhObjGuid = neighborhood->GetGuid();
uint64 nhGuid = nhObjGuid.GetCounter();
sInitiativeManager.SendPlayerInitiativeInfo(this, nhObjGuid, nhGuid);
TC_LOG_DEBUG("housing", "CMSG_GET_AVAILABLE_INITIATIVE_REQUEST NeighborhoodGuid: {}, Player: {}",
getAvailableInitiativeRequest.NeighborhoodGuid.ToString(), player->GetGUID().ToString());
@@ -1977,13 +1994,14 @@ void WorldSession::HandleInitiativeUpdateActiveNeighborhood(WorldPackets::Neighb
return;
}
uint64 nhGuid = neighborhood->GetGuid().GetCounter();
ObjectGuid nhObjGuid = neighborhood->GetGuid();
uint64 nhGuid = nhObjGuid.GetCounter();
// Send initiative service status to confirm the service is active
sInitiativeManager.SendInitiativeServiceStatus(this, true);
// Send current initiative info for the active neighborhood (with real task progress)
sInitiativeManager.SendPlayerInitiativeInfo(this, nhGuid);
sInitiativeManager.SendPlayerInitiativeInfo(this, nhObjGuid, nhGuid);
TC_LOG_DEBUG("housing", "SMSG_INITIATIVE_SERVICE_STATUS + SMSG_GET_PLAYER_INITIATIVE_INFO_RESULT sent for NeighborhoodGuid: {}",
initiativeUpdateActiveNeighborhood.NeighborhoodGuid.ToString());
@@ -2034,19 +2052,11 @@ void WorldSession::HandleInitiativeReportProgress(WorldPackets::Neighborhood::In
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)
{
WorldPackets::Housing::GetPlayerInitiativeInfoResult response;
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
SendPacket(response.Write());
return;
}
uint64 nhGuid = neighborhood->GetGuid().GetCounter();
// Client is requesting initiative info for this neighborhood ? send current state
sInitiativeManager.SendPlayerInitiativeInfo(this, nhGuid);
// This is a client-side progress report (e.g. criteria completion tick), NOT a
// request for initiative info. Responding with SMSG_GET_PLAYER_INITIATIVE_INFO_RESULT
// caused the client to re-report in an infinite loop, stalling the UI.
// The initiative data is already delivered via the PlayerInitiativeComponent_C
// fragment (Fragment 37) in the player's UPDATE_OBJECT at login.
}
void WorldSession::HandleGetInitiativeClaimRewardRequest(WorldPackets::Neighborhood::GetInitiativeClaimRewardRequest const& packet)
@@ -2102,7 +2112,7 @@ void WorldSession::HandleGetInitiativeLeaderboardRequest(WorldPackets::Neighborh
if (!neighborhood)
{
WorldPackets::Housing::GetPlayerInitiativeInfoResult response;
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.HasError = true;
SendPacket(response.Write());
return;
}
@@ -2177,19 +2187,20 @@ void WorldSession::HandleGetInitiativeTaskAcceptRequest(WorldPackets::Neighborho
if (!neighborhood)
{
WorldPackets::Housing::GetPlayerInitiativeInfoResult response;
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.HasError = true;
SendPacket(response.Write());
return;
}
uint64 nhGuid = neighborhood->GetGuid().GetCounter();
ObjectGuid nhObjGuid = neighborhood->GetGuid();
uint64 nhGuid = nhObjGuid.GetCounter();
// Verify the task exists in the active initiative
ActiveInitiative* active = sInitiativeManager.GetActiveInitiative(nhGuid);
if (!active)
{
WorldPackets::Housing::GetPlayerInitiativeInfoResult response;
response.Result = static_cast<uint8>(HOUSING_RESULT_GENERIC_FAILURE);
response.HasError = true;
SendPacket(response.Write());
return;
}
@@ -2198,7 +2209,7 @@ void WorldSession::HandleGetInitiativeTaskAcceptRequest(WorldPackets::Neighborho
if (taskItr == active->TaskProgress.end())
{
WorldPackets::Housing::GetPlayerInitiativeInfoResult response;
response.Result = static_cast<uint8>(HOUSING_RESULT_GENERIC_FAILURE);
response.HasError = true;
SendPacket(response.Write());
return;
}
@@ -2208,7 +2219,7 @@ void WorldSession::HandleGetInitiativeTaskAcceptRequest(WorldPackets::Neighborho
taskItr->second.Status = INITIATIVE_TASK_STATUS_IN_PROGRESS;
// Send updated initiative info
sInitiativeManager.SendPlayerInitiativeInfo(this, nhGuid);
sInitiativeManager.SendPlayerInitiativeInfo(this, nhObjGuid, nhGuid);
}
void WorldSession::HandleGetInitiativeTaskAbandonRequest(WorldPackets::Neighborhood::GetInitiativeTaskAbandonRequest const& packet)
@@ -2224,17 +2235,18 @@ void WorldSession::HandleGetInitiativeTaskAbandonRequest(WorldPackets::Neighborh
if (!neighborhood)
{
WorldPackets::Housing::GetPlayerInitiativeInfoResult response;
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.HasError = true;
SendPacket(response.Write());
return;
}
uint64 nhGuid = neighborhood->GetGuid().GetCounter();
ObjectGuid nhObjGuid = neighborhood->GetGuid();
uint64 nhGuid = nhObjGuid.GetCounter();
ActiveInitiative* active = sInitiativeManager.GetActiveInitiative(nhGuid);
if (!active)
{
WorldPackets::Housing::GetPlayerInitiativeInfoResult response;
response.Result = static_cast<uint8>(HOUSING_RESULT_GENERIC_FAILURE);
response.HasError = true;
SendPacket(response.Write());
return;
}
@@ -2249,7 +2261,7 @@ void WorldSession::HandleGetInitiativeTaskAbandonRequest(WorldPackets::Neighborh
SendPacket(clearPacket.Write());
// Send updated initiative info
sInitiativeManager.SendPlayerInitiativeInfo(this, nhGuid);
sInitiativeManager.SendPlayerInitiativeInfo(this, nhObjGuid, nhGuid);
}
void WorldSession::HandleGetInitiativeTaskProgressRequest(WorldPackets::Neighborhood::GetInitiativeTaskProgressRequest const& packet)
@@ -2265,15 +2277,16 @@ void WorldSession::HandleGetInitiativeTaskProgressRequest(WorldPackets::Neighbor
if (!neighborhood)
{
WorldPackets::Housing::GetPlayerInitiativeInfoResult response;
response.Result = static_cast<uint8>(HOUSING_RESULT_NEIGHBORHOOD_NOT_FOUND);
response.HasError = true;
SendPacket(response.Write());
return;
}
uint64 nhGuid = neighborhood->GetGuid().GetCounter();
ObjectGuid nhObjGuid = neighborhood->GetGuid();
uint64 nhGuid = nhObjGuid.GetCounter();
// Send current initiative info which includes all task progress
sInitiativeManager.SendPlayerInitiativeInfo(this, nhGuid);
sInitiativeManager.SendPlayerInitiativeInfo(this, nhObjGuid, nhGuid);
}
// ============================================================
+8 -6
View File
@@ -75,8 +75,11 @@ bool Housing::LoadFromDB(PreparedQueryResult housing, PreparedQueryResult decor,
return false;
Field* fields = housing->Fetch();
// Expected columns: houseGuid, neighborhoodGuid, plotIndex, level, favor, settingsFlags, exteriorLocked, houseSize, houseType
_houseGuid = ObjectGuid::Create<HighGuid::Housing>(/*subType*/ 3, /*arg1*/ sRealmList->GetCurrentRealmId().Realm, /*arg2*/ 7, fields[0].GetUInt64());
// Expected columns: houseId, neighborhoodGuid, plotIndex, level, favor, settingsFlags, exteriorLocked, houseSize, houseType, ...
// fields[0] = houseId (DB2 entry ID) ? NOT used as GUID counter.
// Housing GUID counter must match HousingPlayerHouseEntity GUID (WorldSession.cpp), which uses battlenetAccountId.
uint32 bnetAccountId = _owner->GetSession()->GetBattlenetAccountId();
_houseGuid = ObjectGuid::Create<HighGuid::Housing>(/*subType*/ 3, /*arg1*/ sRealmList->GetCurrentRealmId().Realm, /*arg2*/ 7, uint64(bnetAccountId));
_neighborhoodGuid = ObjectGuid::Create<HighGuid::Housing>(/*subType*/ 4, /*arg1*/ sRealmList->GetCurrentRealmId().Realm, /*arg2*/ 0, fields[1].GetUInt64());
_plotIndex = fields[2].GetUInt8();
_level = fields[3].GetUInt32();
@@ -2037,8 +2040,7 @@ void Housing::SyncUpdateFields()
HousingPlayerHouseEntity& houseEntity = _owner->GetSession()->GetHousingPlayerHouseEntity();
houseEntity.SetBnetAccount(_owner->GetSession()->GetBattlenetAccountGUID());
houseEntity.SetEntityGUID(_houseGuid);
houseEntity.SetHouseType(_houseType);
houseEntity.SetHouseSize(static_cast<uint32>(_houseSize));
// HouseType and HouseSize are NOT part of this fragment (IDA-verified).
houseEntity.SetPlotIndex(static_cast<int32>(_plotIndex));
houseEntity.SetLevel(_level);
houseEntity.SetFavor(_favor64);
@@ -2051,9 +2053,9 @@ void Housing::SyncUpdateFields()
GetMaxFixtureBudget()
);
TC_LOG_ERROR("network", "Housing::SyncUpdateFields: EntityGUID={} BnetAccount={} HouseType={} HouseSize={} PlotIndex={} Level={} Favor={} Budgets=[{},{},{},{}]",
TC_LOG_DEBUG("housing", "Housing::SyncUpdateFields: EntityGUID={} BnetAccount={} PlotIndex={} Level={} Favor={} Budgets=[{},{},{},{}]",
_houseGuid.ToString(), _owner->GetSession()->GetBattlenetAccountGUID().ToString(),
_houseType, _houseSize, _plotIndex, _level, _favor64,
_plotIndex, _level, _favor64,
GetMaxInteriorDecorBudget(), GetMaxExteriorDecorBudget(), GetMaxRoomBudget(), GetMaxFixtureBudget());
}
+9 -35
View File
@@ -497,7 +497,7 @@ void HousingMap::SetPlotOwnershipState(uint8 plotIndex, bool owned)
uint32 neighborhoodMapId = _neighborhood->GetNeighborhoodMapID();
std::vector<NeighborhoodPlotData const*> plots = sHousingMgr.GetPlotsForMap(neighborhoodMapId);
TC_LOG_ERROR("housing", "SetPlotOwnershipState: Broadcasting WorldState for plot {} "
TC_LOG_DEBUG("housing", "SetPlotOwnershipState: Broadcasting WorldState for plot {} "
"(neighborhoodMapId={}, owned={}, numPlots={})",
plotIndex, neighborhoodMapId, owned, plots.size());
@@ -507,7 +507,7 @@ void HousingMap::SetPlotOwnershipState(uint8 plotIndex, bool owned)
{
if (plotData->WorldState != 0)
{
TC_LOG_ERROR("housing", "SetPlotOwnershipState: Found plot {} with WorldState={}, "
TC_LOG_DEBUG("housing", "SetPlotOwnershipState: Found plot {} with WorldState={}, "
"broadcasting to {} players",
plotIndex, plotData->WorldState,
std::distance(GetPlayers().begin(), GetPlayers().end()));
@@ -520,7 +520,7 @@ void HousingMap::SetPlotOwnershipState(uint8 plotIndex, bool owned)
HousingPlotOwnerType ownerType = GetPlotOwnerTypeForPlayer(mapPlayer, plotIndex);
mapPlayer->SendUpdateWorldState(plotData->WorldState, static_cast<uint32>(ownerType), false);
TC_LOG_ERROR("housing", "SetPlotOwnershipState: Sent WorldState {} = {} ({}) to player {}",
TC_LOG_DEBUG("housing", "SetPlotOwnershipState: Sent WorldState {} = {} ({}) to player {}",
plotData->WorldState, uint32(ownerType),
ownerType == HOUSING_PLOT_OWNER_SELF ? "SELF" :
ownerType == HOUSING_PLOT_OWNER_FRIEND ? "FRIEND" :
@@ -531,7 +531,7 @@ void HousingMap::SetPlotOwnershipState(uint8 plotIndex, bool owned)
}
else
{
TC_LOG_ERROR("housing", "SetPlotOwnershipState: Plot {} matched but WorldState=0, skipping broadcast",
TC_LOG_DEBUG("housing", "SetPlotOwnershipState: Plot {} matched but WorldState=0, skipping broadcast",
plotIndex);
}
break;
@@ -1329,7 +1329,7 @@ void HousingMap::SendPerPlayerPlotWorldStates(Player* player)
++friendCount;
}
TC_LOG_INFO("housing", "HousingMap::SendPerPlayerPlotWorldStates: Player {} ? sent {} WorldState updates "
TC_LOG_DEBUG("housing", "HousingMap::SendPerPlayerPlotWorldStates: Player {} ? sent {} WorldState updates "
"(self={}, friend={}, {} plots had no WorldState ID in DB2)",
player->GetGUID().ToString(), sentCount, selfCount, friendCount, noWsCount);
}
@@ -1454,35 +1454,9 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo
// since DB2 HouseRotation X/Y are always 0 and the computed facing is a pure yaw).
QuaternionData rot = QuaternionData::fromEulerAnglesZYX(facing, 0.0f, 0.0f);
// Spawn the platform WMO (GO entry 574432, GAMEOBJECT_TYPE_PHASEABLE_MO, displayId 113521).
// Retail sniff: each plot has a platform WMO that raises the house above terrain level.
// Some neighborhood maps have static platform spawns in the gameobject table (from sniffs);
// for maps without them, this dynamic spawn ensures a platform is always present.
{
constexpr uint32 PLATFORM_ENTRY = 574432;
Position platformPos(x, y, z, facing);
GameObject* platform = GameObject::CreateGameObject(PLATFORM_ENTRY, this, platformPos, rot, 255, GO_STATE_READY);
if (platform)
{
platform->SetFlag(GO_FLAG_NODESPAWN);
PhasingHandler::InitDbPhaseShift(platform->GetPhaseShift(), PHASE_USE_FLAGS_ALWAYS_VISIBLE, 0, 0);
if (!AddToMap(platform))
{
TC_LOG_ERROR("housing", "HousingMap::SpawnHouseForPlot: Failed to add platform GO 574432 to map for plot {}", plotIndex);
delete platform;
}
else
{
TC_LOG_DEBUG("housing", "HousingMap::SpawnHouseForPlot: Spawned platform GO 574432 guid={} at ({:.2f},{:.2f},{:.2f}) facing={:.3f} for plot {}",
platform->GetGUID().ToString(), x, y, z, facing, plotIndex);
}
}
else
{
TC_LOG_ERROR("housing", "HousingMap::SpawnHouseForPlot: Failed to create platform GO 574432 for plot {} at ({:.2f},{:.2f},{:.2f})",
plotIndex, x, y, z);
}
}
// Platform WMO (GO entry 574432) is loaded from the static gameobject table (sniff data).
// Do NOT dynamically spawn a second platform ? it renders visibly on top of the static
// one and the static spawn already provides DynamicMapTree collision for ground-clamping.
Position pos(x, y, z, facing);
Neighborhood::PlotInfo const* plotInfo = _neighborhood->GetPlotInfo(plotIndex);
@@ -1526,7 +1500,7 @@ GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* custo
}
// Spawn the front door GO (entry 586576, type Goober, displayId 116973)
// The interactive door GO must be at the actual doorway (top of stairs), NOT at
// The interactive door GO must be at the actual doorway (top of stairs), NOT at
// the door mesh origin (which sits at stair base level, ~0.56 below house floor).
// Door mesh local offset from base: (9.2805, -3.4555, -0.5611) ? mesh origin at stair foot
// Adjusted offset: raised Z to door frame level, pulled X back toward actual door threshold.
+1 -1
View File
@@ -950,7 +950,7 @@ void HousingMgr::LoadNeighborhoodInitiativeData()
data.RewardCurrencyID = entry->RewardCurrencyID;
}
TC_LOG_DEBUG("housing", "HousingMgr::LoadNeighborhoodInitiativeData: Loaded {} NeighborhoodInitiative entries", uint32(_neighborhoodInitiativeStore.size()));
TC_LOG_INFO("housing", "HousingMgr::LoadNeighborhoodInitiativeData: Loaded {} NeighborhoodInitiative entries", uint32(_neighborhoodInitiativeStore.size()));
}
void HousingMgr::LoadRoomComponentData()
+121 -16
View File
@@ -46,6 +46,10 @@ void InitiativeManager::Initialize()
BuildDB2IndexMaps();
LoadFromDB();
// Auto-start initiatives for neighborhoods that don't have active ones.
// Must run AFTER LoadFromDB() and AFTER sNeighborhoodMgr.Initialize().
CheckAndStartInitiatives();
TC_LOG_INFO("housing", "InitiativeManager: Initialized with {} initiative definitions, {} active instances across all neighborhoods",
uint32(sNeighborhoodInitiativeStore.GetNumRows()), [this]() -> uint32 {
uint32 count = 0;
@@ -775,15 +779,78 @@ void InitiativeManager::SendInitiativeServiceStatus(WorldSession* session, bool
TC_LOG_DEBUG("housing", "InitiativeManager: Sent InitiativeServiceStatus (enabled={})", enabled);
}
void InitiativeManager::SendPlayerInitiativeInfo(WorldSession* session, uint64 neighborhoodGuid) const
void InitiativeManager::SendPlayerInitiativeInfo(WorldSession* session, ObjectGuid const& neighborhoodGuid, uint64 neighborhoodLowGuid) const
{
WorldPackets::Housing::GetPlayerInitiativeInfoResult result;
result.Result = static_cast<uint32>(HOUSING_RESULT_SUCCESS);
result.NeighborhoodGUID = neighborhoodGuid;
// Populate with current task progress for the active initiative
ActiveInitiative* active = GetActiveInitiative(neighborhoodGuid);
ActiveInitiative* active = GetActiveInitiative(neighborhoodLowGuid);
if (active)
{
result.HasInitiativeData = true;
uint32 cycleID = GetActiveCycleForInitiative(active->InitiativeID);
// Compute remaining duration from initiative start + cycle duration.
// Sniff-verified: RemainingDuration is in seconds (sniff value 972957 ? 11.25 days).
// If duration would be 0, use a 7-day fallback so the client shows the initiative
// as active (Duration=0 ? client treats as expired ? empty endeavor list).
int64 remainingDuration = 0;
{
int64 totalDurationSec = 0;
if (auto cycleIt = _initiativeActiveCycle.find(active->InitiativeID); cycleIt != _initiativeActiveCycle.end())
{
if (InitiativeCycleEntry const* cycle = sInitiativeCycleStore.LookupEntry(cycleIt->second))
totalDurationSec = static_cast<int64>(cycle->Duration) * 86400;
}
// Fallback: if DB2 has no duration, use 7 days
if (totalDurationSec <= 0)
totalDurationSec = 7 * 86400;
int64 elapsed = GameTime::GetGameTime() - active->StartTime;
remainingDuration = totalDurationSec - elapsed;
// If expired, reset the start time to now so the initiative stays active
if (remainingDuration <= 0)
{
active->StartTime = static_cast<uint32>(GameTime::GetGameTime());
remainingDuration = totalDurationSec;
}
}
// Current milestone: find the highest milestone reached.
// Sniff-verified: ProgressRequired=1000.0 (the 0-1000 scale, not 0.0-1.0).
// active->Progress is stored as 0.0-1.0, so scale it to 0-1000 for comparison
// with DB2 milestones (which use the 0-1000 scale).
int32 currentMilestoneID = -1;
float progressRequired = 1000.0f; // Sniff: always 1000
float currentProgress = active->Progress * 1000.0f;
auto msIt = _cycleMilestones.find(cycleID);
if (msIt != _cycleMilestones.end())
{
for (auto const& ms : msIt->second)
{
if (currentProgress >= ms.ProgressRequired)
currentMilestoneID = static_cast<int32>(ms.MilestoneID);
if (progressRequired < ms.ProgressRequired)
progressRequired = ms.ProgressRequired;
}
}
float playerContribution = 0.0f;
if (session->GetPlayer())
playerContribution = static_cast<float>(
GetPlayerContribution(neighborhoodLowGuid, active->InitiativeID, session->GetPlayer()->GetGUID().GetCounter()));
result.RemainingDuration = remainingDuration;
result.CurrentInitiativeID = static_cast<int32>(active->InitiativeID);
result.CurrentMilestoneID = currentMilestoneID;
result.CurrentCycleID = static_cast<int32>(cycleID);
result.ProgressRequired = progressRequired;
result.CurrentProgress = currentProgress;
result.PlayerTotalContribution = playerContribution;
// Populate task progress
for (auto const& [taskId, taskProgress] : active->TaskProgress)
{
WorldPackets::Housing::JamPlayerInitiativeTaskInfo taskInfo;
@@ -795,8 +862,8 @@ void InitiativeManager::SendPlayerInitiativeInfo(WorldSession* session, uint64 n
}
session->SendPacket(result.Write());
TC_LOG_DEBUG("housing", "InitiativeManager: Sent GetPlayerInitiativeInfoResult with {} tasks for neighborhood {}",
uint32(result.Tasks.size()), neighborhoodGuid);
TC_LOG_DEBUG("housing", "InitiativeManager: Sent GetPlayerInitiativeInfoResult HasData={} InitID={} Tasks={} for neighborhood {}",
result.HasInitiativeData, result.CurrentInitiativeID, uint32(result.Tasks.size()), neighborhoodLowGuid);
}
void InitiativeManager::SendActivityLog(WorldSession* session, uint64 neighborhoodGuid) const
@@ -940,6 +1007,34 @@ void InitiativeManager::BroadcastRewardAvailable(Neighborhood* neighborhood, uin
void InitiativeManager::CheckAndStartInitiatives()
{
// First, remove any active initiatives that have no tasks or no cycle defined.
// Task-less initiatives produce empty endeavor lists.
// Cycle-less initiatives produce CycleID=0 in the player fragment, causing the
// client's Lua UI to fail displaying the endeavor (no milestones, no duration).
for (auto& [nhGuid, initiatives] : _activeInitiatives)
{
std::erase_if(initiatives, [this](std::unique_ptr<ActiveInitiative> const& init) {
if (init->Completed)
return false;
if (_initiativeTasks.find(init->InitiativeID) == _initiativeTasks.end())
{
TC_LOG_INFO("housing", "InitiativeManager: Removing task-less initiative (DB2 ID {}) from neighborhood {}",
init->InitiativeID, init->NeighborhoodGuid);
return true;
}
if (GetActiveCycleForInitiative(init->InitiativeID) == 0)
{
TC_LOG_INFO("housing", "InitiativeManager: Removing cycle-less initiative (DB2 ID {}) from neighborhood {}",
init->InitiativeID, init->NeighborhoodGuid);
return true;
}
return false;
});
}
// For each neighborhood that doesn't have an active initiative, start one
for (Neighborhood* neighborhood : sNeighborhoodMgr.GetAllNeighborhoods())
{
@@ -975,17 +1070,27 @@ void InitiativeManager::CheckAndStartInitiatives()
}
}
if (!recentlyCompleted)
{
// Look up cycle priority weight for this initiative's active cycle
uint32 cycleID = SelectWeightedCycle(entry->ID);
int32 weight = 1; // default equal weight
auto prioItr = _cyclePriorities.find(cycleID);
if (prioItr != _cyclePriorities.end() && !prioItr->second.empty())
weight = std::max<int32>(1, prioItr->second[0].second);
if (recentlyCompleted)
continue;
candidates.emplace_back(entry->ID, weight);
}
// Skip initiatives that have no tasks defined ? they produce empty
// endeavor lists and waste a neighborhood's active initiative slot.
if (_initiativeTasks.find(entry->ID) == _initiativeTasks.end())
continue;
// Skip initiatives that have no cycle defined ? the client requires a
// valid CycleID to display milestones, duration, and rewards in the UI.
if (GetActiveCycleForInitiative(entry->ID) == 0)
continue;
// Look up cycle priority weight for this initiative's active cycle
uint32 cycleID = SelectWeightedCycle(entry->ID);
int32 weight = 1; // default equal weight
auto prioItr = _cyclePriorities.find(cycleID);
if (prioItr != _cyclePriorities.end() && !prioItr->second.empty())
weight = std::max<int32>(1, prioItr->second[0].second);
candidates.emplace_back(entry->ID, weight);
}
if (!candidates.empty())
+1 -1
View File
@@ -134,7 +134,7 @@ public:
// Send packets to a session
void SendInitiativeServiceStatus(WorldSession* session, bool enabled) const;
void SendPlayerInitiativeInfo(WorldSession* session, uint64 neighborhoodGuid) const;
void SendPlayerInitiativeInfo(WorldSession* session, ObjectGuid const& neighborhoodGuid, uint64 neighborhoodLowGuid) const;
void SendActivityLog(WorldSession* session, uint64 neighborhoodGuid) const;
void SendInitiativeRewardsResult(WorldSession* session, uint32 result) const;
+6 -6
View File
@@ -85,12 +85,6 @@ bool Neighborhood::LoadFromDB(PreparedQueryResult neighborhood, PreparedQueryRes
_plots[member.PlotIndex].PlotIndex = member.PlotIndex;
_plots[member.PlotIndex].OwnerGuid = member.PlayerGuid;
// Resolve HouseGuid from character_housing JOIN (column 4)
uint64 houseId = memberFields[4].GetUInt64();
if (houseId != 0)
_plots[member.PlotIndex].HouseGuid = ObjectGuid::Create<HighGuid::Housing>(
/*subType*/ 3, /*arg1*/ sRealmList->GetCurrentRealmId().Realm, /*arg2*/ 7, houseId);
// Resolve BNet account GUID from characters.account JOIN (column 5).
// The client requires non-zero HouseOwnerBnetAccountGUID on the plot AreaTrigger's
// FHousingPlotAreaTrigger_C fragment for IsInsidePlot() validation.
@@ -99,7 +93,13 @@ bool Neighborhood::LoadFromDB(PreparedQueryResult neighborhood, PreparedQueryRes
{
uint32 bnetAccountId = Battlenet::AccountMgr::GetIdByGameAccount(gameAccountId);
if (bnetAccountId != 0)
{
_plots[member.PlotIndex].OwnerBnetGuid = ObjectGuid::Create<HighGuid::BNetAccount>(bnetAccountId);
// HouseGuid counter MUST match HousingPlayerHouseEntity GUID (WorldSession.cpp),
// which uses battlenetAccountId. Using ch.houseId (DB2 entry) was wrong.
_plots[member.PlotIndex].HouseGuid = ObjectGuid::Create<HighGuid::Housing>(
/*subType*/ 3, /*arg1*/ sRealmList->GetCurrentRealmId().Realm, /*arg2*/ 7, uint64(bnetAccountId));
}
}
}
} while (members->NextRow());
@@ -1994,7 +1994,22 @@ namespace WorldPackets::Housing
WorldPacket const* GetPlayerInitiativeInfoResult::Write()
{
_worldPacket << uint8(Result);
_worldPacket << NeighborhoodGUID;
_worldPacket.WriteBit(HasError);
_worldPacket.WriteBit(HasInitiativeData);
_worldPacket.FlushBits();
if (HasInitiativeData)
{
_worldPacket << int64(RemainingDuration);
_worldPacket << int32(CurrentInitiativeID);
_worldPacket << int32(CurrentMilestoneID);
_worldPacket << int32(CurrentCycleID);
_worldPacket << float(ProgressRequired);
_worldPacket << float(CurrentProgress);
_worldPacket << float(PlayerTotalContribution);
}
_worldPacket << uint32(Tasks.size());
for (auto const& task : Tasks)
{
@@ -2003,9 +2018,9 @@ namespace WorldPackets::Housing
_worldPacket << uint32(task.Status);
}
TC_LOG_DEBUG("network.opcode", "SMSG_GET_PLAYER_INITIATIVE_INFO_RESULT Result: {} TaskCount: {}", Result, Tasks.size());
for (size_t i = 0; i < Tasks.size(); ++i)
TC_LOG_DEBUG("network.opcode", " Task[{}]: TaskID={} Progress={} Status={}", i, Tasks[i].TaskID, Tasks[i].Progress, Tasks[i].Status);
TC_LOG_DEBUG("network.opcode", "SMSG_GET_PLAYER_INITIATIVE_INFO_RESULT NH={} Error={} HasData={} InitID={} CycleID={} Progress={:.1f}/{:.0f} Tasks={}",
NeighborhoodGUID.ToString(), HasError, HasInitiativeData,
CurrentInitiativeID, CurrentCycleID, CurrentProgress, ProgressRequired, Tasks.size());
return &_worldPacket;
}
@@ -2781,6 +2796,7 @@ namespace WorldPackets::Neighborhood
void InitiativeReportProgress::Read()
{
_worldPacket >> NeighborhoodGuid;
TC_LOG_DEBUG("network.opcode", "CMSG_INITIATIVE_REPORT_PROGRESS NeighborhoodGuid: {}",
NeighborhoodGuid.ToString());
}
@@ -56,16 +56,17 @@ namespace WorldPackets::Housing
// IDA-verified wire format for house/resident entries nested inside neighborhood data.
// Deserializer: Deserialize_ResidentArray (0x7FF724C3EEF0), stride 80 bytes in memory.
// Wire: PackedGUID + PackedGUID + PackedGUID + uint8 + uint32 + uint8(bit7=hasOpt) [+ uint64]
// Wire order: PackedGUID(House) + PackedGUID(Owner) + PackedGUID(Neighborhood) + uint8 + uint32 + uint8(bit7=hasOpt) [+ uint64]
// IDA proof: offset-0 GUID compared vs house records; offset-16 GUID passed to ai_Process_PlayerContextUpdate (name lookup).
struct JamCliHouse
{
ObjectGuid OwnerGUID; // offset 0: house owner player GUID
ObjectGuid HouseGUID; // offset 16: house entity GUID
ObjectGuid NeighborhoodGUID; // offset 32: neighborhood GUID (matched by client against SetViewingNeighborhood stored value)
uint8 HouseLevel = 0; // offset 48
uint32 PlotIndex = 0; // offset 72
bool HasOptionalField = false; // offset 64: derived from bit 7 of wire uint8
uint64 OptionalValue = 0; // offset 56: only present if HasOptionalField
ObjectGuid HouseGUID; // wire pos 1, client offset 0: house entity GUID (compared vs HouseInfo records)
ObjectGuid OwnerGUID; // wire pos 2, client offset 16: owner player GUID (used for name lookup)
ObjectGuid NeighborhoodGUID; // wire pos 3, client offset 32: neighborhood GUID
uint8 HouseLevel = 0; // client offset 48
uint32 PlotIndex = 0; // client offset 72
bool HasOptionalField = false; // client offset 64: derived from bit 7 of wire uint8
uint64 OptionalValue = 0; // client offset 56: only present if HasOptionalField
};
// IDA-verified wire format for neighborhood entries in house finder responses.
@@ -2096,7 +2097,20 @@ namespace WorldPackets::Housing
public:
GetPlayerInitiativeInfoResult() : ServerPacket(SMSG_GET_PLAYER_INITIATIVE_INFO_RESULT) {}
WorldPacket const* Write() override;
uint8 Result = 0;
ObjectGuid NeighborhoodGUID;
bool HasError = false;
bool HasInitiativeData = false;
// InitiativeInfo block (only written when HasInitiativeData = true)
int64 RemainingDuration = 0;
int32 CurrentInitiativeID = 0;
int32 CurrentMilestoneID = -1;
int32 CurrentCycleID = 0;
float ProgressRequired = 0.0f;
float CurrentProgress = 0.0f;
float PlayerTotalContribution = 0.0f;
std::vector<JamPlayerInitiativeTaskInfo> Tasks;
};
+2 -2
View File
@@ -125,8 +125,8 @@ WorldSession::WorldSession(uint32 id, std::string&& name, uint32 battlenetAccoun
_accountId(id),
_accountName(std::move(name)),
_battlenetAccount(new Battlenet::Account(this, ObjectGuid::Create<HighGuid::BNetAccount>(battlenetAccountId), std::move(battlenetAccountEmail))),
_housingPlayerHouseEntity(new HousingPlayerHouseEntity(this, ObjectGuid::Create<HighGuid::Housing>(/*subType*/3, /*arg1*/1, /*arg2*/7, /*arg3*/battlenetAccountId))),
_housingNeighborhoodMirrorEntity(new HousingNeighborhoodMirrorEntity(this, ObjectGuid::Create<HighGuid::Housing>(/*subType*/4, /*arg1*/2, /*arg2*/0, /*arg3*/battlenetAccountId))),
_housingPlayerHouseEntity(new HousingPlayerHouseEntity(this, ObjectGuid::Create<HighGuid::Housing>(/*subType*/3, /*arg1*/sRealmList->GetCurrentRealmId().Realm, /*arg2*/7, /*arg3*/battlenetAccountId))),
_housingNeighborhoodMirrorEntity(new HousingNeighborhoodMirrorEntity(this, ObjectGuid::Create<HighGuid::Housing>(/*subType*/4, /*arg1*/sRealmList->GetCurrentRealmId().Realm, /*arg2*/0, /*arg3*/battlenetAccountId))),
m_accountExpansion(expansion),
m_expansion(std::min<uint8>(expansion, sWorld->getIntConfig(CONFIG_EXPANSION))),
_os(std::move(os)),