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:
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user