Housing phase 1: quest access and durable house purchase
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user