From c67de7f095cd9e549e30ebded773888e3f9cdeab Mon Sep 17 00:00:00 2001 From: luis Date: Sat, 14 Mar 2026 13:10:10 -0300 Subject: [PATCH] world quest part 1 --- .../master/2026_03_14_01_characters.sql | 11 + .../world/master/2026_03_14_00_world.sql | 140 ++++ .../AI/CombatBehaviors/DispelCoordinator.cpp | 32 +- src/server/game/Handlers/HousingHandler.cpp | 6 +- .../game/Handlers/NeighborhoodHandler.cpp | 15 +- src/server/game/Housing/Housing.cpp | 39 +- src/server/game/Housing/HousingMap.cpp | 89 ++- src/server/game/Housing/HousingMap.h | 4 +- src/server/game/Quests/QuestDef.h | 2 + src/server/game/Quests/WorldQuestMgr.cpp | 658 ++++++++++++++++++ src/server/game/Quests/WorldQuestMgr.h | 194 ++++++ src/server/game/Server/Packets/MiscPackets.h | 16 + src/server/game/Server/Protocol/Opcodes.cpp | 4 +- src/server/game/World/World.cpp | 11 + 14 files changed, 1146 insertions(+), 75 deletions(-) create mode 100644 sql/updates/characters/master/2026_03_14_01_characters.sql create mode 100644 sql/updates/world/master/2026_03_14_00_world.sql create mode 100644 src/server/game/Quests/WorldQuestMgr.cpp create mode 100644 src/server/game/Quests/WorldQuestMgr.h diff --git a/sql/updates/characters/master/2026_03_14_01_characters.sql b/sql/updates/characters/master/2026_03_14_01_characters.sql new file mode 100644 index 000000000..d790378c5 --- /dev/null +++ b/sql/updates/characters/master/2026_03_14_01_characters.sql @@ -0,0 +1,11 @@ +DROP TABLE `world_quest`; +CREATE TABLE IF NOT EXISTS `world_quest` ( + `questId` INT UNSIGNED NOT NULL DEFAULT '0', + `rewardId` INT UNSIGNED NOT NULL DEFAULT '0', + `startTime` INT UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY (`questId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Create indexes for better performance +ALTER TABLE `world_quest` ADD INDEX `idx_world_quest_start_time` (`startTime`); +ALTER TABLE `world_quest` ADD INDEX `idx_world_quest_reward_id` (`rewardId`); diff --git a/sql/updates/world/master/2026_03_14_00_world.sql b/sql/updates/world/master/2026_03_14_00_world.sql new file mode 100644 index 000000000..16841b502 --- /dev/null +++ b/sql/updates/world/master/2026_03_14_00_world.sql @@ -0,0 +1,140 @@ +-- Conversation +DELETE FROM `conversation_template` WHERE `Id`=28287; +INSERT INTO `conversation_template` (`Id`, `FirstLineID`, `TextureKitId`, `VerifiedBuild`) VALUES +(28287, 77763, 0, 65893); + +UPDATE `conversation_template` SET `ScriptName` = 'conversation_after_vereesas_ceremony' WHERE `Id` = 28287; + +DELETE FROM `conversation_actors` WHERE (`ConversationId`=28287 AND `Idx` IN (1,0)); +INSERT INTO `conversation_actors` (`ConversationId`, `ConversationActorId`, `ConversationActorGuid`, `Idx`, `CreatureId`, `CreatureDisplayInfoId`, `NoActorObject`, `ActivePlayerObject`, `VerifiedBuild`) VALUES +(28287, 103076, 0, 1, 0, 0, 0, 0, 65893), -- Full: 0x2042613F00E3DC4001F632000012E1AB Creature/0 R4248/S128562 Map: 2552 (Khaz Algar (Surface)) Entry: 233329 (Silver Hand Avenger) Low: 1237419 +(28287, 103060, 10006960, 0, 0, 0, 0, 0, 65893); -- Full: 0x2042613F00E3D88001F63200001183E4 Creature/0 R4248/S128562 Map: 2552 (Khaz Algar (Surface)) Entry: 233314 (Vereesa Windrunner) Low: 1147876 + +DELETE FROM `conversation_line_template` WHERE `Id` IN (77764, 77763); +INSERT INTO `conversation_line_template` (`Id`, `UiCameraID`, `ActorIdx`, `Flags`, `ChatType`, `VerifiedBuild`) VALUES +(77764, 0, 1, 0, 0, 65893), +(77763, 0, 0, 0, 0, 65893); + +-- Condition +DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId`=26 AND `SourceGroup` = 25036 AND `SourceEntry` = 0); +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `Comment`) VALUE +(26, 25036, 0, 0, 0, 47, 0, 84997, 2|64, 0, 0, 'Apply Phase 25036 if Quest 84997 is complete|rewarded'), +(26, 25036, 0, 0, 0, 47, 0, 85001, 2|8|64, 0, 1, 'Apply Phase 25036 if Quest 84997 is not complete|in progress|rewarded'); + +-- Quest +DELETE FROM `quest_request_items` WHERE `ID` IN (85001 /*Blessings Be Upon You*/, 84998 /*Bringer of the Void*/); +INSERT INTO `quest_request_items` (`ID`, `EmoteOnComplete`, `EmoteOnIncomplete`, `EmoteOnCompleteDelay`, `EmoteOnIncompleteDelay`, `CompletionText`, `VerifiedBuild`) VALUES +(85001, 0, 5, 0, 0, 'These poor souls are imperiled through no fault of their own!', 65893), -- Blessings Be Upon You +(84998, 0, 0, 0, 0, 'Why does this... void creature want... us dead?', 65893); -- Bringer of the Void + +DELETE FROM `creature_queststarter` WHERE (`id`=233312 AND `quest`=85001) OR (`id`=233314 AND `quest`=84998); +INSERT INTO `creature_queststarter` (`id`, `quest`, `VerifiedBuild`) VALUES +(233312, 85001, 65893), -- Blessings Be Upon You offered by Arator +(233314, 84998, 65893); -- Bringer of the Void offered by Vereesa Windrunner + +DELETE FROM `creature_questender` WHERE (`id`=233329 AND `quest`=85001) OR (`id`=233314 AND `quest`=84998); +INSERT INTO `creature_questender` (`id`, `quest`, `VerifiedBuild`) VALUES +(233329, 85001, 65893), -- Blessings Be Upon You ended by Silver Hand Avenger +(233314, 84998, 65893); -- Bringer of the Void ended by Vereesa Windrunner + +DELETE FROM `quest_template_addon` WHERE `ID` IN (85001,84998); +INSERT INTO `quest_template_addon` (`ID`, `MaxLevel`, `AllowableClasses`, `SourceSpellID`, `PrevQuestID`, `NextQuestID`, `ExclusiveGroup`, `BreadcrumbForQuestId`, `RewardMailTemplateID`, `RewardMailDelay`, `RequiredSkillID`, `RequiredSkillPoints`, `RequiredMinRepFaction`, `RequiredMaxRepFaction`, `RequiredMinRepValue`, `RequiredMaxRepValue`, `ProvidedItemCount`, `SpecialFlags`, `ScriptName`) VALUES +(85001, 0, 0, 0, 84997, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ''), +(84998, 0, 0, 0, 84997, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ''); + +-- SpellArea +DELETE FROM `spell_area` WHERE `spell` = 470452 AND `area` = 15943; +INSERT INTO `spell_area` (`spell`, `area`, `quest_start`, `quest_end`, `aura_spell`, `racemask`, `gender`, `flags`, `quest_start_status`, `quest_end_status`) VALUES +(470452, 15943, 85001, 85002, 0, 0, 2, 3, 74, 11); + +-- Creature Summon data +DELETE FROM `creature_summoned_data` WHERE `CreatureID` = 233329; +INSERT INTO `creature_summoned_data` (`CreatureID`, `CreatureIDVisibleToSummoner`, `GroundMountDisplayID`, `FlyingMountDisplayID`, `DespawnOnQuestsRemoved`) VALUES +(233329, 233312, NULL, NULL, NULL); + + -- Arator smart ai +SET @ENTRY := 233312; +UPDATE `creature_template` SET `AIName` = 'SmartAI', `ScriptName` = '' WHERE `entry` = @ENTRY; +DELETE FROM `smart_scripts` WHERE `source_type` = 0 AND `entryOrGuid` = @ENTRY; +INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `action_param7`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`, `Difficulties`) VALUES +(@ENTRY, 0, 0, 0, 19, 0, 100, 0, 85001, 0, 0, 0, 0, 143, 28287, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 'On player accepted quest 85001 - Player who accepted quest: Start conversation (28287) Actors<> (first line 77763)', ''); + +DELETE FROM `conditions` WHERE `SourceTypeOrReferenceId` = 22 AND `SourceEntry` = 233312 AND `SourceId` = 0; +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `ConditionStringValue1`, `NegativeCondition`, `Comment`) VALUES +(22, 1, 233312, 0, 0, 47, 0, 84998, 8, 0, '', 0, 'Action invoker has Bringer of the Void (84998) in state incomplete'); + +-- Vereesa Windrunner smart ai +SET @ENTRY := 233314; +UPDATE `creature_template` SET `AIName` = 'SmartAI', `ScriptName` = '' WHERE `entry` = @ENTRY; +DELETE FROM `smart_scripts` WHERE `source_type` = 0 AND `entryOrGuid` = @ENTRY; +INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `action_param7`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`, `Difficulties`) VALUES +(@ENTRY, 0, 0, 0, 19, 0, 100, 0, 84998, 0, 0, 0, 0, 143, 28287, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 'On player accepted quest Bringer of the Void (84998) - Player who accepted quest: Start conversation (28287) Actors<> (first line 77763)', ''); + +DELETE FROM `conditions` WHERE `SourceTypeOrReferenceId` = 22 AND `SourceEntry` = 233314 AND `SourceId` = 0; +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `ConditionStringValue1`, `NegativeCondition`, `Comment`) VALUES +(22, 1, 233314, 0, 0, 47, 0, 85001, 8, 0, '', 0, 'Action invoker has Blessings Be Upon You (85001) in state incomplete'); + +-- Silver Hand Avenger smart ai +SET @ENTRY := 233329; +UPDATE `creature_template` SET `AIName` = 'SmartAI', `ScriptName` = '' WHERE `entry` = @ENTRY; +DELETE FROM `smart_scripts` WHERE `source_type` = 0 AND `entryOrGuid` = @ENTRY; +INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `action_param7`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`, `Difficulties`) VALUES +(@ENTRY, 0, 0, 0, 54, 0, 100, 0, 0, 0, 0, 0, 0, 85, 470456, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 'On just summoned - Self: Cast spell 470456 on self', ''); + +DELETE FROM `spell_script_names` WHERE `spell_id`=436854 AND `ScriptName`='spell_switch_flight'; +INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES (436854, 'spell_switch_flight'); + +UPDATE `creature_template` SET `ScriptName`='npc_scarlet_corpse_49340' WHERE `entry`=49340; + +-- ============================================================================= +-- Apply ONLY to the HOTFIXES database for TrinityCore master branch! +-- World Quests are handled via hotfixes DB (DB2/hotfix mirrors) in retail-like emulation. +-- +-- WARNING: DROPPING these tables will DELETE any existing data/hotfixes! +-- Only run if your hotfixes DB is for testing or empty. +-- For production: Use ALTER TABLE to add/modify columns instead. +-- +-- First apply latest official TDB_hotfixes_full_*.sql from TrinityCore releases. +-- Then run this as a custom update. +-- ============================================================================= + +-- World Quest base definitions +DROP TABLE IF EXISTS `world_quest`; +CREATE TABLE IF NOT EXISTS `world_quest` ( + `id` INT UNSIGNED NOT NULL DEFAULT '0', + `duration` INT UNSIGNED NOT NULL DEFAULT '0', + `variable` INT UNSIGNED NOT NULL DEFAULT '0', + `value` TINYINT UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='World Quest definitions (hotfix table)'; + +-- World Quest Rewards +DROP TABLE IF EXISTS `world_quest_reward`; +CREATE TABLE IF NOT EXISTS `world_quest_reward` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `QuestType` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT 'QuestInfoID from QuestInfo.db2', + `RewardType` TINYINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '0=Item, 1=Currency, 2=Gold, etc.', + `RewardID` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT 'ItemID, CurrencyID, or 0', + `RewardCount` INT UNSIGNED NOT NULL DEFAULT '0', + `RewardContext` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT 'Item context/bonus flags', + PRIMARY KEY (`id`), + KEY `QuestType` (`QuestType`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='World Quest reward definitions (hotfix table)'; + +-- World Quest Area/Zone Tasks (mapping) +DROP TABLE IF EXISTS `world_quest_area_task`; +CREATE TABLE IF NOT EXISTS `world_quest_area_task` ( + `questId` INT UNSIGNED NOT NULL DEFAULT '0', + `areaId` INT UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY (`questId`, `areaId`), + KEY `areaId` (`areaId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='World Quest to zone/area assignments'; + +-- Custom: Reward linking by Quest Info (not in official TC) +DROP TABLE IF EXISTS `world_quest_reward_by_quest_info`; +CREATE TABLE IF NOT EXISTS `world_quest_reward_by_quest_info` ( + `questInfoId` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT 'From QuestInfo.db2', + `rewardId` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT 'Link to world_quest_reward.id or direct', + PRIMARY KEY (`questInfoId`, `rewardId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Custom: Reward linking by quest type'; + diff --git a/src/modules/Playerbot/AI/CombatBehaviors/DispelCoordinator.cpp b/src/modules/Playerbot/AI/CombatBehaviors/DispelCoordinator.cpp index d85e1fc63..cf1eb5dc3 100644 --- a/src/modules/Playerbot/AI/CombatBehaviors/DispelCoordinator.cpp +++ b/src/modules/Playerbot/AI/CombatBehaviors/DispelCoordinator.cpp @@ -510,10 +510,13 @@ float DispelCoordinator::DebuffData::GetAdjustedPriority(Unit* target) const stackCount = aura->GetStackAmount(); // Count nearby allies and analyze clustering - for (GroupReference& itr : group->GetMembers()) + if (!group || !group->GetMembersCount()) + return priority; + + for (GroupReference const& itr : group->GetMembers()) { Player* member = itr.GetSource(); - if (!member || member == targetPlayer || member->isDead()) + if (!member || member == targetPlayer || member->isDead() || !member->IsInWorld()) continue; float distSq = target->GetExactDistSq(member); @@ -683,15 +686,20 @@ void DispelCoordinator::UpdateDispellerCapabilities() m_dispellers.clear(); - for (GroupReference& itr : m_group->GetMembers()) + // Additional validation to prevent crashes with corrupted group data + if (!m_group->GetMembersCount()) + return; + + for (GroupReference const& itr : m_group->GetMembers()) { Player* member = itr.GetSource(); - if (!member || member->isDead()) + if (!member || member->isDead() || !member->IsInWorld()) continue; DispellerCapability cap; cap.botGuid = member->GetGUID(); cap.botClass = static_cast(member->GetClass()); + cap.canDispel = GetClassDispelTypes(cap.botClass); if (cap.canDispel.empty()) @@ -1043,13 +1051,13 @@ bool DispelCoordinator::ExecutePurge() { ::std::vector debuffs; - if (!m_group) + if (!m_group || !m_group->GetMembersCount()) return debuffs; - for (GroupReference& itr : m_group->GetMembers()) + for (GroupReference const& itr : m_group->GetMembers()) { Player* member = itr.GetSource(); - if (!member || member->isDead()) + if (!member || member->isDead() || !member->IsInWorld()) continue; // Check all auras @@ -1201,10 +1209,10 @@ uint32 DispelCoordinator::GetNearbyAlliesCount(Unit* center, float radius) const return 0; uint32 count = 0; - for (GroupReference& itr : m_group->GetMembers()) + for (GroupReference const& itr : m_group->GetMembers()) { Player* member = itr.GetSource(); - if (!member || member == center || member->isDead()) + if (!member || member == center || member->isDead() || !member->IsInWorld()) continue; float radiusSq = radius * radius; @@ -1217,13 +1225,13 @@ uint32 DispelCoordinator::GetNearbyAlliesCount(Unit* center, float radius) const bool DispelCoordinator::IsTankTakingDamage() const { - if (!m_group) + if (!m_group || !m_group->GetMembersCount()) return false; - for (GroupReference& itr : m_group->GetMembers()) + for (GroupReference const& itr : m_group->GetMembers()) { Player* member = itr.GetSource(); - if (!member || !IsTank(member)) + if (!member || !IsTank(member) || !member->IsInWorld()) continue; // Simple check - tank below 70% health diff --git a/src/server/game/Handlers/HousingHandler.cpp b/src/server/game/Handlers/HousingHandler.cpp index 90f3f5ee1..23d28c50d 100644 --- a/src/server/game/Handlers/HousingHandler.cpp +++ b/src/server/game/Handlers/HousingHandler.cpp @@ -2546,14 +2546,16 @@ void WorldSession::HandleHousingSvcsNeighborhoodReservePlot(WorldPackets::Housin neighborhood->UpdatePlotHouseInfo(plotIndex, housing->GetHouseGuid(), GetBattlenetAccountGUID()); // Spawn house GO and MeshObjects + int32 extCompID = static_cast(housing->GetCoreExteriorComponentID()); + int32 wmoDataID = static_cast(housing->GetHouseType()); GameObject* houseGo = nullptr; if (housing->HasCustomPosition()) { Position customPos = housing->GetHousePosition(); - houseGo = housingMap->SpawnHouseForPlot(plotIndex, &customPos); + houseGo = housingMap->SpawnHouseForPlot(plotIndex, &customPos, extCompID, wmoDataID); } else - houseGo = housingMap->SpawnHouseForPlot(plotIndex); + houseGo = housingMap->SpawnHouseForPlot(plotIndex, nullptr, extCompID, wmoDataID); TC_LOG_ERROR("housing", "HandleHousingSvcsNeighborhoodReservePlot: SpawnHouseForPlot for plot {}: {}", plotIndex, houseGo ? houseGo->GetGUID().ToString() : "FAILED"); diff --git a/src/server/game/Handlers/NeighborhoodHandler.cpp b/src/server/game/Handlers/NeighborhoodHandler.cpp index 43751dec8..3d0b6e215 100644 --- a/src/server/game/Handlers/NeighborhoodHandler.cpp +++ b/src/server/game/Handlers/NeighborhoodHandler.cpp @@ -1255,10 +1255,13 @@ void WorldSession::HandleNeighborhoodBuyHouse(WorldPackets::Neighborhood::Neighb { housingMap->SetPlotOwnershipState(resolvedPlotIndex, true); - // Spawn the house structure GO at DB2 default position - // Uses sniff-verified defaults: ExteriorComponentID=141, WmoDataID=9 - TC_LOG_ERROR("housing", "HandleNeighborhoodBuyHouse: Calling SpawnHouseForPlot for plot {}", resolvedPlotIndex); - GameObject* houseGo = housingMap->SpawnHouseForPlot(resolvedPlotIndex); + // Spawn the house using the player's Housing data + Housing const* buyHousing = player->GetHousing(); + int32 buyExtCompID = buyHousing ? static_cast(buyHousing->GetCoreExteriorComponentID()) : 0; + int32 buyWmoDataID = buyHousing ? static_cast(buyHousing->GetHouseType()) : 0; + TC_LOG_ERROR("housing", "HandleNeighborhoodBuyHouse: Calling SpawnHouseForPlot for plot {} (extComp={}, wmoData={})", + resolvedPlotIndex, buyExtCompID, buyWmoDataID); + GameObject* houseGo = housingMap->SpawnHouseForPlot(resolvedPlotIndex, nullptr, buyExtCompID, buyWmoDataID); TC_LOG_ERROR("housing", "HandleNeighborhoodBuyHouse: SpawnHouseForPlot result: {}", houseGo ? houseGo->GetGUID().ToString() : "FAILED/NULL"); } @@ -1435,7 +1438,9 @@ void WorldSession::HandleNeighborhoodMoveHouse(WorldPackets::Neighborhood::Neigh fixtureOverrides.empty() ? nullptr : &fixtureOverrides); } else - housingMap->SpawnHouseForPlot(targetPlotIndex); + { + TC_LOG_ERROR("housing", "HandleNeighborhoodMoveHouse: No Housing object for player ? cannot spawn house at plot {}", targetPlotIndex); + } } if (Housing const* housing = player->GetHousing()) diff --git a/src/server/game/Housing/Housing.cpp b/src/server/game/Housing/Housing.cpp index 31c6d0547..bec5a4df2 100644 --- a/src/server/game/Housing/Housing.cpp +++ b/src/server/game/Housing/Housing.cpp @@ -1822,20 +1822,47 @@ std::unordered_map Housing::GetFixtureOverrideMap() const uint32 Housing::GetCoreExteriorComponentID() const { - // The core fixture is a BASE component set via SetCoreFixture (OptionId == 0). - // Only Type=9 (Base) components count ? roof/door/window core changes are - // handled as root overrides in GetFixtureOverrideMap() instead. + // The core fixture is the primary component set via SetCoreFixture (OptionId == 0). + // It can be any root type ? Base (9) for Alliance, or different types for Horde. for (auto const& [pointId, fixture] : _fixtures) { if (fixture.OptionId == 0) { ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(fixture.FixturePointId); - if (comp && comp->Type == 9) // Type 9 = Base + // Only return this fixture if it matches the current house type's WMO data. + // When the player switches house type, old fixtures from the previous type + // must not override the new type's default component. + if (comp && comp->ParentComponentID == 0 && (_houseType == 0 || comp->HouseExteriorWmoDataID == _houseType)) return fixture.FixturePointId; } } - // No explicit base fixture set ? return faction-appropriate default. - return (_houseType == 87) ? 3805 : 141; + // No explicit core fixture set ? find first default root component for this house's WMO data ID. + if (_houseType > 0) + { + auto const* roots = sHousingMgr.GetRootComponentsForWmoData(static_cast(_houseType)); + if (roots) + { + uint32 fallbackComp = 0; + for (uint32 compID : *roots) + { + ExteriorComponentEntry const* comp = sExteriorComponentStore.LookupEntry(compID); + if (!comp) + continue; + if (!fallbackComp) + fallbackComp = compID; + if (comp->Flags & 0x1) // IsDefault + return compID; + } + if (fallbackComp) + return fallbackComp; + } + TC_LOG_ERROR("housing", "Housing::GetCoreExteriorComponentID: No root component found for houseType={}", _houseType); + } + else + { + TC_LOG_ERROR("housing", "Housing::GetCoreExteriorComponentID: No fixtures and houseType=0 ? cannot determine base component"); + } + return 0; } HousingResult Housing::AddToCatalog(uint32 decorEntryId, uint8 sourceType, std::string sourceValue) diff --git a/src/server/game/Housing/HousingMap.cpp b/src/server/game/Housing/HousingMap.cpp index 5f77a7e6f..b8637ed88 100644 --- a/src/server/game/Housing/HousingMap.cpp +++ b/src/server/game/Housing/HousingMap.cpp @@ -366,21 +366,24 @@ void HousingMap::SpawnPlotGameObjects() plotIdx, plotInfo->OwnerGuid.ToString(), plot->HousePosition[0], plot->HousePosition[1], plot->HousePosition[2]); - // Try to get persisted position and fixture data from the player's Housing object (if online) + // Get exterior component and WMO data from the player's Housing object Housing* housing = GetHousingForPlayer(plotInfo->OwnerGuid); - int32 exteriorComponentID = 141; // Default: Stucco Base - int32 houseExteriorWmoDataID = 9; // Default: Human theme - if (housing) + if (!housing) { - uint32 coreComponent = housing->GetCoreExteriorComponentID(); - if (coreComponent) - exteriorComponentID = static_cast(coreComponent); - uint32 houseType = housing->GetHouseType(); - if (houseType) - houseExteriorWmoDataID = static_cast(houseType); - TC_LOG_DEBUG("housing", "HousingMap::SpawnPlotGameObjects: Plot {} using persisted fixture data: ExteriorComponentID={}, WmoDataID={}", - plotIdx, exteriorComponentID, houseExteriorWmoDataID); + TC_LOG_ERROR("housing", "HousingMap::SpawnPlotGameObjects: Plot {} owned by {} but Housing object not found ? cannot spawn house", + plotIdx, plotInfo->OwnerGuid.ToString()); + continue; } + int32 exteriorComponentID = static_cast(housing->GetCoreExteriorComponentID()); + int32 houseExteriorWmoDataID = static_cast(housing->GetHouseType()); + if (!exteriorComponentID || !houseExteriorWmoDataID) + { + TC_LOG_ERROR("housing", "HousingMap::SpawnPlotGameObjects: Plot {} has invalid data: ExteriorComponentID={}, WmoDataID={}", + plotIdx, exteriorComponentID, houseExteriorWmoDataID); + continue; + } + TC_LOG_DEBUG("housing", "HousingMap::SpawnPlotGameObjects: Plot {} using ExteriorComponentID={}, WmoDataID={}", + plotIdx, exteriorComponentID, houseExteriorWmoDataID); // Build fixture override map from player's saved fixture selections FixtureOverrideMap fixtureOverrides; @@ -658,33 +661,34 @@ bool HousingMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/) if (!alreadySpawned) { - // Read persisted fixture data from Housing - int32 exteriorComponentID = 141; // Default: Stucco Base - int32 houseExteriorWmoDataID = 9; // Default: Human theme - uint32 coreComponent = housing->GetCoreExteriorComponentID(); - if (coreComponent) - exteriorComponentID = static_cast(coreComponent); - uint32 houseType = housing->GetHouseType(); - if (houseType) - houseExteriorWmoDataID = static_cast(houseType); - - TC_LOG_DEBUG("housing", "HousingMap::AddPlayerToMap: Plot {} spawning house with ExteriorComponentID={}, WmoDataID={}", - plotIdx, exteriorComponentID, houseExteriorWmoDataID); - - auto fixtureOverrides = housing->GetFixtureOverrideMap(); - FixtureOverrideMap const* overridesPtr = fixtureOverrides.empty() ? nullptr : &fixtureOverrides; - - GameObject* go = nullptr; - if (housing->HasCustomPosition()) + // Read exterior data from Housing ? no hardcoded fallbacks + int32 exteriorComponentID = static_cast(housing->GetCoreExteriorComponentID()); + int32 houseExteriorWmoDataID = static_cast(housing->GetHouseType()); + if (!exteriorComponentID || !houseExteriorWmoDataID) { - Position customPos = housing->GetHousePosition(); - go = SpawnHouseForPlot(plotIdx, &customPos, exteriorComponentID, houseExteriorWmoDataID, overridesPtr); + TC_LOG_ERROR("housing", "HousingMap::AddPlayerToMap: Plot {} has invalid data: ExteriorComponentID={}, WmoDataID={} ? skipping spawn", + plotIdx, exteriorComponentID, houseExteriorWmoDataID); } else - go = SpawnHouseForPlot(plotIdx, nullptr, exteriorComponentID, houseExteriorWmoDataID, overridesPtr); + { + TC_LOG_DEBUG("housing", "HousingMap::AddPlayerToMap: Plot {} spawning house with ExteriorComponentID={}, WmoDataID={}", + plotIdx, exteriorComponentID, houseExteriorWmoDataID); - TC_LOG_DEBUG("housing", "HousingMap::AddPlayerToMap: SpawnHouseForPlot result for plot {}: {}", - plotIdx, go ? go->GetGUID().ToString() : "FAILED"); + auto fixtureOverrides = housing->GetFixtureOverrideMap(); + FixtureOverrideMap const* overridesPtr = fixtureOverrides.empty() ? nullptr : &fixtureOverrides; + + GameObject* go = nullptr; + if (housing->HasCustomPosition()) + { + Position customPos = housing->GetHousePosition(); + go = SpawnHouseForPlot(plotIdx, &customPos, exteriorComponentID, houseExteriorWmoDataID, overridesPtr); + } + else + go = SpawnHouseForPlot(plotIdx, nullptr, exteriorComponentID, houseExteriorWmoDataID, overridesPtr); + + TC_LOG_DEBUG("housing", "HousingMap::AddPlayerToMap: SpawnHouseForPlot result for plot {}: {}", + plotIdx, go ? go->GetGUID().ToString() : "FAILED"); + } // else (valid exteriorComponentID && houseExteriorWmoDataID) } else { @@ -706,15 +710,8 @@ bool HousingMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/) int32 faction = _neighborhood ? _neighborhood->GetFactionRestriction() : NEIGHBORHOOD_FACTION_ALLIANCE; - // Use actual housing data, not hardcoded defaults - int32 lateExtCompID = 141; - int32 lateWmoDataID = 9; - uint32 lateCoreComp = housing->GetCoreExteriorComponentID(); - if (lateCoreComp) - lateExtCompID = static_cast(lateCoreComp); - uint32 lateHouseType = housing->GetHouseType(); - if (lateHouseType) - lateWmoDataID = static_cast(lateHouseType); + int32 lateExtCompID = static_cast(housing->GetCoreExteriorComponentID()); + int32 lateWmoDataID = static_cast(housing->GetHouseType()); SpawnFullHouseMeshObjects(plotIdx, pos, rot, housing->GetHouseGuid(), lateExtCompID, lateWmoDataID, faction); @@ -1508,8 +1505,8 @@ void HousingMap::RemovePlayerHousing(ObjectGuid playerGuid) // House Structure GO Management // ============================================================ -GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* customPos /*= nullptr*/, - int32 exteriorComponentID /*= 141*/, int32 houseExteriorWmoDataID /*= 9*/, +GameObject* HousingMap::SpawnHouseForPlot(uint8 plotIndex, Position const* customPos, + int32 exteriorComponentID, int32 houseExteriorWmoDataID, FixtureOverrideMap const* fixtureOverrides /*= nullptr*/) { if (!_neighborhood) diff --git a/src/server/game/Housing/HousingMap.h b/src/server/game/Housing/HousingMap.h index c0c886ef5..a5a869908 100644 --- a/src/server/game/Housing/HousingMap.h +++ b/src/server/game/Housing/HousingMap.h @@ -62,8 +62,8 @@ public: // House structure GO management // Sniff-verified defaults: ExteriorComponentID=141 (Stucco Base), HouseExteriorWmoDataID=9 (Human theme) - GameObject* SpawnHouseForPlot(uint8 plotIndex, Position const* customPos = nullptr, - int32 exteriorComponentID = 141, int32 houseExteriorWmoDataID = 9, + GameObject* SpawnHouseForPlot(uint8 plotIndex, Position const* customPos, + int32 exteriorComponentID, int32 houseExteriorWmoDataID, FixtureOverrideMap const* fixtureOverrides = nullptr); void DespawnHouseForPlot(uint8 plotIndex); GameObject* GetHouseGameObject(uint8 plotIndex); diff --git a/src/server/game/Quests/QuestDef.h b/src/server/game/Quests/QuestDef.h index 3f0a116ea..6fa07d78c 100644 --- a/src/server/game/Quests/QuestDef.h +++ b/src/server/game/Quests/QuestDef.h @@ -626,6 +626,8 @@ class TC_GAME_API Quest bool IsAutoPush() const { return HasFlagEx(QUEST_FLAGS_EX_AUTO_PUSH); } bool IsWorldQuest() const { return HasFlagEx(QUEST_FLAGS_EX_IS_WORLD_QUEST); } + bool IsEmissaryQuest() const { return _questInfoID == QUEST_INFO_EMISSARY_QUEST; } + bool IsCallingQuest() const { return _questInfoID == QUEST_INFO_CALLING_QUEST; } // Possibly deprecated flag bool IsUnavailable() const { return HasFlag(QUEST_FLAGS_DEPRECATED); } diff --git a/src/server/game/Quests/WorldQuestMgr.cpp b/src/server/game/Quests/WorldQuestMgr.cpp new file mode 100644 index 000000000..2a390f486 --- /dev/null +++ b/src/server/game/Quests/WorldQuestMgr.cpp @@ -0,0 +1,658 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * ... + */ + +#include "WorldQuestMgr.h" +#include "DatabaseEnv.h" +#include "DB2Stores.h" +#include "DB2Structure.h" +#include "ObjectMgr.h" +#include "Player.h" +#include "WorldPacket.h" +#include "WorldSession.h" +#include "Log.h" +#include "ItemBonusMgr.h" +#include "QuestPackets.h" +#include "WorldStatePackets.h" +#include "CriteriaHandler.h" +#include "GameTime.h" +#include "World.h" +#include "Optional.h" +#include +#include + +using namespace WorldPackets::Quest; + +WorldQuestTemplate::WorldQuestTemplate(uint32 questId, uint32 duration, uint32 variable, uint8 value) + : QuestId(questId), Duration(duration), Variable(variable), Value(value), + Expansion(0), Type(0), Timer(duration* MINUTE* IN_MILLISECONDS), + MinLevel(0), MaxLevel(MAX_LEVEL), Flags(0) +{ + Quest const* quest = sObjectMgr->GetQuestTemplate(questId); + if (quest) + { + Expansion = quest->GetExpansion(); + MaxLevel = quest->GetMaxLevel(); // this still exists + if (Optional tuning = sDB2Manager.GetContentTuningData(quest->GetContentTuningId(), {})) + { + MinLevel = tuning->MinLevel; + MaxLevel = tuning->MaxLevel; + } + + Optional tag = quest->GetQuestTag(); + if (tag) + { + switch (*tag) + { + case QuestTagType::Pvp: + Type = WORLD_QUEST_TYPE_PVP; + break; + case QuestTagType::PetBattle: + Type = WORLD_QUEST_TYPE_PET_BATTLE; + break; + case QuestTagType::Profession: + Type = WORLD_QUEST_TYPE_PROFESSION; + break; + case QuestTagType::Dungeon: + Type = WORLD_QUEST_TYPE_DUNGEON; + break; + case QuestTagType::Raid: + Type = WORLD_QUEST_TYPE_RAID; + break; + default: + Type = WORLD_QUEST_TYPE_GENERIC; + break; + } + } + else + { + Type = WORLD_QUEST_TYPE_GENERIC; + } + + if (quest->GetAreaGroupID() > 0) + { + AreaIDs = sDB2Manager.GetAreasForGroup(quest->GetAreaGroupID()); + } + else if (quest->GetZoneOrSort() > 0) + { + AreaIDs.push_back(quest->GetZoneOrSort()); + } + } +} + +WorldQuestMgr::~WorldQuestMgr() +{ + CleanWorldQuestTemplates(); +} + +void WorldQuestMgr::CleanWorldQuestTemplates() +{ + std::set toDeleteTemplates; + std::set toDeleteActiveTemplates; + + for (auto& expansionTemplates : _worldQuestTemplates) + for (auto& teamTemplates : expansionTemplates.second) + for (auto& itr : teamTemplates.second) + toDeleteTemplates.insert(itr.second); + + for (auto& expansionEmissaryTemplates : _emissaryWorldQuestTemplates) + for (auto& teamEmissaryTemplates : expansionEmissaryTemplates.second) + for (auto& itr : teamEmissaryTemplates.second) + toDeleteTemplates.insert(itr.second); + + for (auto& expansionWorldQuests : _activeWorldQuests) + for (auto& teamWorldQuest : expansionWorldQuests.second) + for (auto& activeWorldQuest : teamWorldQuest.second) + toDeleteActiveTemplates.insert(activeWorldQuest.second); + + for (auto* wqTemplate : toDeleteTemplates) + delete wqTemplate; + + for (auto* activeWQ : toDeleteActiveTemplates) + delete activeWQ; + + _worldQuestTemplates.clear(); + _emissaryWorldQuestTemplates.clear(); + _activeWorldQuests.clear(); +} + +// ... (AddWorldQuestTask, RemoveWorldQuestTask, GetWorldQuestTask, instance() ? unchanged) + +void WorldQuestMgr::LoadWorldQuestTemplates() +{ + CleanWorldQuestTemplates(); + + QueryResult result = WorldDatabase.Query("SELECT id, duration, variable, value FROM world_quest"); + if (!result) + return; + + do + { + Field* fields = result->Fetch(); + uint32 questId = fields[0].GetUInt32(); + Quest const* quest = sObjectMgr->GetQuestTemplate(questId); + if (!quest) + { + TC_LOG_DEBUG("sql.sql", "World Quest: {} exists but no quest template found. Skip.", questId); + continue; + } + + WorldQuestTemplate* wqTemplate = new WorldQuestTemplate(questId, fields[1].GetUInt32(), fields[2].GetUInt32(), fields[3].GetUInt8()); + AddWorldQuestTemplate(quest, wqTemplate); + + } while (result->NextRow()); + + if (_emissaryWorldQuestTemplates.size() < 3) + TC_LOG_DEBUG("sql.sql", "World Quest: Only {} emissary quests loaded, 3 needed.", _emissaryWorldQuestTemplates.size()); + + TC_LOG_INFO("server.loading", ">> Loaded {} World Quest Templates", + _worldQuestTemplates.size() + _emissaryWorldQuestTemplates.size()); +} + +void WorldQuestMgr::AddWorldQuestTemplate(Quest const* quest, WorldQuestTemplate* worldQuestTemplate) +{ + uint8 questTeamId = GetQuestTeamId(quest); + if (questTeamId == TEAM_NEUTRAL) + { + _worldQuestTemplates[quest->GetExpansion()][TEAM_ALLIANCE][quest->GetQuestId()] = worldQuestTemplate; + _worldQuestTemplates[quest->GetExpansion()][TEAM_HORDE][quest->GetQuestId()] = worldQuestTemplate; + + if (quest->IsEmissaryQuest()) + { + _emissaryWorldQuestTemplates[quest->GetExpansion()][TEAM_ALLIANCE][quest->GetQuestId()] = worldQuestTemplate; + _emissaryWorldQuestTemplates[quest->GetExpansion()][TEAM_HORDE][quest->GetQuestId()] = worldQuestTemplate; + } + if (quest->IsCallingQuest()) + { + _callingsWorldQuestTemplates[quest->GetExpansion()][TEAM_ALLIANCE][quest->GetQuestId()] = worldQuestTemplate; + _callingsWorldQuestTemplates[quest->GetExpansion()][TEAM_HORDE][quest->GetQuestId()] = worldQuestTemplate; + } + } + else + { + _worldQuestTemplates[quest->GetExpansion()][questTeamId][quest->GetQuestId()] = worldQuestTemplate; + + if (quest->IsEmissaryQuest()) + _emissaryWorldQuestTemplates[quest->GetExpansion()][questTeamId][quest->GetQuestId()] = worldQuestTemplate; + if (quest->IsCallingQuest()) + _callingsWorldQuestTemplates[quest->GetExpansion()][questTeamId][quest->GetQuestId()] = worldQuestTemplate; + } + + AddWorldQuestTask(quest); +} + +uint8 WorldQuestMgr::GetActiveEmissaryQuestsCount(uint8 expansion, uint8 teamId) +{ + // FIX: Only one definition ? removed the duplicate. + uint8 count = 0; + auto expansionItr = _emissaryWorldQuestTemplates.find(expansion); + if (expansionItr != _emissaryWorldQuestTemplates.end()) + { + auto teamItr = expansionItr->second.find(teamId); + if (teamItr != expansionItr->second.end()) + for (auto& questItr : teamItr->second) + if (IsQuestActive(questItr.first)) + ++count; + } + return count; +} + +uint32 WorldQuestMgr::GetActiveQuestsCount(uint8 expansion, uint8 teamId) +{ + // FIX: Only one definition ? removed the duplicate. + auto expansionItr = _activeWorldQuests.find(expansion); + if (expansionItr == _activeWorldQuests.end()) + return 0; + + auto teamItr = expansionItr->second.find(teamId); + if (teamItr == expansionItr->second.end()) + return 0; + + return static_cast(teamItr->second.size()); +} + +void WorldQuestMgr::Update() +{ + for (auto& expansionWorldQuest : _activeWorldQuests) + { + for (auto& teamWorldQuest : expansionWorldQuest.second) + { + auto& worldQuests = teamWorldQuest.second; + for (auto itr = worldQuests.begin(); itr != worldQuests.end();) + { + if (itr->second->GetRemainingTime() <= 0) + { + DisableQuest(itr->second, false); + itr = worldQuests.erase(itr); + } + else + ++itr; + } + } + } + + for (uint8 expansion : WorldQuestsExpansions) + { + for (uint8 teamId : WorldQuestsTeams) + { + if (_worldQuestTemplates[expansion][teamId].empty()) + continue; + + int32 questDiff = 18 - static_cast(GetActiveQuestsCount(expansion, teamId)) + + static_cast(GetActiveEmissaryQuestsCount(expansion, teamId)); + + if (questDiff > 0) + { + std::set inactive; + for (auto& it : _worldQuestTemplates[expansion][teamId]) + if (WorldQuestTemplate* wqt = it.second) + if (!IsQuestActive(wqt->QuestId)) + inactive.insert(wqt); + + while (questDiff > 0 && !inactive.empty()) + { + auto it = inactive.begin(); + std::advance(it, urand(0, static_cast(inactive.size() - 1))); + WorldQuestTemplate* wqt = *it; + ActivateQuest(wqt); + inactive.erase(wqt); + --questDiff; + } + } + + RefreshEmissaryQuests(); + } + } +} + +void WorldQuestMgr::RefreshEmissaryQuests() +{ + // FIX: Only one definition ? removed the duplicate stub. + for (uint8 expansion : WorldQuestsExpansions) + { + for (uint8 teamId : WorldQuestsTeams) + { + uint8 activeCount = GetActiveEmissaryQuestsCount(expansion, teamId); + uint8 requiredCount = 3; + + if (activeCount >= requiredCount) + continue; + + std::set inactive; + auto emissaryItr = _emissaryWorldQuestTemplates.find(expansion); + if (emissaryItr != _emissaryWorldQuestTemplates.end()) + { + auto teamItr = emissaryItr->second.find(teamId); + if (teamItr != emissaryItr->second.end()) + for (auto& questItr : teamItr->second) + { + // FIX: renamed local var from 'template' (reserved keyword) to 'wqTemplate' + WorldQuestTemplate* wqTemplate = questItr.second; + if (!IsQuestActive(wqTemplate->QuestId)) + inactive.insert(wqTemplate); + } + } + + while (activeCount < requiredCount && !inactive.empty()) + { + auto it = inactive.begin(); + std::advance(it, urand(0, static_cast(inactive.size() - 1))); + WorldQuestTemplate* wqTemplate = *it; + ActivateQuest(wqTemplate); + inactive.erase(wqTemplate); + ++activeCount; + } + } + } +} + +void WorldQuestMgr::AddEmissaryQuestsOnPlayerIfNeeded(Player* player) +{ + // FIX: Only one definition ? removed the duplicate stub. + if (!player) + return; + + uint8 playerTeam = player->GetTeamId(); + for (uint8 expansion : WorldQuestsExpansions) + { + auto emissaryItr = _emissaryWorldQuestTemplates.find(expansion); + if (emissaryItr == _emissaryWorldQuestTemplates.end()) + continue; + + auto teamItr = emissaryItr->second.find(playerTeam); + if (teamItr == emissaryItr->second.end()) + continue; + + for (auto& questItr : teamItr->second) + { + uint32 questId = questItr.first; + if (player->GetQuestStatus(questId) == QUEST_STATUS_NONE) + { + Quest const* quest = sObjectMgr->GetQuestTemplate(questId); + if (quest && player->CanTakeQuest(quest, false)) + player->AddQuest(quest, nullptr); + } + } + } +} + +std::vector WorldQuestMgr::GetCriteriasForQuest(uint32 quest_id) +{ + std::vector criterias; + + // Get criteria entries for the given quest + // This would typically query the criteria database + return criterias; +} + +void WorldQuestMgr::LoadWorldQuestRewardTemplates() +{ + QueryResult result = WorldDatabase.Query("SELECT id, questType, rewardType - 1 AS rewardType, rewardId, rewardCount, rewardContext FROM world_quest_reward"); + if (!result) + return; + + do + { + Field* fields = result->Fetch(); + + WorldQuestReward worldQuestReward = WorldQuestReward(); + worldQuestReward.Id = fields[0].GetUInt32(); + worldQuestReward.Type = static_cast(fields[2].GetUInt8()); + worldQuestReward.ItemBonusListID = fields[5].GetUInt32(); + + switch (worldQuestReward.Type) + { + case WORLD_QUEST_REWARD_ITEM: + worldQuestReward.Id = fields[3].GetUInt32(); + worldQuestReward.Count = fields[4].GetUInt32(); + break; + case WORLD_QUEST_REWARD_CURRENCY: + worldQuestReward.Id = fields[3].GetUInt32(); + worldQuestReward.Count = fields[4].GetUInt32(); + break; + case WORLD_QUEST_REWARD_GOLD: + worldQuestReward.Id = 0; + worldQuestReward.Count = fields[4].GetUInt32(); + break; + } + + _worldQuestRewards[worldQuestReward.Id].push_back(worldQuestReward); + _worldQuestRewardByQuestInfos[fields[1].GetUInt32()].push_back(worldQuestReward.Id); + + } while (result->NextRow()); + + TC_LOG_INFO("server.loading", ">> Loaded {} World Quest Reward Templates", _worldQuestRewards.size()); +} + +void WorldQuestMgr::LoadActiveWorldQuests() +{ + // not asynch, only at startup + QueryResult result = CharacterDatabase.Query("SELECT questId, rewardId, startTime FROM world_quest"); + + if (!result) + { + TC_LOG_INFO("sql.sql", "World Quest: There is no active world quests."); + return; + } + + do + { + Field* fields = result->Fetch(); + + uint32 questId = fields[0].GetUInt32(); + uint32 rewardId = fields[1].GetUInt32(); + uint32 startTime = fields[2].GetUInt32(); + + Quest const* quest = sObjectMgr->GetQuestTemplate(questId); + if (!quest) + { + TC_LOG_DEBUG("sql.sql", "World Quest: Quest {} has world quest duration but quest does not exist.", questId); + continue; + } + + WorldQuestTemplate* worldQuestTemplate = GetWorldQuestTemplate(questId); + if (!worldQuestTemplate) + { + TC_LOG_DEBUG("sql.sql", "World Quest: Quest {} has world quest duration but quest is not a world quest.", questId); + continue; + } + + uint8 questTeamId = GetQuestTeamId(quest); + if (questTeamId == TEAM_NEUTRAL) + { + _activeWorldQuests[quest->GetExpansion()][TEAM_ALLIANCE][worldQuestTemplate->QuestId] = new ActiveWorldQuest(questId, rewardId, startTime, worldQuestTemplate->Timer); + _activeWorldQuests[quest->GetExpansion()][TEAM_HORDE][worldQuestTemplate->QuestId] = new ActiveWorldQuest(questId, rewardId, startTime, worldQuestTemplate->Timer); + } + else + _activeWorldQuests[quest->GetExpansion()][questTeamId][worldQuestTemplate->QuestId] = new ActiveWorldQuest(questId, rewardId, startTime, worldQuestTemplate->Timer); + + } while (result->NextRow()); + + TC_LOG_INFO("server.loading", ">> Loaded {} Active World Quests", _activeWorldQuests.size()); +} + +void WorldQuestMgr::AddWorldQuestTask(Quest const* quest) +{ + if (quest->GetAreaGroupID() > 0) + { + std::vector areaGroupMembers = sDB2Manager.GetAreasForGroup(quest->GetAreaGroupID()); + for (uint32 areaId : areaGroupMembers) + _worldQuestAreaTaskStore[areaId].insert(quest); + } + else + { + WorldQuestTemplate const* worldQuest = GetWorldQuestTemplate(quest->GetQuestId()); + if (worldQuest && !worldQuest->AreaIDs.empty()) + { + for (uint32 areaId : worldQuest->AreaIDs) + _worldQuestAreaTaskStore[areaId].insert(quest); + return; + } + + for (QuestObjective const& obj : quest->GetObjectives()) + { + switch (obj.Type) + { + case QUEST_OBJECTIVE_MONSTER: + if (QueryResult result = WorldDatabase.PQuery("SELECT `areaId` FROM `creature` WHERE `id` = '{}' AND `zoneId` = '{}';", obj.ObjectID, quest->GetZoneOrSort())) + { + do + { + _worldQuestAreaTaskStore[(*result)[0].GetUInt16()].insert(quest); + } while (result->NextRow()); + } + break; + case QUEST_OBJECTIVE_GAMEOBJECT: + if (QueryResult result = WorldDatabase.PQuery("SELECT `areaId` FROM `gameobject` WHERE `id` = '{}' AND `zoneId` = '{}';", obj.ObjectID, quest->GetZoneOrSort())) + { + do + { + _worldQuestAreaTaskStore[(*result)[0].GetUInt16()].insert(quest); + } while (result->NextRow()); + } + break; + default: + break; + } + } + } +} + +void WorldQuestMgr::RemoveWorldQuestTask(Quest const* quest) +{ + if (!quest) + return; + + for (auto& taskSet : _worldQuestAreaTaskStore) + taskSet.second.erase(quest); +} + +std::set const* WorldQuestMgr::GetWorldQuestTask(uint32 areaId) const +{ + auto itr = _worldQuestAreaTaskStore.find(areaId); + if (itr != _worldQuestAreaTaskStore.end()) + return &itr->second; + + return nullptr; +} + +void WorldQuestMgr::ActivateQuest(WorldQuestTemplate* worldQuestTemplate) +{ + Quest const* quest = worldQuestTemplate->GetQuest(); + if (!quest) + return; + + // Shouldn't activate same quest twice + if (IsQuestActive(worldQuestTemplate->QuestId)) + return; + + uint32 questId = worldQuestTemplate->QuestId; + uint32 rewardId = GetRandomRewardForQuestType(quest->GetQuestInfoID()); + uint32 startTime = GameTime::GetGameTime(); + + uint8 questTeamId = GetQuestTeamId(quest); + if (questTeamId == TEAM_NEUTRAL) + { + _activeWorldQuests[quest->GetExpansion()][TEAM_ALLIANCE][worldQuestTemplate->QuestId] = new ActiveWorldQuest(questId, rewardId, startTime, worldQuestTemplate->Timer); + _activeWorldQuests[quest->GetExpansion()][TEAM_HORDE][worldQuestTemplate->QuestId] = new ActiveWorldQuest(questId, rewardId, startTime, worldQuestTemplate->Timer); + } + else + _activeWorldQuests[quest->GetExpansion()][questTeamId][worldQuestTemplate->QuestId] = new ActiveWorldQuest(questId, rewardId, startTime, worldQuestTemplate->Timer); + + // We add Emissary Quests to all eligible players + if (quest->IsEmissaryQuest()) + { + SessionMap const& smap = sWorld->GetAllSessions(); + for (SessionMap::const_iterator iter = smap.begin(); iter != smap.end(); ++iter) + if (Player* player = iter->second->GetPlayer()) + if (player->GetQuestStatus(worldQuestTemplate->QuestId) == QUEST_STATUS_NONE) + player->AddQuest(quest, nullptr); + } + + // Save to character database + CharacterDatabase.PExecute("REPLACE INTO world_quest (questId, rewardId, startTime) VALUES ({}, {}, {})", + questId, rewardId, startTime); + + AddWorldQuestTask(quest); +} + +void WorldQuestMgr::DisableQuest(ActiveWorldQuest* activeWorldQuest, bool deleteFromMap) +{ + if (!activeWorldQuest) + return; + + uint32 questId = activeWorldQuest->QuestId; + Quest const* quest = sObjectMgr->GetQuestTemplate(questId); + if (!quest) + return; + + // Remove from database + CharacterDatabase.PExecute("DELETE FROM world_quest WHERE questId = {}", questId); + + // Remove from map + if (deleteFromMap) + { + for (auto& expansionQuests : _activeWorldQuests) + { + for (auto& teamQuests : expansionQuests.second) + { + teamQuests.second.erase(questId); + } + } + } + + delete activeWorldQuest; +} + +bool WorldQuestMgr::IsQuestActive(uint32 questId) +{ + for (auto& expansionQuests : _activeWorldQuests) + { + for (auto& teamQuests : expansionQuests.second) + { + if (teamQuests.second.find(questId) != teamQuests.second.end()) + return true; + } + } + return false; +} + +WorldQuestTemplate* WorldQuestMgr::GetWorldQuestTemplate(uint32 questId) +{ + for (auto& expansionTemplates : _worldQuestTemplates) + { + for (auto& teamTemplates : expansionTemplates.second) + { + auto itr = teamTemplates.second.find(questId); + if (itr != teamTemplates.second.end()) + return itr->second; + } + } + + for (auto& expansionTemplates : _emissaryWorldQuestTemplates) + { + for (auto& teamTemplates : expansionTemplates.second) + { + auto itr = teamTemplates.second.find(questId); + if (itr != teamTemplates.second.end()) + return itr->second; + } + } + + return nullptr; +} + +ActiveWorldQuest* WorldQuestMgr::GetActiveWorldQuest(uint32 questId) +{ + for (auto& expansionQuests : _activeWorldQuests) + { + for (auto& teamQuests : expansionQuests.second) + { + auto itr = teamQuests.second.find(questId); + if (itr != teamQuests.second.end()) + return itr->second; + } + } + return nullptr; +} + +uint32 WorldQuestMgr::GetRandomRewardForQuestType(uint32 questType) +{ + auto rewardItr = _worldQuestRewardByQuestInfos.find(questType); + if (rewardItr == _worldQuestRewardByQuestInfos.end() || rewardItr->second.empty()) + return 0; + + // Select random reward ID from available rewards for this quest type + auto it = rewardItr->second.begin(); + std::advance(it, urand(0, rewardItr->second.size() - 1)); + return *it; +} + +std::vector WorldQuestMgr::GetRewardsForPlayerById(Player* /*player*/, uint32 rewardId) +{ + std::vector rewards; + + auto rewardItr = _worldQuestRewards.find(rewardId); + if (rewardItr != _worldQuestRewards.end()) + { + for (WorldQuestReward const& reward : rewardItr->second) + { + // Check if player is eligible for this reward + // This would typically involve level checks, class checks, etc. + rewards.push_back(&reward); + } + } + + return rewards; +} + +TeamId WorldQuestMgr::GetQuestTeamId(Quest const* quest) +{ + if (!quest) + return TEAM_NEUTRAL; + + // Simplified team detection - in a real implementation this would check quest flags + // For now, return neutral as default + return TEAM_NEUTRAL; +} diff --git a/src/server/game/Quests/WorldQuestMgr.h b/src/server/game/Quests/WorldQuestMgr.h new file mode 100644 index 000000000..b4f837b9a --- /dev/null +++ b/src/server/game/Quests/WorldQuestMgr.h @@ -0,0 +1,194 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef TRINITYCORE_WORLD_QUEST_MGR_H +#define TRINITYCORE_WORLD_QUEST_MGR_H + +#include "Common.h" +#include "QuestDef.h" +#include "GameTime.h" +#include +#include +#include + +struct WorldQuestTemplate; +struct ActiveWorldQuest; +struct WorldQuestReward; + +class WorldQuestMgr; + +typedef std::unordered_map>> WorldQuestTemplateMap; +typedef std::unordered_map>> ActiveWorldQuestMap; +typedef std::unordered_map> QuestAreaTaskMap; +typedef std::unordered_map> WorldQuestRewardMap; +typedef std::unordered_map> WorldQuestRewardByQuestInfoMap; + +enum WorldQuestRewardType +{ + WORLD_QUEST_REWARD_ITEM = 0, + WORLD_QUEST_REWARD_CURRENCY = 1, + WORLD_QUEST_REWARD_GOLD = 2, +}; + +enum WorldQuestType +{ + WORLD_QUEST_TYPE_PVP = 0, + WORLD_QUEST_TYPE_PET_BATTLE = 1, + WORLD_QUEST_TYPE_PROFESSION = 2, + WORLD_QUEST_TYPE_DUNGEON = 3, + WORLD_QUEST_TYPE_RAID = 4, + WORLD_QUEST_TYPE_WORLD_BOSS = 5, + WORLD_QUEST_TYPE_BOUNTY = 6, + WORLD_QUEST_TYPE_ASSIST = 7, + WORLD_QUEST_TYPE_ARTIFACT_POWER = 8, + WORLD_QUEST_TYPE_RESOURCE = 9, + WORLD_QUEST_TYPE_FACTION = 10, + WORLD_QUEST_TYPE_GENERIC = 11, +}; + +struct WorldQuestReward +{ + WorldQuestRewardType Type; + uint32 Id; + uint32 Count; + uint32 ItemBonusListID; +}; + +struct WorldQuestTemplate +{ + WorldQuestTemplate(uint32 questId, uint32 duration, uint32 variable, uint8 value); + + uint32 QuestId; + uint32 Duration; + uint32 Variable; + uint8 Value; + uint8 Expansion; + uint8 Type; + std::vector AreaIDs; + std::vector RewardIDs; + uint32 Timer; + uint32 MinLevel; + uint32 MaxLevel; + uint32 Flags; + + Quest const* GetQuest() const { return sObjectMgr->GetQuestTemplate(QuestId); } +}; + +struct ActiveWorldQuest +{ + ActiveWorldQuest(uint32 questId, uint32 rewardId, uint32 startTime, uint32 timer) + : QuestId(questId), RewardId(rewardId), StartTime(startTime), IsCompleted(false), + EndTime(startTime + timer) + { + } + + uint32 QuestId; + uint32 RewardId; + uint32 StartTime; + uint32 EndTime; + bool IsCompleted; + + uint32 GetRemainingTime() const + { + uint32 currentTime = GameTime::GetGameTime(); + if (currentTime >= EndTime) + return 0; + return (EndTime - currentTime) / IN_MILLISECONDS; + } +}; + +class TC_GAME_API WorldQuestMgr +{ +private: + WorldQuestMgr() = default; + +public: + ~WorldQuestMgr(); + + static WorldQuestMgr* instance() + { + static WorldQuestMgr instance; + return &instance; + } + + // World quest expansions and teams + const std::vector WorldQuestsExpansions = { EXPANSION_LEGION, EXPANSION_BATTLE_FOR_AZEROTH, EXPANSION_SHADOWLANDS, EXPANSION_DRAGONFLIGHT }; + const std::vector WorldQuestsTeams = { TEAM_ALLIANCE, TEAM_HORDE }; + + // Loading functions + void LoadWorldQuestTemplates(); + void LoadWorldQuestRewardTemplates(); + void AddWorldQuestTemplate(Quest const* quest, WorldQuestTemplate* worldQuestTemplate); + void LoadActiveWorldQuests(); + void Update(); + + // Cleanup functions + void CleanWorldQuestTemplates(); + + // World quest tasks + void AddWorldQuestTask(Quest const* quest); + void RemoveWorldQuestTask(Quest const* quest); + + // Criteria and emissary + std::vector GetCriteriasForQuest(uint32 quest_id); + void RefreshEmissaryQuests(); + void AddEmissaryQuestsOnPlayerIfNeeded(Player* player); + + // Quest activation/deactivation + void ActivateQuest(WorldQuestTemplate* worldQuestTemplate); + void DisableQuest(ActiveWorldQuest* activeWorldQuest, bool deleteFromMap = true); + + // Quest status (public - used by external code) + bool IsQuestActive(uint32 questId); + + // Rewards + void RewardQuestForPlayer(Player* player, uint32 questId); + + // Statistics (public - used by external code) + uint32 GetActiveQuestsCount(uint8 expansion, uint8 teamId); + uint8 GetActiveEmissaryQuestsCount(uint8 expansion, uint8 teamId); + + // Timer (public - used by external code) + uint32 GetTimerForQuest(uint32 questId); + + // Team (public - used by external code) + TeamId GetQuestTeamId(Quest const* quest); + WorldQuestTemplate* GetWorldQuestTemplate(uint32 questId); + +private: + // Methods that need access to private member variables + std::set const* GetWorldQuestTask(uint32 areaId) const; + + ActiveWorldQuest* GetActiveWorldQuest(uint32 questId); + uint32 GetRandomRewardForQuestType(uint32 questType); + std::vector GetRewardsForPlayerById(Player* player, uint32 rewardId); + + WorldQuestTemplateMap _worldQuestTemplates; + WorldQuestTemplateMap _emissaryWorldQuestTemplates; + WorldQuestTemplateMap _callingsWorldQuestTemplates; + QuestAreaTaskMap _worldQuestAreaTaskStore; + QuestAreaTaskMap _questAreaTaskStore; + + WorldQuestRewardMap _worldQuestRewards; + WorldQuestRewardByQuestInfoMap _worldQuestRewardByQuestInfos; + + ActiveWorldQuestMap _activeWorldQuests; +}; + +#define sWorldQuestMgr WorldQuestMgr::instance() + +#endif // TRINITYCORE_WORLD_QUEST_MGR_H diff --git a/src/server/game/Server/Packets/MiscPackets.h b/src/server/game/Server/Packets/MiscPackets.h index d3bfce98c..d44e10a8a 100644 --- a/src/server/game/Server/Packets/MiscPackets.h +++ b/src/server/game/Server/Packets/MiscPackets.h @@ -1189,6 +1189,22 @@ namespace WorldPackets uint8 RequestedPartyDelveTier = 0; uint8 StatusFlags = 0; }; + + class ShowDelvesDisplayUI final : public ServerPacket + { + public: + ShowDelvesDisplayUI() : ServerPacket(SMSG_SHOW_DELVES_DISPLAY_UI, 0) {} + + WorldPacket const* Write() override { return &_worldPacket; } + }; + + class ShowDelvesCompanionConfigurationUI final : public ServerPacket + { + public: + ShowDelvesCompanionConfigurationUI() : ServerPacket(SMSG_SHOW_DELVES_COMPANION_CONFIGURATION_UI, 0) {} + + WorldPacket const* Write() override { return &_worldPacket; } + }; //WowCommunity } } diff --git a/src/server/game/Server/Protocol/Opcodes.cpp b/src/server/game/Server/Protocol/Opcodes.cpp index 01f9d32f2..65d70c566 100644 --- a/src/server/game/Server/Protocol/Opcodes.cpp +++ b/src/server/game/Server/Protocol/Opcodes.cpp @@ -2442,8 +2442,8 @@ void OpcodeTable::InitializeServerOpcodes() DEFINE_SERVER_OPCODE_HANDLER(SMSG_SET_VEHICLE_REC_ID, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_SHADOWLANDS_CAPACITANCE_UPDATE, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); DEFINE_SERVER_OPCODE_HANDLER(SMSG_SHIPMENT_FACTION_UPDATE_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_SHOW_DELVES_COMPANION_CONFIGURATION_UI, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_SHOW_DELVES_DISPLAY_UI, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_SHOW_DELVES_COMPANION_CONFIGURATION_UI, STATUS_NEVER, CONNECTION_TYPE_REALM); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_SHOW_DELVES_DISPLAY_UI, STATUS_NEVER, CONNECTION_TYPE_REALM); DEFINE_SERVER_OPCODE_HANDLER(SMSG_SHOW_NEUTRAL_PLAYER_FACTION_SELECT_UI, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); DEFINE_SERVER_OPCODE_HANDLER(SMSG_SHOW_QUEST_COMPLETION_TEXT, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); DEFINE_SERVER_OPCODE_HANDLER(SMSG_SHOW_TAXI_NODES, STATUS_NEVER, CONNECTION_TYPE_REALM); diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index dc4ce6700..e16e46580 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -95,6 +95,7 @@ #include "SkillDiscovery.h" #include "SkillExtraItems.h" #include "SmartScriptMgr.h" +#include "WorldQuestMgr.h" #include "SpellMgr.h" #include "SupportMgr.h" #include "TaxiPathGraph.h" @@ -1617,6 +1618,11 @@ bool World::SetInitialWorldSettings() QuestMgr::Load(); sObjectMgr->LoadQuests(); // must be loaded after DBCs, creature_template, items, gameobject tables + TC_LOG_INFO("server.loading", "Loading World Quests..."); + sWorldQuestMgr->LoadWorldQuestTemplates(); + sWorldQuestMgr->LoadWorldQuestRewardTemplates(); + sWorldQuestMgr->LoadActiveWorldQuests(); + TC_LOG_INFO("server.loading", "Checking Quest Disables"); DisableMgr::CheckQuestDisables(); // must be after loading quests @@ -2419,6 +2425,11 @@ void World::Update(uint32 diff) sBattlefieldMgr->Update(diff); } + { + TC_METRIC_TIMER("world_update_time", TC_METRIC_TAG("type", "Update world quests")); + sWorldQuestMgr->Update(); + } + sInitiativeManager.Update(diff); sNeighborhoodMgr.Update(diff);