Housing phase 1: quest access and durable house purchase

This commit is contained in:
luis
2026-10-04 21:13:35 -03:00
parent 3a2e513361
commit cb19c51fbc
13 changed files with 623 additions and 400 deletions
-24
View File
@@ -1,24 +0,0 @@
build*/
.directory
.mailmap
*.orig
*.rej
*~
.hg/
*.kdev*
.DS_Store
CMakeLists.txt.user
*.bak
*.patch
*.diff
*.REMOTE.*
*.BACKUP.*
*.BASE.*
*.LOCAL.*
nbproject/*
.idea/*
.browse.VC*
.vscode
cmake-build-*/
.vs
@@ -0,0 +1,314 @@
-- Housing quest/purchase milestone. Apply to characters through the normal DB updater.
-- Additive schema only: no DROP, TRUNCATE, DELETE or imported TrinityCore housing schema.
-- Full GUIDs use an ASCII binary collation; character counters remain BIGINT UNSIGNED.
CREATE TABLE IF NOT EXISTS `character_housing` (
OwnerGuid bigint unsigned NOT NULL,
HouseGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
CosmeticOwnerGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
NeighborhoodGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
PlotID tinyint unsigned NOT NULL,
ExteriorMapID int unsigned NOT NULL,
HouseSettingFlags int unsigned NOT NULL DEFAULT 0,
ReservationTime bigint unsigned DEFAULT NULL,
PRIMARY KEY (HouseGuid),
UNIQUE KEY uq_owner (OwnerGuid),
UNIQUE KEY uq_plot (NeighborhoodGuid, ExteriorMapID, PlotID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_neighborhood` (
NeighborhoodGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
OwnerGuid bigint unsigned NOT NULL,
Name varchar(127) NOT NULL DEFAULT '',
Type int unsigned NOT NULL DEFAULT 0,
Flags int unsigned NOT NULL DEFAULT 0,
IsPublic tinyint unsigned NOT NULL DEFAULT 0,
GuildId int unsigned DEFAULT NULL,
PRIMARY KEY (NeighborhoodGuid),
UNIQUE KEY uq_guild (GuildId),
KEY idx_owner (OwnerGuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_plot_template` (
PlotID tinyint unsigned NOT NULL,
ExteriorMapID int unsigned NOT NULL,
InteriorMapID int unsigned NOT NULL DEFAULT 2783,
DestinationX float NOT NULL DEFAULT 0,
DestinationY float NOT NULL DEFAULT 0,
DestinationZ float NOT NULL DEFAULT 0,
DestinationO float NOT NULL DEFAULT 0,
CornerstoneEntry int unsigned NOT NULL,
CornerstonePositionX float NOT NULL DEFAULT 0,
CornerstonePositionY float NOT NULL DEFAULT 0,
CornerstonePositionZ float NOT NULL DEFAULT 0,
CornerstoneRotationZ float NOT NULL DEFAULT 0,
HousePositionX float NOT NULL DEFAULT 0,
HousePositionY float NOT NULL DEFAULT 0,
HousePositionZ float NOT NULL DEFAULT 0,
HouseRotationZ float NOT NULL DEFAULT 0,
DoorEntry int unsigned NOT NULL DEFAULT 0,
Cost bigint unsigned NOT NULL,
VerifiedBuild int NOT NULL DEFAULT 0,
PRIMARY KEY (ExteriorMapID, PlotID, VerifiedBuild)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_plot_reservation` (
ExteriorMapID int unsigned NOT NULL,
PlotID tinyint unsigned NOT NULL,
OwnerGuid bigint unsigned NOT NULL,
NeighborhoodGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
ReservedAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
ExpiresAt timestamp NOT NULL,
PRIMARY KEY (NeighborhoodGuid, ExteriorMapID, PlotID),
UNIQUE KEY uq_owner (OwnerGuid),
KEY idx_expiry (ExpiresAt)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_plot_transaction` (
TransactionID bigint unsigned NOT NULL AUTO_INCREMENT,
OwnerGuid bigint unsigned NOT NULL,
HouseGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
NeighborhoodGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
OldPlotID tinyint unsigned NOT NULL,
NewPlotID tinyint unsigned NOT NULL,
ExteriorMapID int unsigned NOT NULL,
Cost bigint unsigned NOT NULL DEFAULT 0,
TransactionType tinyint unsigned NOT NULL,
CreatedAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (TransactionID),
KEY idx_owner_house (OwnerGuid, HouseGuid, TransactionID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `character_housing_map_context` (
OwnerGuid bigint unsigned NOT NULL,
MapID int unsigned NOT NULL,
ContextGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
PRIMARY KEY (OwnerGuid, MapID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_map_instance` (
InstanceID int unsigned NOT NULL AUTO_INCREMENT,
MapID int unsigned NOT NULL,
ContextGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
PRIMARY KEY (InstanceID),
UNIQUE KEY uq_map_context (MapID, ContextGuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `character_housing_neighborhood_invite` (
NeighborhoodGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
InviteeGuid bigint unsigned NOT NULL,
InviterGuid bigint unsigned NOT NULL,
CreatedAt bigint unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (NeighborhoodGuid, InviteeGuid),
KEY idx_invitee (InviteeGuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `character_housing_neighborhood_role` (
NeighborhoodGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
CharacterGuid bigint unsigned NOT NULL,
Role tinyint unsigned NOT NULL DEFAULT 0,
GrantedBy bigint unsigned NOT NULL DEFAULT 0,
GrantedAt bigint unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (NeighborhoodGuid, CharacterGuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `character_housing_finder_ignore` (
CharacterGuid bigint unsigned NOT NULL,
NeighborhoodGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
CreatedAt bigint unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (CharacterGuid, NeighborhoodGuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `character_housing_return_location` (
OwnerGuid bigint unsigned NOT NULL,
HouseGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
NeighborhoodGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
ExteriorMapID int unsigned NOT NULL,
PlotID tinyint unsigned NOT NULL,
PositionX float NOT NULL DEFAULT 0,
PositionY float NOT NULL DEFAULT 0,
PositionZ float NOT NULL DEFAULT 0,
Orientation float NOT NULL DEFAULT 0,
PRIMARY KEY (OwnerGuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `character_housing_permission` (
HouseGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
CharacterGuid bigint unsigned NOT NULL,
Permissions int unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (HouseGuid, CharacterGuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Empty entity/storage backing tables are needed by the existing house graph
-- and dashboard readers immediately after a purchase. No decor or room data is seeded.
CREATE TABLE IF NOT EXISTS `housing_room_entity` (
RoomGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
HouseGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
HouseRoomID int NOT NULL,
Flags int NOT NULL DEFAULT 0,
PositionX float NOT NULL DEFAULT 0,
PositionY float NOT NULL DEFAULT 0,
PositionZ float NOT NULL DEFAULT 0,
RotationX float NOT NULL DEFAULT 0,
RotationY float NOT NULL DEFAULT 0,
RotationZ float NOT NULL DEFAULT 0,
RotationW float NOT NULL DEFAULT 1,
Scale float NOT NULL DEFAULT 1,
AttachmentFlags tinyint unsigned NOT NULL DEFAULT 3,
FloorIndex int NOT NULL DEFAULT 0,
LayoutSlotIndex int unsigned DEFAULT NULL,
PRIMARY KEY (RoomGuid),
KEY idx_house (HouseGuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_room_component_entity` (
EntityGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
HouseGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
FileDataID int unsigned NOT NULL DEFAULT 0,
IsWMO tinyint unsigned NOT NULL DEFAULT 0,
IsRoom tinyint unsigned NOT NULL DEFAULT 0,
AttachParentGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '',
PositionX float NOT NULL DEFAULT 0,
PositionY float NOT NULL DEFAULT 0,
PositionZ float NOT NULL DEFAULT 0,
RotationX float NOT NULL DEFAULT 0,
RotationY float NOT NULL DEFAULT 0,
RotationZ float NOT NULL DEFAULT 0,
RotationW float NOT NULL DEFAULT 1,
Scale float NOT NULL DEFAULT 1,
AttachmentFlags tinyint unsigned NOT NULL DEFAULT 3,
RoomGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
RoomComponentOptionID int NOT NULL DEFAULT 0,
RoomComponentID int NOT NULL DEFAULT 0,
RoomComponentType tinyint unsigned NOT NULL DEFAULT 0,
HouseThemeID int NOT NULL DEFAULT 0,
RoomComponentTextureID int NOT NULL DEFAULT 0,
RoomComponentTypeParam int NOT NULL DEFAULT 0,
PRIMARY KEY (EntityGuid),
KEY idx_house_room (HouseGuid, RoomGuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_room_connection` (
HouseGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
RoomGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
RoomComponentID int NOT NULL,
OffsetX float NOT NULL DEFAULT 0,
OffsetY float NOT NULL DEFAULT 0,
OffsetZ float NOT NULL DEFAULT 0,
RoomComponentType tinyint unsigned NOT NULL DEFAULT 0,
AttachedRoomGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
PRIMARY KEY (HouseGuid, RoomGuid, RoomComponentID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_fixture_entity` (
EntityGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
HouseGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
FileDataID int unsigned NOT NULL DEFAULT 0,
IsWMO tinyint unsigned NOT NULL DEFAULT 0,
IsRoom tinyint unsigned NOT NULL DEFAULT 0,
AttachParentGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '',
PositionX float NOT NULL DEFAULT 0,
PositionY float NOT NULL DEFAULT 0,
PositionZ float NOT NULL DEFAULT 0,
RotationX float NOT NULL DEFAULT 0,
RotationY float NOT NULL DEFAULT 0,
RotationZ float NOT NULL DEFAULT 0,
RotationW float NOT NULL DEFAULT 1,
Scale float NOT NULL DEFAULT 1,
AttachmentFlags tinyint unsigned NOT NULL DEFAULT 3,
ExteriorComponentID int NOT NULL DEFAULT 0,
HouseExteriorWmoDataID int NOT NULL DEFAULT 0,
ExteriorComponentHookID int NOT NULL DEFAULT 0,
ExteriorComponentType tinyint unsigned NOT NULL DEFAULT 0,
Size tinyint unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (EntityGuid),
KEY idx_house (HouseGuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_placed_decor` (
DecorGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
EntityGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL,
HouseGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
FileDataID int unsigned NOT NULL DEFAULT 0,
IsWMO tinyint unsigned NOT NULL DEFAULT 0,
IsRoom tinyint unsigned NOT NULL DEFAULT 0,
AttachParentGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '',
PositionX float NOT NULL DEFAULT 0,
PositionY float NOT NULL DEFAULT 0,
PositionZ float NOT NULL DEFAULT 0,
RotationX float NOT NULL DEFAULT 0,
RotationY float NOT NULL DEFAULT 0,
RotationZ float NOT NULL DEFAULT 0,
RotationW float NOT NULL DEFAULT 1,
Scale float NOT NULL DEFAULT 1,
AttachmentFlags tinyint unsigned NOT NULL DEFAULT 3,
RoomGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '',
OwnerGuid bigint unsigned NOT NULL DEFAULT 0,
ParentHouseFixtureGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '',
PlacedComponentID int unsigned NOT NULL DEFAULT 0,
Flags tinyint unsigned NOT NULL DEFAULT 0,
DyeSlot0 int unsigned NOT NULL DEFAULT 0,
DyeSlot1 int unsigned NOT NULL DEFAULT 0,
DyeSlot2 int unsigned NOT NULL DEFAULT 0,
Locked tinyint unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (DecorGuid),
KEY idx_house (HouseGuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_decor_instance` (
DecorGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
BattlenetAccountId int unsigned NOT NULL,
DecorID int unsigned NOT NULL,
HouseGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '',
PlacementStatus tinyint unsigned NOT NULL DEFAULT 0,
SourceType tinyint unsigned NOT NULL DEFAULT 0,
SourceValue varchar(255) NOT NULL DEFAULT '',
PRIMARY KEY (DecorGuid),
KEY idx_account_decor (BattlenetAccountId, DecorID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `account_housing_decor` (
BattlenetAccountId int unsigned NOT NULL,
DecorId int unsigned NOT NULL,
Quantity int unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (BattlenetAccountId, DecorId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_entity_quarantine` (
EntityType varchar(32) NOT NULL,
EntityGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
Reason varchar(127) NOT NULL,
HitCount int unsigned NOT NULL DEFAULT 1,
PRIMARY KEY (EntityType, EntityGuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_blueprint` (
ID bigint unsigned NOT NULL AUTO_INCREMENT,
BnetAccountID int unsigned NOT NULL,
UUID varchar(127) NOT NULL,
Name varchar(127) NOT NULL DEFAULT '',
Type int unsigned NOT NULL DEFAULT 0,
DateCreated bigint NOT NULL DEFAULT 0,
DateDeleted bigint NOT NULL DEFAULT 0,
Flags tinyint unsigned NOT NULL DEFAULT 0,
DeleteReason tinyint unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (ID),
KEY idx_account (BnetAccountID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_blueprint_item` (
BlueprintID bigint unsigned NOT NULL,
ItemID int unsigned NOT NULL,
Quantity int unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (BlueprintID, ItemID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `housing_deferred_decor_redemption` (
BattlenetAccountId int unsigned NOT NULL,
TransactionID bigint unsigned NOT NULL,
DecorID int unsigned NOT NULL,
DecorGuid varchar(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
PRIMARY KEY (BattlenetAccountId, TransactionID),
KEY idx_decor (DecorGuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-24
View File
@@ -1,24 +0,0 @@
build*/
.directory
.mailmap
*.orig
*.rej
*~
.hg/
*.kdev*
.DS_Store
CMakeLists.txt.user
*.bak
*.patch
*.diff
*.REMOTE.*
*.BACKUP.*
*.BASE.*
*.LOCAL.*
nbproject/*
.idea/*
.browse.VC*
.vscode
cmake-build-*/
.vs
@@ -0,0 +1,25 @@
-- Phase 1: expose the existing housing quest and native cornerstone purchase UI.
-- Requires the deployment's populated world database; does not fabricate quest
-- templates, objectives, NPC spawns or client-build-dependent display IDs.
UPDATE creature_template SET npcflag = npcflag | 3, ScriptName = 'npc_housing_steward'
WHERE entry IN (233063, 233708, 256078);
-- 91863 must be reachable directly through the explicit tutorial action.
-- Preserve all unrelated addon fields (mail, restrictions, script, rewards).
INSERT INTO quest_template_addon (ID, PrevQuestID)
SELECT ID, 0 FROM quest_template WHERE ID = 91863
ON DUPLICATE KEY UPDATE PrevQuestID = VALUES(PrevQuestID);
INSERT IGNORE INTO creature_queststarter (id, quest)
SELECT c.entry, q.ID FROM creature_template c JOIN quest_template q ON q.ID = 91863
WHERE c.entry IN (233063, 233708);
INSERT IGNORE INTO creature_questender (id, quest)
SELECT c.entry, q.ID FROM creature_template c JOIN quest_template q ON q.ID = 91863
WHERE c.entry IN (233063, 233708);
-- The existing generic template is the visual fallback for DB2-specific entries.
UPDATE gameobject_template SET Data0 = 4, Data7 = 70
WHERE entry = 457142 AND type = 43;
-- Keep objective amounts/flags and exploration/location requirements untouched.
@@ -64,6 +64,8 @@
//WowCommunity
#include "Garrison.h"
#include "NPCPackets.h"
#include "HousingMgr.h"
#include "HousingTutorialSession.h"
//WowCommunity
void GameObjectTemplate::InitializeQueryData()
@@ -3485,7 +3487,10 @@ void GameObject::Use(Unit* user, bool ignoreCastInProgress /*= false*/)
WorldPackets::GameObject::GameObjectInteraction gameObjectUILink;
gameObjectUILink.ObjectGUID = GetGUID();
switch (GetGOInfo()->UILink.UILinkType)
// Data7 supersedes the deprecated Data0 on current client templates.
uint32 const uiLinkType = GetGOInfo()->UILink.PlayerInteractionType == 70
? 4 : GetGOInfo()->UILink.UILinkType;
switch (uiLinkType)
{
case 0:
gameObjectUILink.InteractionType = PlayerInteractionType::AdventureJournal;
@@ -3499,6 +3504,22 @@ void GameObject::Use(Unit* user, bool ignoreCastInProgress /*= false*/)
case 3:
gameObjectUILink.InteractionType = PlayerInteractionType::ItemInteraction;
break;
case 4:
{
if (!sHousingMgr.IsPlayerOnHousingExterior(player) ||
GetMap()->GetHousingPlotIdByCornerstoneGuid(GetGUID()) < 0 ||
!player->IsWithinDistInMap(this, 10.0f))
return;
player->PlayerTalkClass->GetInteractionData().StartInteraction(
GetGUID(), static_cast<PlayerInteractionType>(70));
WorldPackets::NPC::NPCInteractionOpenResult open;
open.Npc = GetGUID();
open.InteractionType = static_cast<PlayerInteractionType>(70);
open.Success = true;
player->SendDirectMessage(open.Write());
Housing::CreditTutorialObjective(player, Housing::TutorialExplorePlotsCredit, GetGUID());
return;
}
default:
break;
}
+9 -7
View File
@@ -20,6 +20,8 @@
#include "HousingPackets.h"
#include "HousingMapContext.h"
#include "HousingDefines.h"
#include "HousingMgr.h"
#include "GameObject.h"
#include "HousingDynamicEntity.h"
#include "Log.h"
#include "Map.h"
@@ -457,17 +459,18 @@ void WorldSession::HandleNeighborhoodOpenCornerstoneUI(WorldPackets::Housing::Ne
return;
}
QueryResult plot = CharacterDatabase.PQuery(
"SELECT Cost FROM housing_plot_template WHERE PlotID = {} AND ExteriorMapID = {} LIMIT 1",
packet.PlotIndex, exteriorMapId);
if (!plot)
Housing::PlotState plot;
ObjectGuid const cornerstoneGuid = player->GetMap()->GetHousingPlotCornerstoneGuid(uint8(packet.PlotIndex));
GameObject* cornerstone = player->GetMap()->GetGameObject(cornerstoneGuid);
if (!cornerstone || !player->IsWithinDistInMap(cornerstone, 10.0f) ||
sHousingMgr.LoadPlot(player, uint8(packet.PlotIndex), plot) != Housing::Result::Success)
{
SendPacket(response.Write());
return;
}
response.Cost = plot->Fetch()[0].GetUInt64();
response.CornerstoneGUID = player->GetMap()->GetHousingPlotCornerstoneGuid(uint8(packet.PlotIndex));
response.Cost = plot.Cost;
response.CornerstoneGUID = cornerstoneGuid;
QueryResult neighborhood = CharacterDatabase.PQuery(
"SELECT Name FROM housing_neighborhood WHERE NeighborhoodGuid = '{}' LIMIT 1",
@@ -724,4 +727,3 @@ void WorldSession::ApplyHousingDecorMove(WorldPackets::Housing::HousingDecorMove
response.Result = HOUSING_RESULT_SUCCESS;
SendPacket(response.Write());
}
+74 -2
View File
@@ -6,12 +6,16 @@
#include "HousingMapContext.h"
#include "Map.h"
#include "DatabaseEnv.h"
#include "DB2Stores.h"
#include "GameObject.h"
#include "Group.h"
#include "Log.h"
#include "ObjectAccessor.h"
#include "Player.h"
#include "SocialMgr.h"
#include <mutex>
#include <cmath>
#include <limits>
namespace
{
@@ -19,6 +23,13 @@ namespace
// tables. Serialize reservation/commit transitions so duplicate client
// requests cannot both pass the pre-commit checks on one worldserver.
std::mutex HousingPlotTransactionMutex;
bool IsNearCornerstone(Player const* player, uint8 plotId)
{
GameObject* cornerstone = player->GetMap()->GetGameObject(
player->GetMap()->GetHousingPlotCornerstoneGuid(plotId));
return cornerstone && player->IsWithinDistInMap(cornerstone, 10.0f);
}
}
namespace Housing
@@ -47,6 +58,56 @@ namespace Housing
IsNeighborhoodGuid(player->GetMap()->GetHousingContextGuid());
}
bool Manager::InitializePlotTemplates(uint32 exteriorMapId) const
{
if (!IsExteriorMapId(exteriorMapId))
return false;
CharacterDatabaseTransaction transaction = CharacterDatabase.BeginTransaction();
uint32 count = 0;
for (NeighborhoodPlotEntry const* entry : sNeighborhoodPlotStore)
{
NeighborhoodMapEntry const* neighborhood = sNeighborhoodMapStore.LookupEntry(entry->NeighborhoodMapID);
if (!neighborhood || neighborhood->MapID != int32(exteriorMapId) ||
entry->PlotIndex < 0 || entry->PlotIndex > std::numeric_limits<uint8>::max() ||
entry->CornerstoneGameObjectID <= 0 || entry->Cost > uint64(std::numeric_limits<int64>::max()) ||
!std::isfinite(entry->TeleportPosition.X) || !std::isfinite(entry->TeleportPosition.Y) ||
!std::isfinite(entry->TeleportPosition.Z) || !std::isfinite(entry->TeleportFacing) ||
!std::isfinite(entry->CornerstonePosition.X) || !std::isfinite(entry->CornerstonePosition.Y) ||
!std::isfinite(entry->CornerstonePosition.Z) || !std::isfinite(entry->CornerstoneRotation.Z) ||
!std::isfinite(entry->HousePosition.X) || !std::isfinite(entry->HousePosition.Y) ||
!std::isfinite(entry->HousePosition.Z) || !std::isfinite(entry->HouseRotation.Z))
continue;
// Build 0 is the current DB2-derived fallback. Explicit verified SQL overrides
// have higher priority. Never import another core's coordinates or prices.
transaction->PAppend(
"INSERT INTO housing_plot_template (PlotID, ExteriorMapID, InteriorMapID, "
"DestinationX, DestinationY, DestinationZ, DestinationO, CornerstoneEntry, "
"CornerstonePositionX, CornerstonePositionY, CornerstonePositionZ, CornerstoneRotationZ, "
"HousePositionX, HousePositionY, HousePositionZ, HouseRotationZ, Cost, VerifiedBuild) "
"VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, 0) "
"ON DUPLICATE KEY UPDATE DestinationX = VALUES(DestinationX), DestinationY = VALUES(DestinationY), "
"DestinationZ = VALUES(DestinationZ), DestinationO = VALUES(DestinationO), "
"CornerstoneEntry = VALUES(CornerstoneEntry), CornerstonePositionX = VALUES(CornerstonePositionX), "
"CornerstonePositionY = VALUES(CornerstonePositionY), CornerstonePositionZ = VALUES(CornerstonePositionZ), "
"CornerstoneRotationZ = VALUES(CornerstoneRotationZ), HousePositionX = VALUES(HousePositionX), "
"HousePositionY = VALUES(HousePositionY), HousePositionZ = VALUES(HousePositionZ), "
"HouseRotationZ = VALUES(HouseRotationZ), Cost = VALUES(Cost)",
entry->PlotIndex, exteriorMapId, HouseInteriorMapId,
entry->TeleportPosition.X, entry->TeleportPosition.Y, entry->TeleportPosition.Z, entry->TeleportFacing,
entry->CornerstoneGameObjectID, entry->CornerstonePosition.X, entry->CornerstonePosition.Y,
entry->CornerstonePosition.Z, entry->CornerstoneRotation.Z, entry->HousePosition.X,
entry->HousePosition.Y, entry->HousePosition.Z, entry->HouseRotation.Z, entry->Cost);
++count;
}
if (count)
CharacterDatabase.DirectCommitTransaction(transaction);
return bool(CharacterDatabase.PQuery(
"SELECT 1 FROM housing_plot_template WHERE ExteriorMapID = {} LIMIT 1", exteriorMapId));
}
std::vector<HouseState> Manager::LoadOwnedHouses(Player const* player) const
{
std::vector<HouseState> houses;
@@ -250,6 +311,9 @@ namespace Housing
if (LoadPlot(player, plotId, plot) != Result::Success)
return Result::HouseNotFound;
if (reserve && !IsNearCornerstone(player, plotId))
return Result::PermissionDenied;
QueryResult neighborhood = CharacterDatabase.PQuery(
"SELECT 1 FROM housing_neighborhood WHERE NeighborhoodGuid = '{}' LIMIT 1",
neighborhoodGuid.ToString());
@@ -342,11 +406,18 @@ namespace Housing
return Result::HouseNotFound;
}
return CommitReservedPlot(player, neighborhoodGuid, plotId, state, chargedCost, moved);
return CommitReservedPlotLocked(player, neighborhoodGuid, plotId, state, chargedCost, moved);
}
Result Manager::CommitReservedPlot(Player* player, ObjectGuid const& neighborhoodGuid, uint8 plotId,
HouseState& state, uint64* chargedCost, bool* moved) const
{
std::scoped_lock lock(HousingPlotTransactionMutex);
return CommitReservedPlotLocked(player, neighborhoodGuid, plotId, state, chargedCost, moved);
}
Result Manager::CommitReservedPlotLocked(Player* player, ObjectGuid const& neighborhoodGuid, uint8 plotId,
HouseState& state, uint64* chargedCost, bool* moved) const
{
state = {};
if (chargedCost)
@@ -392,7 +463,8 @@ namespace Housing
}
uint64 const purchaseCost = isMove ? 0 : plot.Cost;
if (purchaseCost && player->GetMoney() < purchaseCost)
if (!IsNearCornerstone(player, plotId) || purchaseCost > uint64(std::numeric_limits<int64>::max()) ||
player->GetMoney() < purchaseCost)
return Result::PermissionDenied;
// Money and ownership must cross the durability boundary together. Debiting
+3
View File
@@ -21,6 +21,7 @@ namespace Housing
uint32 GetExteriorMapId(Player const* player) const;
bool IsPlayerOnHousingExterior(Player const* player, uint32* exteriorMapId = nullptr) const;
bool InitializePlotTemplates(uint32 exteriorMapId) const;
Result LoadOwnedHouse(Player const* player, ObjectGuid const& requestedHouse, HouseState& state) const;
Result LoadHouse(ObjectGuid const& houseGuid, HouseState& state, ObjectGuid* ownerGuid = nullptr) const;
@@ -39,6 +40,8 @@ namespace Housing
Result RelinquishOwnedHouse(Player const* player, ObjectGuid const& houseGuid, HouseState* previousState = nullptr) const;
private:
Result CommitReservedPlotLocked(Player* player, ObjectGuid const& neighborhoodGuid, uint8 plotId,
HouseState& state, uint64* chargedCost, bool* moved) const;
Manager() = default;
};
}
@@ -502,11 +502,7 @@ namespace Housing
response.Result = ToPacketResult(Result::HouseNotFound);
session->SendPacket(response.Write());
// Some retail clients display the tutorial button but reject it before
// sending CMSG_HOUSING_SVCS_START_TUTORIAL. The status request is the
// last verified dashboard interaction that reaches the server, so use
// it as a one-shot compatibility trigger for a new tutorial character.
TryStartTutorialFromDashboard(session);
// Status reads never start the tutorial. Use its opcode or the steward.
}
void HandlePlayerViewHousesByPlayer(WorldSession* session, WorldPackets::Housing::PlayerViewHousesByPlayer& packet)
@@ -926,11 +922,11 @@ namespace Housing
uint32 const guildId = neighborhoodFields[2].GetUInt32();
bool const isResident = response.OwnerGUID == player->GetGUID() ||
(guildId != 0 && guildId == player->GetGuildId()) || CharacterDatabase.PQuery(
"SELECT 1 FROM character_housing WHERE NeighborhoodGuid = '{}' AND OwnerGuid = {} LIMIT 1",
packet.NeighborhoodGUID.ToString(), player->GetGUID().GetCounter()) || CharacterDatabase.PQuery(
"SELECT 1 FROM character_housing_neighborhood_role WHERE NeighborhoodGuid = '{}' "
"AND CharacterGuid = {} AND Role = 1 LIMIT 1",
packet.NeighborhoodGUID.ToString(), player->GetGUID().GetCounter());
"SELECT 1 FROM character_housing WHERE NeighborhoodGuid = '{}' AND OwnerGuid = {} LIMIT 1",
packet.NeighborhoodGUID.ToString(), player->GetGUID().GetCounter()) || CharacterDatabase.PQuery(
"SELECT 1 FROM character_housing_neighborhood_role WHERE NeighborhoodGuid = '{}' "
"AND CharacterGuid = {} AND Role = 1 LIMIT 1",
packet.NeighborhoodGUID.ToString(), player->GetGUID().GetCounter());
if (!response.OwnerGUID.IsPlayer() || !isResident)
{
response.Result = ToPacketResult(Result::PermissionDenied);
@@ -1204,6 +1200,7 @@ namespace Housing
if (result == Result::Success)
{
response.House = ToPacketHouse(state);
CreditTutorialHouseOwnership(player);
if (moved && hadHouse)
{
SynchronizeExteriorMove(player, previousState, state);
+106 -181
View File
@@ -1,230 +1,155 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*/
#include "HousingTutorialSession.h"
#include "DatabaseEnv.h"
#include "HousingDefines.h"
#include "DB2Stores.h"
#include "HousingMgr.h"
#include "HousingMapContext.h"
#include "HousingPackets.h"
#include "HousingTutorialPackets.h"
#include "Log.h"
#include "Map.h"
#include "MapManager.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "SpellMgr.h"
#include "QuestDef.h"
#include "WorldSession.h"
#include <functional>
namespace Housing
{
namespace
{
constexpr uint32 HousingTutorialQuestId = 91863;
constexpr uint32 HousingDecorTutorialQuestId = 91968;
constexpr uint32 HousingPlacementTutorialQuestId = 91969;
constexpr uint32 HousingTutorialAllianceSpellId = 1258476;
constexpr uint32 HousingTutorialHordeSpellId = 1258484;
ObjectGuid EnsureTutorialNeighborhood(Player const* player)
{
if (!player)
return ObjectGuid::Empty;
HouseState house;
if (sHousingMgr.LoadOwnedHouse(player, ObjectGuid::Empty, house) == Result::Success)
return house.NeighborhoodGuid;
QueryResult existing = CharacterDatabase.PQuery(
"SELECT NeighborhoodGuid FROM housing_neighborhood WHERE OwnerGuid = {} "
"ORDER BY NeighborhoodGuid LIMIT 1", player->GetGUID().GetCounter());
if (existing)
{
ObjectGuid const neighborhood = ObjectGuid::FromString(existing->Fetch()[0].GetString());
if (IsNeighborhoodGuid(neighborhood))
return neighborhood;
}
uint32 const exteriorMapId = sHousingMgr.GetExteriorMapId(player);
uint64 const owner = player->GetGUID().GetCounter();
uint32 const mapId = sHousingMgr.GetExteriorMapId(player);
ObjectGuid const neighborhood = ObjectGuid::Create<HighGuid::Housing>(
4, exteriorMapId, uint32(owner & 0xFFFFFFFFu), owner);
4, mapId, uint32(owner & 0xFFFFFFFFu), owner);
if (!IsNeighborhoodGuid(neighborhood))
return ObjectGuid::Empty;
char const* name = player->GetTeamId() == TEAM_HORDE
? "Razorwind Shores" : "Founder''s Point";
char const* name = player->GetTeamId() == TEAM_HORDE ? "Razorwind Shores" : "Founder''s Point";
CharacterDatabaseTransaction transaction = CharacterDatabase.BeginTransaction();
transaction->PAppend(
"INSERT INTO housing_neighborhood (NeighborhoodGuid, OwnerGuid, Name, Type, Flags) "
"VALUES ('{}', {}, '{}', 0, 0) ON DUPLICATE KEY UPDATE OwnerGuid = VALUES(OwnerGuid)",
neighborhood.ToString(), owner, name);
"INSERT IGNORE INTO housing_neighborhood (NeighborhoodGuid, OwnerGuid, Name, Type, Flags) "
"VALUES ('{}', {}, '{}', 0, 0)", neighborhood.ToString(), owner, name);
transaction->PAppend(
"INSERT INTO character_housing_map_context (OwnerGuid, MapID, ContextGuid) VALUES ({}, {}, '{}') "
"ON DUPLICATE KEY UPDATE ContextGuid = VALUES(ContextGuid)",
owner, exteriorMapId, neighborhood.ToString());
// Never block the world-session thread waiting for the character database.
// The deterministic context GUID is valid immediately; both writes are
// idempotent and complete asynchronously before later persistence reads.
CharacterDatabase.CommitTransaction(transaction);
"ON DUPLICATE KEY UPDATE ContextGuid = VALUES(ContextGuid)", owner, mapId, neighborhood.ToString());
// Map creation reads this context immediately; an asynchronous commit races it.
CharacterDatabase.DirectCommitTransaction(transaction);
if (!CharacterDatabase.PQuery(
"SELECT 1 FROM housing_neighborhood n INNER JOIN character_housing_map_context c "
"ON c.ContextGuid = n.NeighborhoodGuid WHERE n.NeighborhoodGuid = '{}' "
"AND n.OwnerGuid = {} AND c.OwnerGuid = {} AND c.MapID = {} LIMIT 1",
neighborhood.ToString(), owner, owner, mapId))
{
TC_LOG_ERROR("housing", "Housing tutorial context could not be persisted for {}", player->GetGUID().ToString());
return ObjectGuid::Empty;
}
return neighborhood;
}
void ValidateTutorialData(Player const* player)
{
if (!sObjectMgr->GetQuestTemplate(HousingTutorialQuestId))
TC_LOG_ERROR("housing", "Housing tutorial quest {} is missing from the world database", HousingTutorialQuestId);
if (!sObjectMgr->GetQuestTemplate(HousingDecorTutorialQuestId))
TC_LOG_ERROR("housing", "Housing tutorial quest {} is missing from the world database", HousingDecorTutorialQuestId);
if (!sObjectMgr->GetQuestTemplate(HousingPlacementTutorialQuestId))
TC_LOG_ERROR("housing", "Housing tutorial quest {} is missing from the world database", HousingPlacementTutorialQuestId);
uint32 const transitionSpell = player && player->GetTeamId() == TEAM_HORDE
? HousingTutorialHordeSpellId : HousingTutorialAllianceSpellId;
if (!sSpellMgr->GetSpellInfo(transitionSpell, DIFFICULTY_NONE))
TC_LOG_ERROR("housing", "Housing tutorial transition spell {} is missing", transitionSpell);
}
bool TeleportToTutorialExterior(Player* player, ObjectGuid const& neighborhood)
{
if (!player)
return false;
uint32 const exteriorMapId = sHousingMgr.GetExteriorMapId(player);
QueryResult plotResult = CharacterDatabase.PQuery(
"SELECT PlotID FROM housing_plot_template WHERE ExteriorMapID = {} "
"ORDER BY PlotID, VerifiedBuild DESC LIMIT 1",
exteriorMapId);
if (!plotResult)
{
TC_LOG_ERROR("housing", "Housing tutorial has no plot template for exterior map {}", exteriorMapId);
return false;
}
uint8 const plotId = plotResult->Fetch()[0].GetUInt8();
PlotState plot;
if (sHousingMgr.LoadPlot(player, plotId, plot) != Result::Success)
{
TC_LOG_ERROR("housing", "Housing tutorial plot {} could not be loaded for player {}",
plotId, player->GetGUID().ToString());
return false;
}
return TeleportToMapContext(player, plot.ExteriorMapId, neighborhood,
plot.DestinationX, plot.DestinationY, plot.DestinationZ, plot.DestinationO);
}
}
static void StartTutorial(WorldSession* session, bool allowDirectTeleport)
bool StartTutorial(WorldSession* session)
{
if (!session)
return;
Player* player = session ? session->GetPlayer() : nullptr;
if (!player || !player->IsInWorld() || player->IsBeingTeleported())
return false;
Player* player = session->GetPlayer();
if (!player || !player->IsInWorld())
return;
auto unavailable = [session]()
{
session->SendNotification("The housing tutorial is currently unavailable.");
return false;
};
ValidateTutorialData(player);
Quest const* quest = sObjectMgr->GetQuestTemplate(TutorialQuestId);
if (!quest)
{
TC_LOG_ERROR("housing", "Housing tutorial quest {} is missing from world data", TutorialQuestId);
return unavailable();
}
if (player->GetQuestStatus(TutorialQuestId) == QUEST_STATUS_NONE &&
(!player->CanTakeQuest(quest, true) || !player->CanAddQuest(quest, true)))
return unavailable();
uint32 const mapId = sHousingMgr.GetExteriorMapId(player);
NeighborhoodMapEntry const* destination = nullptr;
for (NeighborhoodMapEntry const* entry : sNeighborhoodMapStore)
if (entry->MapID == int32(mapId))
{
destination = entry;
break;
}
if (!destination || !MapManager::IsValidMapCoord(mapId,
destination->Position.X, destination->Position.Y, destination->Position.Z, destination->EntryRotation) ||
!sHousingMgr.InitializePlotTemplates(mapId))
{
TC_LOG_ERROR("housing", "Cannot start tutorial: valid NeighborhoodMap/NeighborhoodPlot data missing for map {}", mapId);
return unavailable();
}
ObjectGuid const neighborhood = EnsureTutorialNeighborhood(player);
if (neighborhood.IsEmpty())
{
TC_LOG_ERROR("housing", "Cannot start Housing tutorial for player {}: neighborhood context creation failed",
player->GetGUID().ToString());
return;
}
return unavailable();
if (!sHousingMgr.IsPlayerOnHousingExterior(player) || player->GetMap()->GetHousingContextGuid() != neighborhood)
if (!TeleportToMapContext(player, mapId, neighborhood, destination->Position.X,
destination->Position.Y, destination->Position.Z, destination->EntryRotation))
return unavailable();
// Explicitly clear any stale dashboard house context before starting the
// no-house tutorial flow. This is a protocol state update, not the final
// tutorial operation result.
WorldPackets::Housing::HouseStatusResponse houseStatus;
houseStatus.Result = uint8(Result::HouseNotFound);
session->SendPacket(houseStatus.Write());
if (Quest const* quest = sObjectMgr->GetQuestTemplate(HousingTutorialQuestId))
{
if (player->GetQuestStatus(HousingTutorialQuestId) == QUEST_STATUS_NONE && player->CanAddQuest(quest, true))
{
player->AddQuestAndCheckCompletion(quest, nullptr);
TC_LOG_INFO("housing", "Started Housing tutorial quest {} for player {}",
HousingTutorialQuestId, player->GetGUID().ToString());
}
}
// Prefer the retail transition. Creating/loading a HousingMap directly from
// HouseStatus blocks the world thread when the client-data map resources are
// missing or incomplete (observed for map 2953), which trips FreezeDetector.
uint32 const spellId = player->GetTeamId() == TEAM_HORDE
? HousingTutorialHordeSpellId
: HousingTutorialAllianceSpellId;
if (sSpellMgr->GetSpellInfo(spellId, DIFFICULTY_NONE))
{
TC_LOG_INFO("housing", "Starting retail Housing tutorial spell {} for player {} (exterior map {})",
spellId, player->GetGUID().ToString(), sHousingMgr.GetExteriorMapId(player));
player->CastSpell(player, spellId, false);
return;
}
if (!allowDirectTeleport)
{
TC_LOG_ERROR("housing",
"Cannot transition Housing tutorial for player {}: retail spell {} is missing; "
"direct dashboard teleport was suppressed to protect the world thread",
player->GetGUID().ToString(), spellId);
return;
}
// Only an explicit tutorial opcode may use the compatibility teleport.
// It is never invoked from the automatic HouseStatus fallback.
if (TeleportToTutorialExterior(player, neighborhood))
{
TC_LOG_INFO("housing", "Started Housing tutorial for player {} on exterior map {}",
player->GetGUID().ToString(), sHousingMgr.GetExteriorMapId(player));
return;
}
TC_LOG_ERROR("housing", "Cannot start Housing tutorial for player {}: transition spell {} "
"is missing and the direct exterior teleport failed",
player->GetGUID().ToString(), spellId);
}
bool TryStartTutorialFromDashboard(WorldSession* session)
{
Player* player = session ? session->GetPlayer() : nullptr;
if (!player || !player->IsInWorld() || sHousingMgr.IsPlayerOnHousingExterior(player))
return false;
// HouseStatus runs on the world thread. Never create a HousingMap, cast a
// transition spell, or perform synchronous character-database reads here:
// both observed transition paths can block map/vmap loading until the
// FreezeDetector terminates the server.
if (player->GetQuestStatus(HousingTutorialQuestId) != QUEST_STATUS_NONE)
return false;
Quest const* quest = sObjectMgr->GetQuestTemplate(HousingTutorialQuestId);
if (!quest || !player->CanAddQuest(quest, true))
{
TC_LOG_ERROR("housing",
"Cannot arm Housing tutorial fallback for player {}: quest {} is missing or cannot be added",
player->GetGUID().ToString(), HousingTutorialQuestId);
return false;
}
player->AddQuestAndCheckCompletion(quest, nullptr);
TC_LOG_INFO("housing",
"Armed Housing tutorial quest {} from dashboard for player {}; map transition suppressed",
HousingTutorialQuestId, player->GetGUID().ToString());
if (player->GetQuestStatus(TutorialQuestId) == QUEST_STATUS_NONE)
player->AddQuestAndCheckCompletion(quest, nullptr);
TC_LOG_INFO("housing", "Started housing tutorial for {} in neighborhood {}",
player->GetGUID().ToString(), neighborhood.ToString());
return true;
}
void HandleStartTutorial(WorldSession* session,
WorldPackets::Housing::HousingSvcsStartTutorial& /*packet*/)
void CreditTutorialObjective(Player* player, uint32 credit, ObjectGuid const& source)
{
if (Player* player = session ? session->GetPlayer() : nullptr)
TC_LOG_INFO("housing", "Received Housing Start Tutorial request for player {} on map {}",
player->GetGUID().ToString(), player->GetMapId());
if (!sHousingMgr.IsPlayerOnHousingExterior(player) || player->GetQuestStatus(TutorialQuestId) != QUEST_STATUS_INCOMPLETE)
return;
std::function<bool(QuestObjective const*)> const filter = [](QuestObjective const* objective)
{
return objective->QuestID == TutorialQuestId;
};
player->UpdateQuestObjectiveProgress(QUEST_OBJECTIVE_MONSTER, int32(credit), 1, source, nullptr, &filter);
}
StartTutorial(session, true);
void CreditTutorialHouseOwnership(Player* player)
{
if (!sHousingMgr.IsPlayerOnHousingExterior(player) || player->GetQuestStatus(TutorialQuestId) != QUEST_STATUS_INCOMPLETE)
return;
HouseState house;
if (sHousingMgr.LoadOwnedHouse(player, ObjectGuid::Empty, house) != Result::Success ||
house.NeighborhoodGuid != player->GetMap()->GetHousingContextGuid())
return;
Quest const* quest = sObjectMgr->GetQuestTemplate(TutorialQuestId);
if (!quest)
return;
// Reconcile only ownership objectives from the durable house row, including
// a disconnect after purchase but before the quest progress was saved.
for (QuestObjective const& objective : quest->GetObjectives())
{
if (objective.Type != QUEST_OBJECTIVE_MONSTER || objective.ObjectID != int32(TutorialAcquireHomeCredit))
continue;
int32 const remaining = objective.Amount - player->GetQuestObjectiveData(objective);
if (remaining <= 0)
continue;
std::function<bool(QuestObjective const*)> const filter = [&objective](QuestObjective const* candidate)
{
return candidate->QuestID == TutorialQuestId && candidate->ID == objective.ID;
};
player->UpdateQuestObjectiveProgress(QUEST_OBJECTIVE_MONSTER, int32(TutorialAcquireHomeCredit),
remaining, house.HouseGuid, nullptr, &filter);
}
}
void HandleStartTutorial(WorldSession* session, WorldPackets::Housing::HousingSvcsStartTutorial& /*packet*/)
{
StartTutorial(session);
}
}
@@ -1,28 +1,24 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*/
#ifndef TRINITYCORE_HOUSING_TUTORIAL_SESSION_H
#define TRINITYCORE_HOUSING_TUTORIAL_SESSION_H
#include "Define.h"
#include "ObjectGuid.h"
class Player;
class WorldSession;
namespace WorldPackets::Housing
{
class HousingSvcsStartTutorial;
}
namespace WorldPackets::Housing { class HousingSvcsStartTutorial; }
namespace Housing
{
// Compatibility path for clients that render the tutorial button but block
// CMSG_HOUSING_SVCS_START_TUTORIAL locally. Returns true when the tutorial
// quest was armed from the dashboard's no-house status request.
TC_GAME_API bool TryStartTutorialFromDashboard(WorldSession* session);
TC_GAME_API void HandleStartTutorial(WorldSession* session,
WorldPackets::Housing::HousingSvcsStartTutorial& packet);
constexpr uint32 TutorialQuestId = 91863;
constexpr uint32 TutorialGreetStewardCredit = 249851;
constexpr uint32 TutorialJoinCredit = 248857;
constexpr uint32 TutorialExplorePlotsCredit = 248858;
constexpr uint32 TutorialAcquireHomeCredit = 249093;
// Explicit actions only: status queries must not accept quests or teleport.
TC_GAME_API bool StartTutorial(WorldSession* session);
TC_GAME_API void CreditTutorialObjective(Player* player, uint32 credit, ObjectGuid const& source);
TC_GAME_API void CreditTutorialHouseOwnership(Player* player);
TC_GAME_API void HandleStartTutorial(WorldSession* session, WorldPackets::Housing::HousingSvcsStartTutorial& packet);
}
#endif // TRINITYCORE_HOUSING_TUTORIAL_SESSION_H
+8 -3
View File
@@ -4377,8 +4377,10 @@ void Map::SpawnHousingPlotCornerstones()
QueryResult result = CharacterDatabase.PQuery(
"SELECT PlotID, CornerstoneEntry, CornerstonePositionX, CornerstonePositionY, "
"CornerstonePositionZ, CornerstoneRotationZ FROM housing_plot_template "
"WHERE ExteriorMapID = {} ORDER BY PlotID", GetId());
"CornerstonePositionZ, CornerstoneRotationZ FROM housing_plot_template p "
"WHERE ExteriorMapID = {} AND VerifiedBuild = (SELECT MAX(v.VerifiedBuild) "
"FROM housing_plot_template v WHERE v.ExteriorMapID = p.ExteriorMapID AND v.PlotID = p.PlotID) "
"ORDER BY PlotID", GetId());
if (!result)
{
TC_LOG_ERROR("housing", "No housing plot templates found for map {}", GetId());
@@ -4498,13 +4500,16 @@ void Map::RefreshHousingPlotHouseDoor(uint8 plotId)
"SELECT p.DoorEntry, p.HousePositionX, p.HousePositionY, p.HousePositionZ, p.HouseRotationZ "
"FROM housing_plot_template p WHERE p.ExteriorMapID = {} AND p.PlotID = {} "
"AND EXISTS (SELECT 1 FROM character_housing h WHERE h.PlotID = p.PlotID "
"AND h.ExteriorMapID = p.ExteriorMapID AND h.NeighborhoodGuid = '{}') LIMIT 1",
"AND h.ExteriorMapID = p.ExteriorMapID AND h.NeighborhoodGuid = '{}') "
"ORDER BY p.VerifiedBuild DESC LIMIT 1",
GetId(), uint32(plotId), _housingContextGuid.ToString());
if (!result)
return;
Field* fields = result->Fetch();
uint32 const entry = fields[0].GetUInt32();
if (!entry)
return; // Door data is optional for the quest/purchase milestone.
GameObjectTemplate const* doorTemplate = entry ? sObjectMgr->GetGameObjectTemplate(entry) : nullptr;
if (!doorTemplate)
{
+42 -131
View File
@@ -14,159 +14,70 @@
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ScriptMgr.h"
#include "CreatureAI.h"
#include "GossipDef.h"
#include "HousingMgr.h"
#include "HousingTutorialSession.h"
#include "Log.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "ScriptedGossip.h"
#include "SpellMgr.h"
enum HousingTutorialData
namespace
{
// Quest IDs
QUEST_MY_FIRST_HOME = 91863,
constexpr uint32 StartTutorialAction = 1000;
constexpr uint32 JoinNeighborhoodAction = 1001;
bool IsFactionSteward(Player const* player, Creature const* creature)
{
return creature->GetEntry() == (player->GetTeamId() == TEAM_HORDE ? 233708u : 233063u);
}
}
// Quest: "My First Home" (91863) kill credit NPCs
// These IDs correspond to quest_objectives.ObjectID entries for quest 91863.
// Each KilledMonsterCredit() call increments the counter for that objective.
NPC_KILL_CREDIT_GREET_STEWARD = 249851, // objective: greet the steward (Amount 1)
NPC_KILL_CREDIT_ASK_STEWARD = 248857, // objective: ask steward to join (Amount 1)
NPC_KILL_CREDIT_EXPLORE_PLOTS = 248858, // objective: explore available plots (Amount 1)
NPC_KILL_CREDIT_VISIT_LOCATIONS = 248860, // objective: visit neighborhood locations (Amount 6)
NPC_KILL_CREDIT_ACQUIRE_HOME = 249093, // objectives: acquire a home (Amount 2+1 = 3 total)
// Tutorial transition spells (retail fallback)
HOUSING_TUTORIAL_ALLIANCE_SPELL = 1258476,
HOUSING_TUTORIAL_HORDE_SPELL = 1258484,
};
// Lyssabel Dawnpetal (233063) / Tocho (233708) - Housing tutorial steward NPCs.
// When the player interacts with the steward during the "My First Home" quest (91863),
// the gossip grants quest kill credits for greeting the steward and asking them to join.
// If the player has not yet started the tutorial, the NPC starts it for them.
struct npc_housing_steward : public CreatureAI
{
npc_housing_steward(Creature* creature) : CreatureAI(creature) {}
using CreatureAI::CreatureAI;
void UpdateAI(uint32 /*diff*/) override {}
bool StartTutorialForPlayer(Player* player)
{
Quest const* quest = sObjectMgr->GetQuestTemplate(QUEST_MY_FIRST_HOME);
if (!quest)
{
TC_LOG_ERROR("housing", "npc_housing_steward: Housing tutorial quest {} missing from world database",
QUEST_MY_FIRST_HOME);
return false;
}
if (player->GetQuestStatus(QUEST_MY_FIRST_HOME) == QUEST_STATUS_NONE
&& player->CanAddQuest(quest, true))
{
player->AddQuestAndCheckCompletion(quest, nullptr);
TC_LOG_INFO("housing", "npc_housing_steward: Started Housing tutorial quest {} for player {}",
QUEST_MY_FIRST_HOME, player->GetGUID().ToString());
}
uint32 const exteriorMapId = sHousingMgr.GetExteriorMapId(player);
QueryResult plotResult = CharacterDatabase.PQuery(
"SELECT PlotID FROM housing_plot_template WHERE ExteriorMapID = {} "
"ORDER BY PlotID, VerifiedBuild DESC LIMIT 1",
exteriorMapId);
if (plotResult)
{
uint8 const plotId = plotResult->Fetch()[0].GetUInt8();
Housing::PlotState plot;
if (sHousingMgr.LoadPlot(player, plotId, plot) == Housing::Result::Success)
{
if (player->TeleportTo(plot.ExteriorMapId, plot.DestinationX, plot.DestinationY,
plot.DestinationZ, plot.DestinationO))
{
TC_LOG_INFO("housing", "npc_housing_steward: Teleported player {} to tutorial plot on map {}",
player->GetGUID().ToString(), exteriorMapId);
return true;
}
}
}
uint32 const spellId = player->GetTeamId() == TEAM_HORDE
? HOUSING_TUTORIAL_HORDE_SPELL
: HOUSING_TUTORIAL_ALLIANCE_SPELL;
if (sSpellMgr->GetSpellInfo(spellId, DIFFICULTY_NONE))
{
TC_LOG_WARN("housing", "npc_housing_steward: Plot destination unavailable; casting retail spell {} for player {}",
spellId, player->GetGUID().ToString());
player->CastSpell(player, spellId, false);
return true;
}
TC_LOG_ERROR("housing", "npc_housing_steward: No tutorial transition available for player {}: "
"no plot data and spell {} missing", player->GetGUID().ToString(), spellId);
return false;
}
bool OnGossipHello(Player* player) override
{
// If the player has not started the Housing tutorial yet, start it now.
// This covers the case where the client never sends CMSG_HOUSING_SVCS_START_TUTORIAL.
if (player->GetQuestStatus(QUEST_MY_FIRST_HOME) == QUEST_STATUS_NONE)
bool const inNeighborhood = sHousingMgr.IsPlayerOnHousingExterior(player);
if (inNeighborhood && !IsFactionSteward(player, me))
return false;
if (inNeighborhood)
{
CloseGossipMenuFor(player);
StartTutorialForPlayer(player);
return true;
Housing::CreditTutorialObjective(player, Housing::TutorialGreetStewardCredit, me->GetGUID());
player->TalkedToCreature(me->GetEntry(), me->GetGUID());
Housing::CreditTutorialHouseOwnership(player);
if (player->GetQuestStatus(Housing::TutorialQuestId) != QUEST_STATUS_INCOMPLETE)
return false; // preserve normal quest acceptance and turn-in
}
// Grant "Greet the steward" kill credit (quest objective: MONSTER 249851)
player->KilledMonsterCredit(NPC_KILL_CREDIT_GREET_STEWARD);
InitGossipMenuFor(player, 0);
if (me->IsQuestGiver())
player->PrepareQuestMenu(me->GetGUID());
if (inNeighborhood)
AddGossipItemFor(player, GossipOptionNpc::None, "Ask the steward to become your neighbor.",
GOSSIP_SENDER_MAIN, JoinNeighborhoodAction);
else
AddGossipItemFor(player, GossipOptionNpc::None, "Start the housing tutorial.",
GOSSIP_SENDER_MAIN, StartTutorialAction);
SendGossipMenuFor(player, DEFAULT_GOSSIP_MESSAGE, me->GetGUID());
return true;
}
// Satisfy "Talk to Lyssabel/Tocho" objective (quest objective: TALKTO with NPC entry)
player->TalkedToCreature(me->GetEntry(), me->GetGUID());
// Grant all remaining kill credits for quest 91863 objectives that cannot be
// completed through normal gameplay in the repack (visit locations, explore plots,
// acquire home). This ensures the quest reaches QUEST_STATUS_COMPLETE so the
// player can turn it in via the normal quest reward flow.
if (player->GetQuestStatus(QUEST_MY_FIRST_HOME) == QUEST_STATUS_INCOMPLETE)
bool OnGossipSelect(Player* player, uint32 /*menuId*/, uint32 gossipListId) override
{
uint32 const action = GetGossipActionFor(player, gossipListId);
CloseGossipMenuFor(player);
if (action == StartTutorialAction && !sHousingMgr.IsPlayerOnHousingExterior(player))
Housing::StartTutorial(player->GetSession());
else if (action == JoinNeighborhoodAction && sHousingMgr.IsPlayerOnHousingExterior(player) &&
IsFactionSteward(player, me))
{
// Objective: ask steward to join (Amount 1)
player->KilledMonsterCredit(NPC_KILL_CREDIT_ASK_STEWARD);
// Objective: explore available plots (Amount 1)
player->KilledMonsterCredit(NPC_KILL_CREDIT_EXPLORE_PLOTS);
// Objective: visit neighborhood locations (Amount 6 - grant all at once)
for (uint32 i = 0; i < 6; ++i)
player->KilledMonsterCredit(NPC_KILL_CREDIT_VISIT_LOCATIONS);
// Objective: acquire a home (Amount 3 total across two objectives)
for (uint32 i = 0; i < 3; ++i)
player->KilledMonsterCredit(NPC_KILL_CREDIT_ACQUIRE_HOME);
// Objective: talk to the other faction's steward (233708 for Alliance / 233063 for Horde)
// The player can't visit the other faction's map, so grant both talkto credits.
if (player->GetTeamId() == TEAM_ALLIANCE)
player->TalkedToCreature(233708, ObjectGuid::Empty);
else
player->TalkedToCreature(233063, ObjectGuid::Empty);
TC_LOG_INFO("housing", "npc_housing_steward: Player {} granted all remaining quest {} credits via steward {}",
player->GetGUID().ToString(), QUEST_MY_FIRST_HOME, me->GetEntry());
Housing::CreditTutorialObjective(player, Housing::TutorialJoinCredit, me->GetGUID());
Housing::CreditTutorialHouseOwnership(player);
}
TC_LOG_DEBUG("housing", "npc_housing_steward: Player {} greeted steward {} (kill credit {}, talkto {})",
player->GetGUID().ToString(), me->GetEntry(), NPC_KILL_CREDIT_GREET_STEWARD, me->GetEntry());
// If the quest is now complete (or was already complete), return false to let the
// default QuestGiver pathway handle the turn-in (request items / offer reward).
// For any other quest status, also return false for default gossip handling.
return false;
return true;
}
};