diff --git a/sql/updates/characters/master/2026_05_03_00_characters.sql b/sql/updates/characters/master/2026_05_03_00_characters.sql new file mode 100644 index 000000000..f1f5d7e6c --- /dev/null +++ b/sql/updates/characters/master/2026_05_03_00_characters.sql @@ -0,0 +1,330 @@ +-- +-- Table structure for table `character_garrison` +-- +DROP TABLE IF EXISTS `character_garrison`; +CREATE TABLE `character_garrison` ( + `guid` bigint unsigned NOT NULL, + `siteLevelId` int unsigned NOT NULL DEFAULT '0', + `followerActivationsRemainingToday` int unsigned NOT NULL DEFAULT '0', + `type` int unsigned NOT NULL DEFAULT '0', + `missionsStartedToday` int unsigned NOT NULL DEFAULT '0', + `lastMissionStartDay` int unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`guid`), + CONSTRAINT `fk_garrison_owner` FOREIGN KEY (`guid`) REFERENCES `characters` (`guid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `character_garrison_blueprints` +-- +DROP TABLE IF EXISTS `character_garrison_blueprints`; +CREATE TABLE `character_garrison_blueprints` ( + `guid` bigint unsigned NOT NULL, + `buildingId` int unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`guid`,`buildingId`), + CONSTRAINT `fk_blueprints_owner` FOREIGN KEY (`guid`) REFERENCES `characters` (`guid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `character_garrison_buildings` +-- +DROP TABLE IF EXISTS `character_garrison_buildings`; +CREATE TABLE `character_garrison_buildings` ( + `guid` bigint unsigned NOT NULL, + `plotInstanceId` int unsigned NOT NULL DEFAULT '0', + `buildingId` int unsigned NOT NULL DEFAULT '0', + `timeBuilt` bigint unsigned NOT NULL DEFAULT '0', + `active` tinyint unsigned NOT NULL DEFAULT '0', + `currentGarSpecId` int unsigned NOT NULL DEFAULT '0', + `timeSpecCooldown` bigint unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`guid`,`plotInstanceId`), + CONSTRAINT `fk_buildings_owner` FOREIGN KEY (`guid`) REFERENCES `characters` (`guid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `character_garrison_specializations` +-- +DROP TABLE IF EXISTS `character_garrison_specializations`; +CREATE TABLE `character_garrison_specializations` ( + `guid` bigint unsigned NOT NULL, + `specId` int unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`guid`,`specId`), + CONSTRAINT `fk_spec_owner` FOREIGN KEY (`guid`) REFERENCES `characters` (`guid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `character_garrison_follower_abilities` +-- (Must be dropped before followers due to FK constraint) +-- +DROP TABLE IF EXISTS `character_garrison_follower_abilities`; + +-- +-- Table structure for table `character_garrison_followers` +-- +DROP TABLE IF EXISTS `character_garrison_followers`; +CREATE TABLE `character_garrison_followers` ( + `dbId` bigint unsigned NOT NULL AUTO_INCREMENT, + `guid` bigint unsigned NOT NULL, + `followerId` int unsigned NOT NULL DEFAULT '0', + `quality` int unsigned NOT NULL DEFAULT '0', + `level` int unsigned NOT NULL DEFAULT '0', + `itemLevelWeapon` int unsigned NOT NULL DEFAULT '0', + `itemLevelArmor` int unsigned NOT NULL DEFAULT '0', + `xp` int unsigned NOT NULL DEFAULT '0', + `currentBuilding` int unsigned NOT NULL DEFAULT '0', + `currentMission` int unsigned NOT NULL DEFAULT '0', + `status` int unsigned NOT NULL DEFAULT '0', + `durability` int unsigned NOT NULL DEFAULT '0', + `customName` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`dbId`), + UNIQUE KEY `idx_guid_id` (`guid`,`followerId`), + CONSTRAINT `fk_followers_owner` FOREIGN KEY (`guid`) REFERENCES `characters` (`guid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `character_garrison_follower_abilities` +-- +CREATE TABLE `character_garrison_follower_abilities` ( + `dbId` bigint unsigned NOT NULL, + `abilityId` int unsigned NOT NULL DEFAULT '0', + `slot` tinyint unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`dbId`,`abilityId`,`slot`), + CONSTRAINT `fk_foll_abil_dbid` FOREIGN KEY (`dbId`) REFERENCES `character_garrison_followers` (`dbId`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `character_garrison_missions` +-- +DROP TABLE IF EXISTS `character_garrison_missions`; +CREATE TABLE `character_garrison_missions` ( + `dbId` bigint unsigned NOT NULL AUTO_INCREMENT, + `guid` bigint unsigned NOT NULL, + `missionRecID` int unsigned NOT NULL DEFAULT '0', + `offerTime` bigint unsigned NOT NULL DEFAULT '0', + `offerDuration` bigint unsigned NOT NULL DEFAULT '0', + `startTime` bigint unsigned NOT NULL DEFAULT '0', + `travelDuration` bigint unsigned NOT NULL DEFAULT '0', + `missionDuration` bigint unsigned NOT NULL DEFAULT '0', + `missionState` int unsigned NOT NULL DEFAULT '0', + `successChance` int unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`dbId`), + KEY `idx_guid` (`guid`), + CONSTRAINT `fk_missions_owner` FOREIGN KEY (`guid`) REFERENCES `characters` (`guid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `character_garrison_shipments` +-- +DROP TABLE IF EXISTS `character_garrison_shipments`; +CREATE TABLE `character_garrison_shipments` ( + `dbId` bigint unsigned NOT NULL AUTO_INCREMENT, + `guid` bigint unsigned NOT NULL, + `shipmentId` int unsigned NOT NULL DEFAULT '0', + `plotInstanceId` int unsigned NOT NULL DEFAULT '0', + `creationTime` bigint unsigned NOT NULL DEFAULT '0', + `duration` bigint unsigned NOT NULL DEFAULT '0', + `assignedFollowerDbId` bigint unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`dbId`), + KEY `idx_guid` (`guid`), + CONSTRAINT `fk_shipments_owner` FOREIGN KEY (`guid`) REFERENCES `characters` (`guid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `character_garrison_talents` +-- +DROP TABLE IF EXISTS `character_garrison_talents`; +CREATE TABLE `character_garrison_talents` ( + `guid` bigint unsigned NOT NULL, + `garrTalentId` int unsigned NOT NULL DEFAULT '0', + `rank` int unsigned NOT NULL DEFAULT '0', + `researchStartTime` bigint unsigned NOT NULL DEFAULT '0', + `flags` int unsigned NOT NULL DEFAULT '0', + `soulbindConduitId` int unsigned NOT NULL DEFAULT '0', + `soulbindConduitRank` int unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`guid`,`garrTalentId`), + CONSTRAINT `fk_talents_owner` FOREIGN KEY (`guid`) REFERENCES `characters` (`guid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `character_garrison_trophies` +-- +DROP TABLE IF EXISTS `character_garrison_trophies`; +CREATE TABLE `character_garrison_trophies` ( + `guid` bigint unsigned NOT NULL, + `trophyId` int unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`guid`,`trophyId`), + CONSTRAINT `fk_trophies_owner` FOREIGN KEY (`guid`) REFERENCES `characters` (`guid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `character_garrison_archived_missions` +-- +DROP TABLE IF EXISTS `character_garrison_archived_missions`; +CREATE TABLE `character_garrison_archived_missions` ( + `guid` bigint unsigned NOT NULL, + `garrType` int unsigned NOT NULL DEFAULT '0', + `missionRecID` int unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`guid`,`garrType`,`missionRecID`), + CONSTRAINT `fk_archived_owner` FOREIGN KEY (`guid`) REFERENCES `characters` (`guid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `character_select_screen_equipment_cache` +-- +DROP TABLE IF EXISTS `character_select_screen_equipment_cache`; +CREATE TABLE `character_select_screen_equipment_cache` ( + `guid` bigint unsigned NOT NULL, + `headEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `headVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `headSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `headInvType` tinyint unsigned NOT NULL DEFAULT '0', + `headDisplayID` int unsigned NOT NULL DEFAULT '0', + `headDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `headSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `headSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `neckEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `neckVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `neckSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `neckInvType` tinyint unsigned NOT NULL DEFAULT '0', + `neckDisplayID` int unsigned NOT NULL DEFAULT '0', + `neckDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `neckSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `neckSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `shoulderEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `shoulderVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `shoulderSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `shoulderInvType` tinyint unsigned NOT NULL DEFAULT '0', + `shoulderDisplayID` int unsigned NOT NULL DEFAULT '0', + `shoulderDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `shoulderSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `shoulderSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `bodyEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `bodyVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `bodySubclass` tinyint unsigned NOT NULL DEFAULT '0', + `bodyInvType` tinyint unsigned NOT NULL DEFAULT '0', + `bodyDisplayID` int unsigned NOT NULL DEFAULT '0', + `bodyDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `bodySecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `bodySheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `chestEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `chestVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `chestSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `chestInvType` tinyint unsigned NOT NULL DEFAULT '0', + `chestDisplayID` int unsigned NOT NULL DEFAULT '0', + `chestDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `chestSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `chestSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `waistEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `waistVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `waistSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `waistInvType` tinyint unsigned NOT NULL DEFAULT '0', + `waistDisplayID` int unsigned NOT NULL DEFAULT '0', + `waistDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `waistSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `waistSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `legsEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `legsVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `legsSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `legsInvType` tinyint unsigned NOT NULL DEFAULT '0', + `legsDisplayID` int unsigned NOT NULL DEFAULT '0', + `legsDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `legsSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `legsSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `feetEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `feetVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `feetSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `feetInvType` tinyint unsigned NOT NULL DEFAULT '0', + `feetDisplayID` int unsigned NOT NULL DEFAULT '0', + `feetDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `feetSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `feetSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `wristsEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `wristsVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `wristsSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `wristsInvType` tinyint unsigned NOT NULL DEFAULT '0', + `wristsDisplayID` int unsigned NOT NULL DEFAULT '0', + `wristsDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `wristsSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `wristsSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `handsEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `handsVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `handsSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `handsInvType` tinyint unsigned NOT NULL DEFAULT '0', + `handsDisplayID` int unsigned NOT NULL DEFAULT '0', + `handsDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `handsSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `handsSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `finger1EquippedItemID` int unsigned NOT NULL DEFAULT '0', + `finger1VisibleItemID` int unsigned NOT NULL DEFAULT '0', + `finger1Subclass` tinyint unsigned NOT NULL DEFAULT '0', + `finger1InvType` tinyint unsigned NOT NULL DEFAULT '0', + `finger1DisplayID` int unsigned NOT NULL DEFAULT '0', + `finger1DisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `finger1SecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `finger1SheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `finger2EquippedItemID` int unsigned NOT NULL DEFAULT '0', + `finger2VisibleItemID` int unsigned NOT NULL DEFAULT '0', + `finger2Subclass` tinyint unsigned NOT NULL DEFAULT '0', + `finger2InvType` tinyint unsigned NOT NULL DEFAULT '0', + `finger2DisplayID` int unsigned NOT NULL DEFAULT '0', + `finger2DisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `finger2SecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `finger2SheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `trinket1EquippedItemID` int unsigned NOT NULL DEFAULT '0', + `trinket1VisibleItemID` int unsigned NOT NULL DEFAULT '0', + `trinket1Subclass` tinyint unsigned NOT NULL DEFAULT '0', + `trinket1InvType` tinyint unsigned NOT NULL DEFAULT '0', + `trinket1DisplayID` int unsigned NOT NULL DEFAULT '0', + `trinket1DisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `trinket1SecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `trinket1SheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `trinket2EquippedItemID` int unsigned NOT NULL DEFAULT '0', + `trinket2VisibleItemID` int unsigned NOT NULL DEFAULT '0', + `trinket2Subclass` tinyint unsigned NOT NULL DEFAULT '0', + `trinket2InvType` tinyint unsigned NOT NULL DEFAULT '0', + `trinket2DisplayID` int unsigned NOT NULL DEFAULT '0', + `trinket2DisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `trinket2SecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `trinket2SheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `backEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `backVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `backSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `backInvType` tinyint unsigned NOT NULL DEFAULT '0', + `backDisplayID` int unsigned NOT NULL DEFAULT '0', + `backDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `backSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `backSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `mainHandEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `mainHandVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `mainHandSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `mainHandInvType` tinyint unsigned NOT NULL DEFAULT '0', + `mainHandDisplayID` int unsigned NOT NULL DEFAULT '0', + `mainHandDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `mainHandSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `mainHandSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `offHandEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `offHandVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `offHandSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `offHandInvType` tinyint unsigned NOT NULL DEFAULT '0', + `offHandDisplayID` int unsigned NOT NULL DEFAULT '0', + `offHandDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `offHandSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `offHandSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `rangedEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `rangedVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `rangedSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `rangedInvType` tinyint unsigned NOT NULL DEFAULT '0', + `rangedDisplayID` int unsigned NOT NULL DEFAULT '0', + `rangedDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `rangedSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `rangedSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + `tabardEquippedItemID` int unsigned NOT NULL DEFAULT '0', + `tabardVisibleItemID` int unsigned NOT NULL DEFAULT '0', + `tabardSubclass` tinyint unsigned NOT NULL DEFAULT '0', + `tabardInvType` tinyint unsigned NOT NULL DEFAULT '0', + `tabardDisplayID` int unsigned NOT NULL DEFAULT '0', + `tabardDisplayEnchantID` int unsigned NOT NULL DEFAULT '0', + `tabardSecondaryItemModifiedAppearanceID` int unsigned NOT NULL DEFAULT '0', + `tabardSheatheCategory` tinyint unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`guid`), + CONSTRAINT `fk_equip_cache_owner` FOREIGN KEY (`guid`) REFERENCES `characters` (`guid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/sql/updates/hotfixes/master/.gitignore b/sql/updates/hotfixes/master/.gitignore deleted file mode 100644 index 9ef72e70b..000000000 --- a/sql/updates/hotfixes/master/.gitignore +++ /dev/null @@ -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 diff --git a/sql/updates/hotfixes/master/2026_05_03_00_hotfixes.sql b/sql/updates/hotfixes/master/2026_05_03_00_hotfixes.sql new file mode 100644 index 000000000..8f6951ca5 --- /dev/null +++ b/sql/updates/hotfixes/master/2026_05_03_00_hotfixes.sql @@ -0,0 +1,67 @@ +-- +-- Table structure for table `garr_ability_category` +-- +DROP TABLE IF EXISTS `garr_ability_category`; +CREATE TABLE `garr_ability_category` ( + `ID` int unsigned NOT NULL DEFAULT '0', + `Name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `VerifiedBuild` int NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`,`VerifiedBuild`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `garr_ability_category_locale` +-- +DROP TABLE IF EXISTS `garr_ability_category_locale`; +CREATE TABLE `garr_ability_category_locale` ( + `ID` int unsigned NOT NULL DEFAULT '0', + `locale` varchar(4) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `Name_lang` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `VerifiedBuild` int NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`,`locale`,`VerifiedBuild`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `garr_foll_support_spell` +-- +DROP TABLE IF EXISTS `garr_foll_support_spell`; +CREATE TABLE `garr_foll_support_spell` ( + `ID` int unsigned NOT NULL DEFAULT '0', + `HordeSpellID` int NOT NULL DEFAULT '0', + `AllianceSpellID` int NOT NULL DEFAULT '0', + `OrderIndex` tinyint unsigned NOT NULL DEFAULT '0', + `GarrFollowerID` int unsigned NOT NULL DEFAULT '0', + `VerifiedBuild` int NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`,`VerifiedBuild`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `item_modified_appearance` +-- +DROP TABLE IF EXISTS `item_modified_appearance`; +CREATE TABLE `item_modified_appearance` ( + `ID` int unsigned NOT NULL DEFAULT '0', + `ItemID` int unsigned NOT NULL DEFAULT '0', + `ItemAppearanceModifierID` int unsigned NOT NULL DEFAULT '0', + `ItemAppearanceID` int unsigned NOT NULL DEFAULT '0', + `OrderIndex` int unsigned NOT NULL DEFAULT '0', + `TransmogSourceTypeEnum` int unsigned NOT NULL DEFAULT '0', + `Flags` int NOT NULL DEFAULT '0', + `VerifiedBuild` int NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`,`VerifiedBuild`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `item_modified_appearance_extra` +-- +DROP TABLE IF EXISTS `item_modified_appearance_extra`; +CREATE TABLE `item_modified_appearance_extra` ( + `ID` int unsigned NOT NULL DEFAULT '0', + `IconFileDataID` int unsigned NOT NULL DEFAULT '0', + `UnequippedIconFileDataID` int unsigned NOT NULL DEFAULT '0', + `SheatheType` tinyint unsigned NOT NULL DEFAULT '0', + `DisplayWeaponSubclassID` int unsigned NOT NULL DEFAULT '0', + `DisplayInventoryType` tinyint unsigned NOT NULL DEFAULT '0', + `VerifiedBuild` int NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`,`VerifiedBuild`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/src/modules/Playerbot/Session/BotSession_StateIntegration.cpp b/src/modules/Playerbot/Session/BotSession_StateIntegration.cpp index 734435304..690739e25 100644 --- a/src/modules/Playerbot/Session/BotSession_StateIntegration.cpp +++ b/src/modules/Playerbot/Session/BotSession_StateIntegration.cpp @@ -44,7 +44,22 @@ void BotSession::HandleBotPlayerLogin(Player* bot) // CRITICAL FIX: Add bot to world (missing step that prevented bots from entering world) pCurrChar->SendInitialPacketsBeforeAddToMap(); - if (!pCurrChar->GetMap()->AddPlayerToMap(pCurrChar)) + Map* map = pCurrChar->GetMap(); + if (!map) + { + TC_LOG_ERROR("module.playerbot.session", "Bot player {} has no valid map, getting from map manager", characterGuid.ToString()); + map = sMapMgr->FindMap(pCurrChar->GetMapId(), pCurrChar->GetInstanceId()); + if (!map) + { + TC_LOG_ERROR("module.playerbot.session", "Failed to get map for bot player {} (mapId={}, instanceId={})", + characterGuid.ToString(), pCurrChar->GetMapId(), pCurrChar->GetInstanceId()); + // Teleport to homebind as fallback + pCurrChar->TeleportTo(pCurrChar->m_homebind); + map = pCurrChar->GetMap(); + } + } + + if (map && !map->AddPlayerToMap(pCurrChar)) { TC_LOG_ERROR("module.playerbot.session", "Failed to add bot player {} to map", characterGuid.ToString()); // Try to teleport to homebind if map addition fails diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index d98dc17d8..6579bf86e 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -790,21 +790,41 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_UPD_QUEST_TRACK_ABANDON_TIME, "UPDATE quest_tracker SET quest_abandon_time = NOW() WHERE id = ? AND character_guid = ? ORDER BY quest_accept_time DESC LIMIT 1", CONNECTION_ASYNC); // Garrison - PrepareStatement(CHAR_SEL_CHARACTER_GARRISON, "SELECT siteLevelId, followerActivationsRemainingToday FROM character_garrison WHERE guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_INS_CHARACTER_GARRISON, "INSERT INTO character_garrison (guid, siteLevelId, followerActivationsRemainingToday) VALUES (?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_CHARACTER_GARRISON, "SELECT siteLevelId, followerActivationsRemainingToday, type, missionsStartedToday, lastMissionStartDay FROM character_garrison WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_CHARACTER_GARRISON, "INSERT INTO character_garrison (guid, siteLevelId, followerActivationsRemainingToday, type, missionsStartedToday, lastMissionStartDay) VALUES (?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); PrepareStatement(CHAR_DEL_CHARACTER_GARRISON, "DELETE FROM character_garrison WHERE guid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_UPD_CHARACTER_GARRISON_FOLLOWER_ACTIVATIONS, "UPDATE character_garrison SET followerActivationsRemainingToday = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_BLUEPRINTS, "SELECT buildingId FROM character_garrison_blueprints WHERE guid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_INS_CHARACTER_GARRISON_BLUEPRINTS, "INSERT INTO character_garrison_blueprints (guid, buildingId) VALUES (?, ?)", CONNECTION_ASYNC); PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_BLUEPRINTS, "DELETE FROM character_garrison_blueprints WHERE guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_BUILDINGS, "SELECT plotInstanceId, buildingId, timeBuilt, active FROM character_garrison_buildings WHERE guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_INS_CHARACTER_GARRISON_BUILDINGS, "INSERT INTO character_garrison_buildings (guid, plotInstanceId, buildingId, timeBuilt, active) VALUES (?, ?, ?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_BUILDINGS, "SELECT plotInstanceId, buildingId, timeBuilt, active, currentGarSpecId, timeSpecCooldown FROM character_garrison_buildings WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_CHARACTER_GARRISON_BUILDINGS, "INSERT INTO character_garrison_buildings (guid, plotInstanceId, buildingId, timeBuilt, active, currentGarSpecId, timeSpecCooldown) VALUES (?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_BUILDINGS, "DELETE FROM character_garrison_buildings WHERE guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_FOLLOWERS, "SELECT dbId, followerId, quality, level, itemLevelWeapon, itemLevelArmor, xp, currentBuilding, currentMission, status FROM character_garrison_followers WHERE guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_INS_CHARACTER_GARRISON_FOLLOWERS, "INSERT INTO character_garrison_followers (dbId, guid, followerId, quality, level, itemLevelWeapon, itemLevelArmor, xp, currentBuilding, currentMission, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_SPECIALIZATIONS, "SELECT specId FROM character_garrison_specializations WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_CHARACTER_GARRISON_SPECIALIZATIONS, "INSERT INTO character_garrison_specializations (guid, specId) VALUES (?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_SPECIALIZATIONS, "DELETE FROM character_garrison_specializations WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_FOLLOWERS, "SELECT dbId, followerId, quality, level, itemLevelWeapon, itemLevelArmor, xp, currentBuilding, currentMission, status, durability, customName FROM character_garrison_followers WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_CHARACTER_GARRISON_FOLLOWERS, "INSERT INTO character_garrison_followers (dbId, guid, followerId, quality, level, itemLevelWeapon, itemLevelArmor, xp, currentBuilding, currentMission, status, durability, customName) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_FOLLOWERS, "DELETE gfab, gf FROM character_garrison_follower_abilities gfab INNER JOIN character_garrison_followers gf ON gfab.dbId = gf.dbId WHERE gf.guid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_FOLLOWER_ABILITIES, "SELECT gfab.dbId, gfab.abilityId FROM character_garrison_follower_abilities gfab INNER JOIN character_garrison_followers gf ON gfab.dbId = gf.dbId WHERE guid = ? ORDER BY gfab.slot", CONNECTION_ASYNC); PrepareStatement(CHAR_INS_CHARACTER_GARRISON_FOLLOWER_ABILITIES, "INSERT INTO character_garrison_follower_abilities (dbId, abilityId, slot) VALUES (?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_MISSIONS, "SELECT dbId, guid, missionRecID, offerTime, offerDuration, startTime, travelDuration, missionDuration, missionState, successChance FROM character_garrison_missions WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_CHARACTER_GARRISON_MISSIONS, "INSERT INTO character_garrison_missions (dbId, guid, missionRecID, offerTime, offerDuration, startTime, travelDuration, missionDuration, missionState, successChance) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_MISSIONS, "DELETE FROM character_garrison_missions WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_SHIPMENTS, "SELECT dbId, shipmentId, plotInstanceId, creationTime, duration, assignedFollowerDbId FROM character_garrison_shipments WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_CHARACTER_GARRISON_SHIPMENTS, "INSERT INTO character_garrison_shipments (dbId, guid, shipmentId, plotInstanceId, creationTime, duration, assignedFollowerDbId) VALUES (?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_SHIPMENTS, "DELETE FROM character_garrison_shipments WHERE guid = ?", CONNECTION_ASYNC); + + PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_TALENTS, "SELECT garrTalentId, `rank`, researchStartTime, flags, soulbindConduitId, soulbindConduitRank FROM character_garrison_talents WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_CHARACTER_GARRISON_TALENT, "INSERT INTO character_garrison_talents (guid, garrTalentId, `rank`, researchStartTime, flags, soulbindConduitId, soulbindConduitRank) VALUES (?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_TALENTS, "DELETE FROM character_garrison_talents WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_TALENT, "DELETE FROM character_garrison_talents WHERE guid = ? AND garrTalentId = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_TROPHIES, "SELECT trophyId FROM character_garrison_trophies WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_CHARACTER_GARRISON_TROPHY, "INSERT INTO character_garrison_trophies (guid, trophyId) VALUES (?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_TROPHIES, "DELETE FROM character_garrison_trophies WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_ARCHIVED_MISSIONS, "SELECT garrType, missionRecID FROM character_garrison_archived_missions WHERE guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_CHARACTER_GARRISON_ARCHIVED_MISSION, "INSERT INTO character_garrison_archived_missions (guid, garrType, missionRecID) VALUES (?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_ARCHIVED_MISSIONS, "DELETE FROM character_garrison_archived_missions WHERE guid = ?", CONNECTION_ASYNC); // Black Market PrepareStatement(CHAR_SEL_BLACKMARKET_AUCTIONS, "SELECT marketId, currentBid, time, numBids, bidder FROM blackmarket_auctions", CONNECTION_SYNCH); @@ -959,25 +979,6 @@ void CharacterDatabaseConnection::DoPrepareStatements() "SELECT COALESCE(SUM(amount), 0) FROM neighborhood_initiative_contributions WHERE initiativeDbId = ? AND playerGuid = ?", CONNECTION_SYNCH); - //GARRISONS - PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_MISSIONS, "SELECT dbId, guid, missionRecID, offerTime, offerDuration, startTime, travelDuration, missionDuration, missionState, successChance FROM character_garrison_missions WHERE guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_INS_CHARACTER_GARRISON_MISSIONS, "INSERT INTO character_garrison_missions (dbId, guid, missionRecID, offerTime, offerDuration, startTime, travelDuration, missionDuration, missionState, successChance) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); - PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_MISSIONS, "DELETE FROM character_garrison_missions WHERE guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_SHIPMENTS, "SELECT dbId, shipmentId, plotInstanceId, creationTime, duration, assignedFollowerDbId FROM character_garrison_shipments WHERE guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_INS_CHARACTER_GARRISON_SHIPMENTS, "INSERT INTO character_garrison_shipments (dbId, guid, shipmentId, plotInstanceId, creationTime, duration, assignedFollowerDbId) VALUES (?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); - PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_SHIPMENTS, "DELETE FROM character_garrison_shipments WHERE guid = ?", CONNECTION_ASYNC); - - PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_TALENTS, "SELECT garrTalentId, `rank`, researchStartTime, flags, soulbindConduitId, soulbindConduitRank FROM character_garrison_talents WHERE guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_INS_CHARACTER_GARRISON_TALENT, "INSERT INTO character_garrison_talents (guid, garrTalentId, `rank`, researchStartTime, flags, soulbindConduitId, soulbindConduitRank) VALUES (?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); - PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_TALENTS, "DELETE FROM character_garrison_talents WHERE guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_TALENT, "DELETE FROM character_garrison_talents WHERE guid = ? AND garrTalentId = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_TROPHIES, "SELECT trophyId FROM character_garrison_trophies WHERE guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_INS_CHARACTER_GARRISON_TROPHY, "INSERT INTO character_garrison_trophies (guid, trophyId) VALUES (?, ?)", CONNECTION_ASYNC); - PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_TROPHIES, "DELETE FROM character_garrison_trophies WHERE guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_SEL_CHARACTER_GARRISON_ARCHIVED_MISSIONS, "SELECT garrType, missionRecID FROM character_garrison_archived_missions WHERE guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_INS_CHARACTER_GARRISON_ARCHIVED_MISSION, "INSERT INTO character_garrison_archived_missions (guid, garrType, missionRecID) VALUES (?, ?, ?)", CONNECTION_ASYNC); - PrepareStatement(CHAR_DEL_CHARACTER_GARRISON_ARCHIVED_MISSIONS, "DELETE FROM character_garrison_archived_missions WHERE guid = ?", CONNECTION_ASYNC); - //WARBANDS PrepareStatement(CHAR_SEL_WARBAND_GROUPS, "SELECT groupId, orderIndex, warbandSceneId, flags, contentSetId, name FROM character_warband_groups WHERE battlenetAccountId = ? ORDER BY orderIndex", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_WARBAND_GROUP_MEMBERS, "SELECT gm.groupId, gm.memberIndex, gm.guid, gm.warbandScenePlacementId, gm.memberType, gm.contentSetId FROM character_warband_group_members gm INNER JOIN character_warband_groups g ON gm.groupId = g.groupId WHERE g.battlenetAccountId = ? ORDER BY gm.groupId, gm.memberIndex", CONNECTION_ASYNC); diff --git a/src/server/database/Database/Implementation/HotfixDatabase.cpp b/src/server/database/Database/Implementation/HotfixDatabase.cpp index 20db67e1a..9e76cc6e1 100644 --- a/src/server/database/Database/Implementation/HotfixDatabase.cpp +++ b/src/server/database/Database/Implementation/HotfixDatabase.cpp @@ -347,6 +347,20 @@ void HotfixDatabaseConnection::DoPrepareStatements() " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); PREPARE_MAX_ID_STMT(HOTFIX_SEL_CHARACTER_LOADOUT_ITEM, "SELECT MAX(ID) + 1 FROM character_loadout_item", CONNECTION_SYNCH); + // CharShipment.db2 + PrepareStatement(HOTFIX_SEL_CHAR_SHIPMENT, "SELECT ID, ContainerID, TreasureID, DummyItemID, Duration, SpellID, OnCompleteSpellID," + " MaxShipments, GarrFollowerID, Flags FROM char_shipment WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_CHAR_SHIPMENT, "SELECT MAX(ID) + 1 FROM char_shipment", CONNECTION_SYNCH); + + // CharShipmentContainer.db2 + PrepareStatement(HOTFIX_SEL_CHAR_SHIPMENT_CONTAINER, "SELECT ID, Description, PendingText, UiTextureKitID, GarrTypeID, GarrBuildingType," + " BaseCapacity, SmallDisplayInfoID, MediumDisplayInfoID, LargeDisplayInfoID, WorkingSpellVisualID, CompleteSpellVisualID," + " WorkingDisplayInfoID, MediumThreshold, LargeThreshold, Faction, CrossFactionID FROM char_shipment_container" + " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_CHAR_SHIPMENT_CONTAINER, "SELECT MAX(ID) + 1 FROM char_shipment_container", CONNECTION_SYNCH); + PREPARE_LOCALE_STMT(HOTFIX_SEL_CHAR_SHIPMENT_CONTAINER, "SELECT ID, Description_lang, PendingText_lang FROM char_shipment_container_locale" + " WHERE (`VerifiedBuild` > 0) = ? AND locale = ?", CONNECTION_SYNCH); + // ChatChannels.db2 PrepareStatement(HOTFIX_SEL_CHAT_CHANNELS, "SELECT ID, Name, Shortcut, Flags, FactionGroup, Ruleset FROM chat_channels" " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); @@ -776,6 +790,35 @@ void HotfixDatabaseConnection::DoPrepareStatements() PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_ABILITY, "SELECT ID, Name_lang, Description_lang FROM garr_ability_locale WHERE (`VerifiedBuild` > 0) = ?" " AND locale = ?", CONNECTION_SYNCH); + // GarrAbilityCategory.db2 + PrepareStatement(HOTFIX_SEL_GARR_ABILITY_CATEGORY, "SELECT ID, Name FROM garr_ability_category WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_ABILITY_CATEGORY, "SELECT MAX(ID) + 1 FROM garr_ability_category", CONNECTION_SYNCH); + PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_ABILITY_CATEGORY, "SELECT ID, Name_lang FROM garr_ability_category_locale WHERE (`VerifiedBuild` > 0) = ?" + " AND locale = ?", CONNECTION_SYNCH); + + // GarrAbilityEffect.db2 + PrepareStatement(HOTFIX_SEL_GARR_ABILITY_EFFECT, "SELECT ID, GarrAbilityID, EffectType, TargetMask, Amount, CombatWeightBase, " + "CombatWeightMax, ActionValueFlat, AbilityAction, MiscValueA, ActionHours, ActionRecordID FROM garr_ability_effect " + "WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_ABILITY_EFFECT, "SELECT MAX(ID) + 1 FROM garr_ability_effect", CONNECTION_SYNCH); + + // GarrAutoCombatant.db2 + PrepareStatement(HOTFIX_SEL_GARR_AUTO_COMBATANT, "SELECT ID, Attack, Health, MaxHealth, AutoAttackSpellID, Role, BoardIndex, " + "GarrEncounterID, GarrAutoSpellID, Flags FROM garr_auto_combatant WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_AUTO_COMBATANT, "SELECT MAX(ID) + 1 FROM garr_auto_combatant", CONNECTION_SYNCH); + + // GarrAutoSpell.db2 + PrepareStatement(HOTFIX_SEL_GARR_AUTO_SPELL, "SELECT ID, Name, Description, Cooldown, Duration, SchoolMask, SpellVisualID, " + "Flags FROM garr_auto_spell WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_AUTO_SPELL, "SELECT MAX(ID) + 1 FROM garr_auto_spell", CONNECTION_SYNCH); + PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_AUTO_SPELL, "SELECT ID, Name_lang, Description_lang FROM garr_auto_spell_locale WHERE (`VerifiedBuild` > 0) = ?" + " AND locale = ?", CONNECTION_SYNCH); + + // GarrAutoSpellEffect.db2 + PrepareStatement(HOTFIX_SEL_GARR_AUTO_SPELL_EFFECT, "SELECT ID, GarrAutoSpellID, EffectType, Targets, Amount, MiscType, MiscValue, " + "Period FROM garr_auto_spell_effect WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_AUTO_SPELL_EFFECT, "SELECT MAX(ID) + 1 FROM garr_auto_spell_effect", CONNECTION_SYNCH); + // GarrBuilding.db2 PrepareStatement(HOTFIX_SEL_GARR_BUILDING, "SELECT ID, HordeName, AllianceName, Description, Tooltip, GarrTypeID, BuildingType, " "HordeGameObjectID, AllianceGameObjectID, GarrSiteID, UpgradeLevel, BuildSeconds, CurrencyTypeID, CurrencyQty, HordeUiTextureKitID, " @@ -785,6 +828,11 @@ void HotfixDatabaseConnection::DoPrepareStatements() PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_BUILDING, "SELECT ID, HordeName_lang, AllianceName_lang, Description_lang, Tooltip_lang" " FROM garr_building_locale WHERE (`VerifiedBuild` > 0) = ? AND locale = ?", CONNECTION_SYNCH); + // GarrBuildingDoodadSet.db2 + PrepareStatement(HOTFIX_SEL_GARR_BUILDING_DOODAD_SET, "SELECT ID, GarrBuildingID, HordeDoodadSetID, AllianceDoodadSetID, HordeAltDoodadSetID, " + "AllianceAltDoodadSetID FROM garr_building_doodad_set WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_BUILDING_DOODAD_SET, "SELECT MAX(ID) + 1 FROM garr_building_doodad_set", CONNECTION_SYNCH); + // GarrBuildingPlotInst.db2 PrepareStatement(HOTFIX_SEL_GARR_BUILDING_PLOT_INST, "SELECT MapOffsetX, MapOffsetY, ID, GarrBuildingID, GarrSiteLevelPlotInstID, " "UiTextureAtlasMemberID FROM garr_building_plot_inst WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); @@ -797,6 +845,39 @@ void HotfixDatabaseConnection::DoPrepareStatements() PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_CLASS_SPEC, "SELECT ID, ClassSpec_lang, ClassSpecMale_lang, ClassSpecFemale_lang FROM garr_class_spec_locale" " WHERE (`VerifiedBuild` > 0) = ? AND locale = ?", CONNECTION_SYNCH); + // GarrClassSpecPlayerCond.db2 + PrepareStatement(HOTFIX_SEL_GARR_CLASS_SPEC_PLAYER_COND, "SELECT ID, ClassSpec, OrderIndex, GarrClassSpecID, PlayerConditionID, GarrStringID, " + "Flags FROM garr_class_spec_player_cond WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_CLASS_SPEC_PLAYER_COND, "SELECT MAX(ID) + 1 FROM garr_class_spec_player_cond", CONNECTION_SYNCH); + PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_CLASS_SPEC_PLAYER_COND, "SELECT ID, ClassSpec_lang FROM garr_class_spec_player_cond_locale WHERE (`VerifiedBuild` > 0) = ?" + " AND locale = ?", CONNECTION_SYNCH); + + // GarrEncounter.db2 + PrepareStatement(HOTFIX_SEL_GARR_ENCOUNTER, "SELECT ID, Name, CreatureID, CreatureDisplayInfoID, UiAnimHeight, UiAnimScale, UiTextureScale, " + "EnvGarrMechanicTypeID, GarrEncounterSetID FROM garr_encounter WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_ENCOUNTER, "SELECT MAX(ID) + 1 FROM garr_encounter", CONNECTION_SYNCH); + PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_ENCOUNTER, "SELECT ID, Name_lang FROM garr_encounter_locale WHERE (`VerifiedBuild` > 0) = ? AND locale = ?", CONNECTION_SYNCH); + + // GarrEncounterSetXEncounter.db2 + PrepareStatement(HOTFIX_SEL_GARR_ENCOUNTER_SET_X_ENCOUNTER, "SELECT ID, GarrEncounterID, GarrEncounterSetID" + " FROM garr_encounter_set_x_encounter WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_ENCOUNTER_SET_X_ENCOUNTER, "SELECT MAX(ID) + 1 FROM garr_encounter_set_x_encounter", CONNECTION_SYNCH); + + // GarrEncounterXMechanic.db2 + PrepareStatement(HOTFIX_SEL_GARR_ENCOUNTER_X_MECHANIC, "SELECT ID, GarrMechanicID, GarrMechanicSetID, GarrEncounterID" + " FROM garr_encounter_x_mechanic WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_ENCOUNTER_X_MECHANIC, "SELECT MAX(ID) + 1 FROM garr_encounter_x_mechanic", CONNECTION_SYNCH); + + // GarrFollItemSetMember.db2 + PrepareStatement(HOTFIX_SEL_GARR_FOLL_ITEM_SET_MEMBER, "SELECT ID, GarrFollItemSetID, ItemSlot, ItemModifiedAppearanceID, GarrFollowerID" + " FROM garr_foll_item_set_member WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_FOLL_ITEM_SET_MEMBER, "SELECT MAX(ID) + 1 FROM garr_foll_item_set_member", CONNECTION_SYNCH); + + // GarrFollSupportSpell.db2 + PrepareStatement(HOTFIX_SEL_GARR_FOLL_SUPPORT_SPELL, "SELECT ID, HordeSpellID, AllianceSpellID, OrderIndex, GarrFollowerID" + " FROM garr_foll_support_spell WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_FOLL_SUPPORT_SPELL, "SELECT MAX(ID) + 1 FROM garr_foll_support_spell", CONNECTION_SYNCH); + // GarrFollower.db2 PrepareStatement(HOTFIX_SEL_GARR_FOLLOWER, "SELECT ID, HordeSourceText, AllianceSourceText, TitleName, GarrTypeID, GarrFollowerTypeID, " "HordeCreatureID, AllianceCreatureID, HordeGarrFollRaceID, AllianceGarrFollRaceID, HordeGarrClassSpecID, AllianceGarrClassSpecID, Quality, " @@ -808,11 +889,58 @@ void HotfixDatabaseConnection::DoPrepareStatements() PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_FOLLOWER, "SELECT ID, HordeSourceText_lang, AllianceSourceText_lang, TitleName_lang FROM garr_follower_locale" " WHERE (`VerifiedBuild` > 0) = ? AND locale = ?", CONNECTION_SYNCH); + // GarrFollowerLevelXP.db2 + PrepareStatement(HOTFIX_SEL_GARR_FOLLOWER_LEVEL_XP, "SELECT ID, FollowerLevel, GarrFollowerTypeID, XpToNextLevel, ShipmentXP" + " FROM garr_follower_level_xp WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_FOLLOWER_LEVEL_XP, "SELECT MAX(ID) + 1 FROM garr_follower_level_xp", CONNECTION_SYNCH); + + // GarrFollowerQuality.db2 + PrepareStatement(HOTFIX_SEL_GARR_FOLLOWER_QUALITY, "SELECT ID, XpThreshold, QualityItemID, Quality, AbilityCount, TraitCount, " + "GarrFollowerTypeID, ClassSpecID FROM garr_follower_quality WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_FOLLOWER_QUALITY, "SELECT MAX(ID) + 1 FROM garr_follower_quality", CONNECTION_SYNCH); + + // GarrFollowerType.db2 + PrepareStatement(HOTFIX_SEL_GARR_FOLLOWER_TYPE, "SELECT ID, GarrTypeID, MaxFollowers, MaxFollowerBuildingType, MaxItemLevel, LevelRangeBias, " + "ItemLevelRangeBias, Flags FROM garr_follower_type WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_FOLLOWER_TYPE, "SELECT MAX(ID) + 1 FROM garr_follower_type", CONNECTION_SYNCH); + + // GarrFollowerSetXFollower.db2 + PrepareStatement(HOTFIX_SEL_GARR_FOLLOWER_SET_X_FOLLOWER, "SELECT ID, GarrFollowerID, GarrFollowerSetID" + " FROM garr_follower_set_x_follower WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_FOLLOWER_SET_X_FOLLOWER, "SELECT MAX(ID) + 1 FROM garr_follower_set_x_follower", CONNECTION_SYNCH); + // GarrFollowerXAbility.db2 + // GarrFollowerUICreature.db2 + PrepareStatement(HOTFIX_SEL_GARR_FOLLOWER_UI_CREATURE, "SELECT ID, Race, Gender, CreatureID, Scale, FileDataID, GarrFollowerID" + " FROM garr_follower_ui_creature WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_FOLLOWER_UI_CREATURE, "SELECT MAX(ID) + 1 FROM garr_follower_ui_creature", CONNECTION_SYNCH); + PrepareStatement(HOTFIX_SEL_GARR_FOLLOWER_X_ABILITY, "SELECT ID, OrderIndex, FactionIndex, GarrAbilityID, GarrFollowerID" " FROM garr_follower_x_ability WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_FOLLOWER_X_ABILITY, "SELECT MAX(ID) + 1 FROM garr_follower_x_ability", CONNECTION_SYNCH); + // GarrItemLevelUpgradeData.db2 + PrepareStatement(HOTFIX_SEL_GARR_ITEM_LEVEL_UPGRADE_DATA, "SELECT ID, Operation, MinItemLevel, MaxItemLevel, FollowerTypeID" + " FROM garr_item_level_upgrade_data WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_ITEM_LEVEL_UPGRADE_DATA, "SELECT MAX(ID) + 1 FROM garr_item_level_upgrade_data", CONNECTION_SYNCH); + + // GarrMechanic.db2 + PrepareStatement(HOTFIX_SEL_GARR_MECHANIC, "SELECT ID, GarrMechanicTypeID, Factor, GarrAbilityID" + " FROM garr_mechanic WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_MECHANIC, "SELECT MAX(ID) + 1 FROM garr_mechanic", CONNECTION_SYNCH); + + // GarrMechanicSetXMechanic.db2 + PrepareStatement(HOTFIX_SEL_GARR_MECHANIC_SET_X_MECHANIC, "SELECT ID, GarrMechanicID, GarrMechanicSetID" + " FROM garr_mechanic_set_x_mechanic WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_MECHANIC_SET_X_MECHANIC, "SELECT MAX(ID) + 1 FROM garr_mechanic_set_x_mechanic", CONNECTION_SYNCH); + + // GarrMechanicType.db2 + PrepareStatement(HOTFIX_SEL_GARR_MECHANIC_TYPE, "SELECT ID, Name, Description, GarrAbilityCategoryID, Category" + " FROM garr_mechanic_type WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_MECHANIC_TYPE, "SELECT MAX(ID) + 1 FROM garr_mechanic_type", CONNECTION_SYNCH); + PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_MECHANIC_TYPE, "SELECT ID, Name_lang, Description_lang FROM garr_mechanic_type_locale" + " WHERE (`VerifiedBuild` > 0) = ? AND locale = ?", CONNECTION_SYNCH); + // GarrMission.db2 PrepareStatement(HOTFIX_SEL_GARR_MISSION, "SELECT ID, Name, Location, Description, MapPosX, MapPosY, WorldPosX, WorldPosY, GarrTypeID, " "GarrMissionTypeID, GarrFollowerTypeID, MaxFollowers, MissionCost, MissionCostCurrencyTypesID, OfferedGarrMissionTextureID, UiTextureKitID, " @@ -823,6 +951,38 @@ void HotfixDatabaseConnection::DoPrepareStatements() PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_MISSION, "SELECT ID, Name_lang, Location_lang, Description_lang FROM garr_mission_locale" " WHERE (`VerifiedBuild` > 0) = ? AND locale = ?", CONNECTION_SYNCH); + // GarrMissionSet.db2 + PrepareStatement(HOTFIX_SEL_GARR_MISSION_SET, "SELECT ID, GarrTypeID, MissionCostCurrencyTypesID, ContentTuningID, MapID, Flags, Priority" + " FROM garr_mission_set WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_MISSION_SET, "SELECT MAX(ID) + 1 FROM garr_mission_set", CONNECTION_SYNCH); + + // GarrMissionTexture.db2 + PrepareStatement(HOTFIX_SEL_GARR_MISSION_TEXTURE, "SELECT ID, PosX, PosY, UiTextureAtlasMemberID" + " FROM garr_mission_texture WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_MISSION_TEXTURE, "SELECT MAX(ID) + 1 FROM garr_mission_texture", CONNECTION_SYNCH); + + // GarrMissionType.db2 + PrepareStatement(HOTFIX_SEL_GARR_MISSION_TYPE, "SELECT ID, Name, UiTextureAtlasMemberID, UiTextureKitID" + " FROM garr_mission_type WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_MISSION_TYPE, "SELECT MAX(ID) + 1 FROM garr_mission_type", CONNECTION_SYNCH); + PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_MISSION_TYPE, "SELECT ID, Name_lang FROM garr_mission_type_locale WHERE (`VerifiedBuild` > 0) = ?" + " AND locale = ?", CONNECTION_SYNCH); + + // GarrMissionXEncounter.db2 + PrepareStatement(HOTFIX_SEL_GARR_MISSION_X_ENCOUNTER, "SELECT ID, GarrEncounterID, GarrMissionSetEncounterID, CombatWeightBase, CombatWeightMax, " + "GarrMissionID FROM garr_mission_x_encounter WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_MISSION_X_ENCOUNTER, "SELECT MAX(ID) + 1 FROM garr_mission_x_encounter", CONNECTION_SYNCH); + + // GarrMissionXFollower.db2 + PrepareStatement(HOTFIX_SEL_GARR_MISSION_X_FOLLOWER, "SELECT ID, GarrFollowerID, GarrFollowerTypeID, BoardIndex, GarrMissionID" + " FROM garr_mission_x_follower WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_MISSION_X_FOLLOWER, "SELECT MAX(ID) + 1 FROM garr_mission_x_follower", CONNECTION_SYNCH); + + // GarrMssnBonusAbility.db2 + PrepareStatement(HOTFIX_SEL_GARR_MSSN_BONUS_ABILITY, "SELECT ID, GarrMssnBonusAbilityType, MssnSort, GarrAbilityID, Modifier, MissionSetID" + " FROM garr_mssn_bonus_ability WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_MSSN_BONUS_ABILITY, "SELECT MAX(ID) + 1 FROM garr_mssn_bonus_ability", CONNECTION_SYNCH); + // GarrPlot.db2 PrepareStatement(HOTFIX_SEL_GARR_PLOT, "SELECT ID, Name, PlotType, HordeConstructObjID, AllianceConstructObjID, Flags, UpgradeRequirement1, " "UpgradeRequirement2 FROM garr_plot WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); @@ -836,6 +996,12 @@ void HotfixDatabaseConnection::DoPrepareStatements() PrepareStatement(HOTFIX_SEL_GARR_PLOT_INSTANCE, "SELECT ID, Name, GarrPlotID FROM garr_plot_instance WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_PLOT_INSTANCE, "SELECT MAX(ID) + 1 FROM garr_plot_instance", CONNECTION_SYNCH); + // GarrPlotUICategory.db2 + PrepareStatement(HOTFIX_SEL_GARR_PLOT_UI_CATEGORY, "SELECT ID, Name, PlotType FROM garr_plot_ui_category WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_PLOT_UI_CATEGORY, "SELECT MAX(ID) + 1 FROM garr_plot_ui_category", CONNECTION_SYNCH); + PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_PLOT_UI_CATEGORY, "SELECT ID, Name_lang FROM garr_plot_ui_category_locale WHERE (`VerifiedBuild` > 0) = ?" + " AND locale = ?", CONNECTION_SYNCH); + // GarrSiteLevel.db2 PrepareStatement(HOTFIX_SEL_GARR_SITE_LEVEL, "SELECT ID, TownHallUiPosX, TownHallUiPosY, GarrSiteID, GarrLevel, MapID, UpgradeMovieID, " "UiTextureKitID, MaxBuildingLevel, UpgradeCost, UpgradeGoldCost FROM garr_site_level WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); @@ -846,6 +1012,19 @@ void HotfixDatabaseConnection::DoPrepareStatements() " FROM garr_site_level_plot_inst WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_SITE_LEVEL_PLOT_INST, "SELECT MAX(ID) + 1 FROM garr_site_level_plot_inst", CONNECTION_SYNCH); + // GarrSpecialization.db2 + PrepareStatement(HOTFIX_SEL_GARR_SPECIALIZATION, "SELECT ID, Name, Description, GarrTypeID, IconFileDataID, GarrFollowerTypeID, Param, Bonus1, " + "Bonus2 FROM garr_specialization WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_SPECIALIZATION, "SELECT MAX(ID) + 1 FROM garr_specialization", CONNECTION_SYNCH); + PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_SPECIALIZATION, "SELECT ID, Name_lang, Description_lang FROM garr_specialization_locale" + " WHERE (`VerifiedBuild` > 0) = ? AND locale = ?", CONNECTION_SYNCH); + + // GarrString.db2 + PrepareStatement(HOTFIX_SEL_GARR_STRING, "SELECT ID, Text FROM garr_string WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_STRING, "SELECT MAX(ID) + 1 FROM garr_string", CONNECTION_SYNCH); + PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_STRING, "SELECT ID, Text_lang FROM garr_string_locale WHERE (`VerifiedBuild` > 0) = ?" + " AND locale = ?", CONNECTION_SYNCH); + // GarrTalentTree.db2 PrepareStatement(HOTFIX_SEL_GARR_TALENT_TREE, "SELECT ID, Name, GarrTypeID, ClassID, MaxTiers, UiOrder, Flags, UiTextureKitID, " "GarrTalentTreeType, PlayerConditionID, FeatureTypeIndex, FeatureSubtypeIndex, CurrencyID FROM garr_talent_tree" @@ -853,6 +1032,76 @@ void HotfixDatabaseConnection::DoPrepareStatements() PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_TALENT_TREE, "SELECT MAX(ID) + 1 FROM garr_talent_tree", CONNECTION_SYNCH); PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_TALENT_TREE, "SELECT ID, Name_lang FROM garr_talent_tree_locale WHERE (`VerifiedBuild` > 0) = ? AND locale = ?", CONNECTION_SYNCH); + // GarrTalent.db2 + PrepareStatement(HOTFIX_SEL_GARR_TALENT, "SELECT Name, Description, ID, GarrTalentTreeID, Tier, UiOrder, IconFileDataID, " + "PlayerConditionID, GarrAbilityID, Flags, TalentType, PrerequisiteTalentID, ResearchCostSource, " + "ActiveDurationSecs, GarrTalentSocketPropertiesID FROM garr_talent WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_TALENT, "SELECT MAX(ID) + 1 FROM garr_talent", CONNECTION_SYNCH); + PREPARE_LOCALE_STMT(HOTFIX_SEL_GARR_TALENT, "SELECT ID, Name_lang, Description_lang FROM garr_talent_locale" + " WHERE (`VerifiedBuild` > 0) = ? AND locale = ?", CONNECTION_SYNCH); + + // GarrTalentRank.db2 + PrepareStatement(HOTFIX_SEL_GARR_TALENT_RANK, "SELECT ID, `Rank`, PerkSpellID, PerkPlayerConditionID, Points, ResearchCost, " + "ResearchCostCurrencyTypesID, ResearchGoldCost, ResearchDurationSecs, RespecCost, RespecCostCurrencyTypesID, " + "RespecGoldCost, RespecDurationSecs, AlternateResearchCost, AlternateResearchCostCurrencyTypesID, " + "AlternateResearchGoldCost, AlternateResearchDurationSecs, GarrTalentID FROM garr_talent_rank" + " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_TALENT_RANK, "SELECT MAX(ID) + 1 FROM garr_talent_rank", CONNECTION_SYNCH); + + // GarrTalentCost.db2 + PrepareStatement(HOTFIX_SEL_GARR_TALENT_COST, "SELECT MoneyQuantity, ID, GarrTalentTreeID, GarrTalentID, RankIndex, " + "GarrTalentRankID, CostType, CurrencyTypesID, CurrencyQuantity FROM garr_talent_cost" + " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_TALENT_COST, "SELECT MAX(ID) + 1 FROM garr_talent_cost", CONNECTION_SYNCH); + + // GarrTalentResearch.db2 + PrepareStatement(HOTFIX_SEL_GARR_TALENT_RESEARCH, "SELECT ID, GoldCost, CurrencyTypesID, CurrencyTypesCost, DurationSecs, " + "RespecGoldCost, RespecCurrencyTypesID, RespecCurrencyTypesCost, RespecDurationSecs FROM garr_talent_research" + " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_TALENT_RESEARCH, "SELECT MAX(ID) + 1 FROM garr_talent_research", CONNECTION_SYNCH); + + // GarrTalentSocketProperties.db2 + PrepareStatement(HOTFIX_SEL_GARR_TALENT_SOCKET_PROPERTIES, "SELECT ID, GarrTalentSocketType, GarrTalentSocketSubtype" + " FROM garr_talent_socket_properties WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_TALENT_SOCKET_PROPERTIES, "SELECT MAX(ID) + 1 FROM garr_talent_socket_properties", CONNECTION_SYNCH); + + // GarrTalentMapPOI.db2 + PrepareStatement(HOTFIX_SEL_GARR_TALENT_MAP_POI, "SELECT ID, Position1, Position2, GarrTalentID FROM garr_talent_map_poi" + " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_TALENT_MAP_POI, "SELECT MAX(ID) + 1 FROM garr_talent_map_poi", CONNECTION_SYNCH); + + // GarrTalentRankGroupEntry.db2 + PrepareStatement(HOTFIX_SEL_GARR_TALENT_RANK_GROUP_ENTRY, "SELECT ID, GarrTalentRankGroupID, GarrTalentRankID" + " FROM garr_talent_rank_group_entry WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_TALENT_RANK_GROUP_ENTRY, "SELECT MAX(ID) + 1 FROM garr_talent_rank_group_entry", CONNECTION_SYNCH); + + // GarrTalentRankGroupResearchMod.db2 + PrepareStatement(HOTFIX_SEL_GARR_TALENT_RANK_GROUP_RESEARCH_MOD, "SELECT ID, ModifierType, PlayerConditionID, ModifierValue, " + "GarrTalentRankGroupID FROM garr_talent_rank_group_research_mod WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_TALENT_RANK_GROUP_RESEARCH_MOD, "SELECT MAX(ID) + 1 FROM garr_talent_rank_group_research_mod", CONNECTION_SYNCH); + + // GarrTalTreeXGarrTalResearch.db2 + PrepareStatement(HOTFIX_SEL_GARR_TAL_TREE_X_GARR_TAL_RESEARCH, "SELECT ID, GarrTalentTreeID, GarrTalentResearchID, OrderIndex" + " FROM garr_tal_tree_x_garr_tal_research WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_TAL_TREE_X_GARR_TAL_RESEARCH, "SELECT MAX(ID) + 1 FROM garr_tal_tree_x_garr_tal_research", CONNECTION_SYNCH); + + // GarrType.db2 + PrepareStatement(HOTFIX_SEL_GARR_TYPE, "SELECT ID, PrimaryCurrencyTypeID, SecondaryCurrencyTypeID, ExpansionID, Flags, AutoFollowerHealRate, " + "MissionCostCurveID, AutoFollowerHealCostMult, MapIDs1, MapIDs2 FROM garr_type WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_TYPE, "SELECT MAX(ID) + 1 FROM garr_type", CONNECTION_SYNCH); + + // GarrUiAnimClassInfo.db2 + PrepareStatement(HOTFIX_SEL_GARR_UI_ANIM_CLASS_INFO, "SELECT ID, ClassID, IsFemale, WalkSpeed, RunAnimID, WalkAnimID, IdleAnimID" + " FROM garr_ui_anim_class_info WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_UI_ANIM_CLASS_INFO, "SELECT MAX(ID) + 1 FROM garr_ui_anim_class_info", CONNECTION_SYNCH); + + // GarrUiAnimRaceInfo.db2 + PrepareStatement(HOTFIX_SEL_GARR_UI_ANIM_RACE_INFO, "SELECT ID, RaceID, RunSpeedModifier, RunAnimSpeedModifier, WalkAnimSpeedModifier, " + "IdleAnimSpeedModifier, PortraitScale, PortraitHeight, PortraitFieldOfView, PortraitCameraPositionX, PortraitCameraPositionY, " + "PortraitCameraPositionZ, PortraitCameraTargetX, PortraitCameraTargetY" + " FROM garr_ui_anim_race_info WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GARR_UI_ANIM_RACE_INFO, "SELECT MAX(ID) + 1 FROM garr_ui_anim_race_info", CONNECTION_SYNCH); + // GemProperties.db2 PrepareStatement(HOTFIX_SEL_GEM_PROPERTIES, "SELECT ID, EnchantId, Type FROM gem_properties WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); PREPARE_MAX_ID_STMT(HOTFIX_SEL_GEM_PROPERTIES, "SELECT MAX(ID) + 1 FROM gem_properties", CONNECTION_SYNCH); @@ -881,6 +1130,15 @@ void HotfixDatabaseConnection::DoPrepareStatements() "Unknown_1002_14, NeighborhoodMapID, SkillLineID FROM gossip_npc_option WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); PREPARE_MAX_ID_STMT(HOTFIX_SEL_GOSSIP_NPC_OPTION, "SELECT MAX(ID) + 1 FROM gossip_npc_option", CONNECTION_SYNCH); + // GossipXGarrTalentTrees.db2 + PrepareStatement(HOTFIX_SEL_GOSSIP_X_GARR_TALENT_TREES, "SELECT ID, GarrTalentTreeID, GossipID" + " FROM gossip_x_garr_talent_trees WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GOSSIP_X_GARR_TALENT_TREES, "SELECT MAX(ID) + 1 FROM gossip_x_garr_talent_trees", CONNECTION_SYNCH); + + // GuildColorBackground.db2 + PrepareStatement(HOTFIX_SEL_GUILD_COLOR_BACKGROUND, "SELECT ID, Red, Blue, Green FROM guild_color_background WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); + PREPARE_MAX_ID_STMT(HOTFIX_SEL_GUILD_COLOR_BACKGROUND, "SELECT MAX(ID) + 1 FROM guild_color_background", CONNECTION_SYNCH); + // GuildColorBackground.db2 PrepareStatement(HOTFIX_SEL_GUILD_COLOR_BACKGROUND, "SELECT ID, Red, Blue, Green FROM guild_color_background WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH); PREPARE_MAX_ID_STMT(HOTFIX_SEL_GUILD_COLOR_BACKGROUND, "SELECT MAX(ID) + 1 FROM guild_color_background", CONNECTION_SYNCH); diff --git a/src/server/database/Database/Implementation/HotfixDatabase.h b/src/server/database/Database/Implementation/HotfixDatabase.h index 3d3f5ccff..c4cfad935 100644 --- a/src/server/database/Database/Implementation/HotfixDatabase.h +++ b/src/server/database/Database/Implementation/HotfixDatabase.h @@ -456,6 +456,9 @@ enum HotfixDatabaseStatements : uint32 HOTFIX_SEL_GARR_CLASS_SPEC_MAX_ID, HOTFIX_SEL_GARR_CLASS_SPEC_LOCALE, + HOTFIX_SEL_GARR_FOLL_SUPPORT_SPELL, + HOTFIX_SEL_GARR_FOLL_SUPPORT_SPELL_MAX_ID, + HOTFIX_SEL_GARR_FOLLOWER, HOTFIX_SEL_GARR_FOLLOWER_MAX_ID, HOTFIX_SEL_GARR_FOLLOWER_LOCALE, @@ -1372,7 +1375,7 @@ enum HotfixDatabaseStatements : uint32 HOTFIX_SEL_WORLD_STATE_EXPRESSION, HOTFIX_SEL_WORLD_STATE_EXPRESSION_MAX_ID, - // Patch's "Wowcommunity" block was here — duplicated entries the target already + // Patch's "Wowcommunity" block was here ? duplicated entries the target already // had earlier in the alphabetically-sorted enum. Removed during the housing-system // import to fix C2374 redefinition errors. @@ -1433,7 +1436,7 @@ enum HotfixDatabaseStatements : uint32 HOTFIX_SEL_WARBAND_SCENE_SOURCE_INFO_MAX_ID, HOTFIX_SEL_WARBAND_SCENE_SOURCE_INFO_LOCALE, - // Patch's EXTERIOR_COMPONENT_* / INITIATIVE_* duplicate block was here — + // Patch's EXTERIOR_COMPONENT_* / INITIATIVE_* duplicate block was here ? // target had these earlier in the alphabetically-sorted enum (lines ~390 and ~540). // Removed during the housing-system import to fix C2374 redefinition errors. @@ -1563,7 +1566,7 @@ enum HotfixDatabaseStatements : uint32 HOTFIX_SEL_GOSSIP_X_GARR_TALENT_TREES, HOTFIX_SEL_GOSSIP_X_GARR_TALENT_TREES_MAX_ID, - // Patch's ROOM_COMPONENT_*_TEXTURE duplicate block was here — target had these + // Patch's ROOM_COMPONENT_*_TEXTURE duplicate block was here ? target had these // earlier (~line 980). Removed during the housing-system import. HOTFIX_SEL_DRIVE_CAPABILITY, diff --git a/src/server/game/DataStores/DB2LoadInfo.h b/src/server/game/DataStores/DB2LoadInfo.h index bc61b962d..2bc481d43 100644 --- a/src/server/game/DataStores/DB2LoadInfo.h +++ b/src/server/game/DataStores/DB2LoadInfo.h @@ -2481,6 +2481,20 @@ struct GarrFollowerLoadInfo static constexpr DB2LoadInfo Instance{ Fields, 34, &GarrFollowerMeta::Instance, HOTFIX_SEL_GARR_FOLLOWER }; }; +struct GarrFollowerLevelXPLoadInfo +{ + static constexpr DB2FieldMeta Fields[5] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_BYTE, .Name = "FollowerLevel" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "GarrFollowerTypeID" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "XpToNextLevel" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "ShipmentXP" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 5, &GarrFollowerLevelXPMeta::Instance, HOTFIX_SEL_GARR_FOLLOWER_LEVEL_XP }; +}; + struct GarrFollowerXAbilityLoadInfo { static constexpr DB2FieldMeta Fields[5] = @@ -2614,6 +2628,24 @@ struct GarrSiteLevelPlotInstLoadInfo static constexpr DB2LoadInfo Instance{ Fields, 6, &GarrSiteLevelPlotInstMeta::Instance, HOTFIX_SEL_GARR_SITE_LEVEL_PLOT_INST }; }; +struct GarrSpecializationLoadInfo +{ + static constexpr DB2FieldMeta Fields[9] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "Name" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "Description" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "GarrTypeID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "IconFileDataID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "GarrFollowerTypeID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Param" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "Bonus1" }, + {.IsSigned = true, .Type = FT_FLOAT, .Name = "Bonus2" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 9, &GarrSpecializationMeta::Instance, HOTFIX_SEL_GARR_SPECIALIZATION }; +}; + struct GarrTalentTreeLoadInfo { static constexpr DB2FieldMeta Fields[13] = @@ -8043,4 +8075,681 @@ struct WarbandSceneSourceInfoLoadInfo static constexpr DB2LoadInfo Instance{ Fields, 4, &WarbandSceneSourceInfoMeta::Instance, HOTFIX_SEL_WARBAND_SCENE_SOURCE_INFO }; }; +//WowCommunity +struct CharShipmentLoadInfo +{ + static constexpr DB2FieldMeta Fields[10] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "ContainerID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "TreasureID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "DummyItemID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Duration" }, + {.IsSigned = true, .Type = FT_INT, .Name = "SpellID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "OnCompleteSpellID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "MaxShipments" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "GarrFollowerID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Flags" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 10, &CharShipmentMeta::Instance, HOTFIX_SEL_CHAR_SHIPMENT }; +}; + +struct CharShipmentContainerLoadInfo +{ + static constexpr DB2FieldMeta Fields[17] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "Description" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "PendingText" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "UiTextureKitID" }, + {.IsSigned = true, .Type = FT_BYTE, .Name = "GarrTypeID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "GarrBuildingType" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "BaseCapacity" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "SmallDisplayInfoID" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "MediumDisplayInfoID" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "LargeDisplayInfoID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "WorkingSpellVisualID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "CompleteSpellVisualID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "WorkingDisplayInfoID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "MediumThreshold" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "LargeThreshold" }, + {.IsSigned = true, .Type = FT_BYTE, .Name = "Faction" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "CrossFactionID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 17, &CharShipmentContainerMeta::Instance, HOTFIX_SEL_CHAR_SHIPMENT_CONTAINER }; +}; + +struct GarrAbilityCategoryLoadInfo +{ + static constexpr DB2FieldMeta Fields[2] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "Name" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 2, &GarrAbilityCategoryMeta::Instance, HOTFIX_SEL_GARR_ABILITY_CATEGORY }; +}; + +struct GarrAbilityEffectLoadInfo +{ + static constexpr DB2FieldMeta Fields[12] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "GarrAbilityID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "EffectType" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "TargetMask" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Amount" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "CombatWeightBase" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "CombatWeightMax" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "ActionValueFlat" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "AbilityAction" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "MiscValueA" }, + {.IsSigned = true, .Type = FT_INT, .Name = "ActionHours" }, + {.IsSigned = true, .Type = FT_INT, .Name = "ActionRecordID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 12, &GarrAbilityEffectMeta::Instance, HOTFIX_SEL_GARR_ABILITY_EFFECT }; +}; + +struct GarrAutoCombatantLoadInfo +{ + static constexpr DB2FieldMeta Fields[10] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Attack" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Health" }, + {.IsSigned = true, .Type = FT_INT, .Name = "MaxHealth" }, + {.IsSigned = true, .Type = FT_INT, .Name = "AutoAttackSpellID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Role" }, + {.IsSigned = true, .Type = FT_INT, .Name = "BoardIndex" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrEncounterID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrAutoSpellID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Flags" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 10, &GarrAutoCombatantMeta::Instance, HOTFIX_SEL_GARR_AUTO_COMBATANT }; +}; + +struct GarrAutoSpellLoadInfo +{ + static constexpr DB2FieldMeta Fields[8] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "Name" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "Description" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Cooldown" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Duration" }, + {.IsSigned = true, .Type = FT_INT, .Name = "SchoolMask" }, + {.IsSigned = true, .Type = FT_INT, .Name = "SpellVisualID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Flags" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 8, &GarrAutoSpellMeta::Instance, HOTFIX_SEL_GARR_AUTO_SPELL }; +}; + +struct GarrAutoSpellEffectLoadInfo +{ + static constexpr DB2FieldMeta Fields[8] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrAutoSpellID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "EffectType" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "Targets" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "Amount" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "MiscType" }, + {.IsSigned = true, .Type = FT_INT, .Name = "MiscValue" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Period" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 8, &GarrAutoSpellEffectMeta::Instance, HOTFIX_SEL_GARR_AUTO_SPELL_EFFECT }; +}; + +struct GarrEncounterSetXEncounterLoadInfo +{ + static constexpr DB2FieldMeta Fields[3] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrEncounterID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrEncounterSetID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 3, &GarrEncounterSetXEncounterMeta::Instance, HOTFIX_SEL_GARR_ENCOUNTER_SET_X_ENCOUNTER }; +}; + +struct GarrFollowerSetXFollowerLoadInfo +{ + static constexpr DB2FieldMeta Fields[3] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrFollowerID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrFollowerSetID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 3, &GarrFollowerSetXFollowerMeta::Instance, HOTFIX_SEL_GARR_FOLLOWER_SET_X_FOLLOWER }; +}; + +struct GarrBuildingDoodadSetLoadInfo +{ + static constexpr DB2FieldMeta Fields[6] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "GarrBuildingID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "HordeDoodadSetID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "AllianceDoodadSetID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "HordeAltDoodadSetID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "AllianceAltDoodadSetID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 6, &GarrBuildingDoodadSetMeta::Instance, HOTFIX_SEL_GARR_BUILDING_DOODAD_SET }; +}; + +struct GarrClassSpecPlayerCondLoadInfo +{ + static constexpr DB2FieldMeta Fields[7] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "ClassSpec" }, + {.IsSigned = false, .Type = FT_INT, .Name = "OrderIndex" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrClassSpecID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "PlayerConditionID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrStringID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "Flags" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 7, &GarrClassSpecPlayerCondMeta::Instance, HOTFIX_SEL_GARR_CLASS_SPEC_PLAYER_COND }; +}; + +struct GarrEncounterLoadInfo +{ + static constexpr DB2FieldMeta Fields[9] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "Name" }, + {.IsSigned = true, .Type = FT_INT, .Name = "CreatureID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "CreatureDisplayInfoID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "UiAnimHeight" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "UiAnimScale" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "UiTextureScale" }, + {.IsSigned = true, .Type = FT_INT, .Name = "EnvGarrMechanicTypeID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrEncounterSetID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 9, &GarrEncounterMeta::Instance, HOTFIX_SEL_GARR_ENCOUNTER }; +}; + +struct GarrEncounterXMechanicLoadInfo +{ + static constexpr DB2FieldMeta Fields[4] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrMechanicID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "GarrMechanicSetID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrEncounterID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 4, &GarrEncounterXMechanicMeta::Instance, HOTFIX_SEL_GARR_ENCOUNTER_X_MECHANIC }; +}; + +struct GarrFollItemSetMemberLoadInfo +{ + static constexpr DB2FieldMeta Fields[5] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrFollItemSetID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "ItemSlot" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "ItemModifiedAppearanceID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrFollowerID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 5, &GarrFollItemSetMemberMeta::Instance, HOTFIX_SEL_GARR_FOLL_ITEM_SET_MEMBER }; +}; + +struct GarrFollSupportSpellLoadInfo +{ + static constexpr DB2FieldMeta Fields[5] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "HordeSpellID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "AllianceSpellID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "OrderIndex" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrFollowerID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 5, &GarrFollSupportSpellMeta::Instance, HOTFIX_SEL_GARR_FOLL_SUPPORT_SPELL }; +}; + +struct GarrFollowerQualityLoadInfo +{ + static constexpr DB2FieldMeta Fields[8] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "XpThreshold" }, + {.IsSigned = false, .Type = FT_INT, .Name = "QualityItemID" }, + {.IsSigned = true, .Type = FT_BYTE, .Name = "Quality" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "AbilityCount" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "TraitCount" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "GarrFollowerTypeID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "ClassSpecID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 8, &GarrFollowerQualityMeta::Instance, HOTFIX_SEL_GARR_FOLLOWER_QUALITY }; +}; + +struct GarrFollowerTypeLoadInfo +{ + static constexpr DB2FieldMeta Fields[8] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_BYTE, .Name = "GarrTypeID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "MaxFollowers" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "MaxFollowerBuildingType" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "MaxItemLevel" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "LevelRangeBias" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "ItemLevelRangeBias" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Flags" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 8, &GarrFollowerTypeMeta::Instance, HOTFIX_SEL_GARR_FOLLOWER_TYPE }; +}; + +struct GarrFollowerUICreatureLoadInfo +{ + static constexpr DB2FieldMeta Fields[7] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "Race" }, + {.IsSigned = true, .Type = FT_BYTE, .Name = "Gender" }, + {.IsSigned = true, .Type = FT_INT, .Name = "CreatureID" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "Scale" }, + {.IsSigned = true, .Type = FT_INT, .Name = "FileDataID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrFollowerID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 7, &GarrFollowerUICreatureMeta::Instance, HOTFIX_SEL_GARR_FOLLOWER_UI_CREATURE }; +}; + +struct GarrItemLevelUpgradeDataLoadInfo +{ + static constexpr DB2FieldMeta Fields[5] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Operation" }, + {.IsSigned = true, .Type = FT_INT, .Name = "MinItemLevel" }, + {.IsSigned = true, .Type = FT_INT, .Name = "MaxItemLevel" }, + {.IsSigned = true, .Type = FT_BYTE, .Name = "FollowerTypeID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 5, &GarrItemLevelUpgradeDataMeta::Instance, HOTFIX_SEL_GARR_ITEM_LEVEL_UPGRADE_DATA }; +}; + +struct GarrMechanicLoadInfo +{ + static constexpr DB2FieldMeta Fields[4] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrMechanicTypeID" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "Factor" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrAbilityID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 4, &GarrMechanicMeta::Instance, HOTFIX_SEL_GARR_MECHANIC }; +}; + +struct GarrMechanicTypeLoadInfo +{ + static constexpr DB2FieldMeta Fields[5] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "Name" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "Description" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrAbilityCategoryID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "Category" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 5, &GarrMechanicTypeMeta::Instance, HOTFIX_SEL_GARR_MECHANIC_TYPE }; +}; + +struct GarrMechanicSetXMechanicLoadInfo +{ + static constexpr DB2FieldMeta Fields[3] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrMechanicID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrMechanicSetID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 3, &GarrMechanicSetXMechanicMeta::Instance, HOTFIX_SEL_GARR_MECHANIC_SET_X_MECHANIC }; +}; + +struct GarrMissionSetLoadInfo +{ + static constexpr DB2FieldMeta Fields[7] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_BYTE, .Name = "GarrTypeID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "MissionCostCurrencyTypesID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "ContentTuningID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "MapID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Flags" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Priority" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 7, &GarrMissionSetMeta::Instance, HOTFIX_SEL_GARR_MISSION_SET }; +}; + +struct GarrMissionTypeLoadInfo +{ + static constexpr DB2FieldMeta Fields[4] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "Name" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "UiTextureAtlasMemberID" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "UiTextureKitID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 4, &GarrMissionTypeMeta::Instance, HOTFIX_SEL_GARR_MISSION_TYPE }; +}; + +struct GarrMissionTextureLoadInfo +{ + static constexpr DB2FieldMeta Fields[4] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "PosX" }, + {.IsSigned = true, .Type = FT_FLOAT, .Name = "PosY" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "UiTextureAtlasMemberID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 4, &GarrMissionTextureMeta::Instance, HOTFIX_SEL_GARR_MISSION_TEXTURE }; +}; + +struct GarrMissionXEncounterLoadInfo +{ + static constexpr DB2FieldMeta Fields[6] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrEncounterID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrMissionSetEncounterID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "CombatWeightBase" }, + {.IsSigned = true, .Type = FT_BYTE, .Name = "CombatWeightMax" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrMissionID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 6, &GarrMissionXEncounterMeta::Instance, HOTFIX_SEL_GARR_MISSION_X_ENCOUNTER }; +}; + +struct GarrMissionXFollowerLoadInfo +{ + static constexpr DB2FieldMeta Fields[5] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrFollowerID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrFollowerTypeID" }, + {.IsSigned = true, .Type = FT_BYTE, .Name = "BoardIndex" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrMissionID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 5, &GarrMissionXFollowerMeta::Instance, HOTFIX_SEL_GARR_MISSION_X_FOLLOWER }; +}; + +struct GarrMssnBonusAbilityLoadInfo +{ + static constexpr DB2FieldMeta Fields[6] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "GarrMssnBonusAbilityType" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "MssnSort" }, + {.IsSigned = false, .Type = FT_SHORT, .Name = "GarrAbilityID" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "Modifier" }, + {.IsSigned = true, .Type = FT_INT, .Name = "MissionSetID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 6, &GarrMssnBonusAbilityMeta::Instance, HOTFIX_SEL_GARR_MSSN_BONUS_ABILITY }; +}; + +struct GarrPlotUICategoryLoadInfo +{ + static constexpr DB2FieldMeta Fields[3] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "Name" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "PlotType" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 3, &GarrPlotUICategoryMeta::Instance, HOTFIX_SEL_GARR_PLOT_UI_CATEGORY }; +}; + +struct GarrStringLoadInfo +{ + static constexpr DB2FieldMeta Fields[2] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "Text" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 2, &GarrStringMeta::Instance, HOTFIX_SEL_GARR_STRING }; +}; + +struct GarrTypeLoadInfo +{ + static constexpr DB2FieldMeta Fields[10] = + { + {.IsSigned = false, .Type = FT_BYTE, .Name = "ID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "PrimaryCurrencyTypeID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "SecondaryCurrencyTypeID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "ExpansionID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Flags" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "AutoFollowerHealRate" }, + {.IsSigned = true, .Type = FT_INT, .Name = "MissionCostCurveID" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "AutoFollowerHealCostMult" }, + {.IsSigned = true, .Type = FT_INT, .Name = "MapIDs1" }, + {.IsSigned = true, .Type = FT_INT, .Name = "MapIDs2" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 10, &GarrTypeMeta::Instance, HOTFIX_SEL_GARR_TYPE }; +}; + +struct GarrUiAnimClassInfoLoadInfo +{ + static constexpr DB2FieldMeta Fields[7] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "ClassID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "IsFemale" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "WalkSpeed" }, + {.IsSigned = false, .Type = FT_INT, .Name = "RunAnimID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "WalkAnimID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "IdleAnimID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 7, &GarrUiAnimClassInfoMeta::Instance, HOTFIX_SEL_GARR_UI_ANIM_CLASS_INFO }; +}; + +struct GarrUiAnimRaceInfoLoadInfo +{ + static constexpr DB2FieldMeta Fields[14] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_BYTE, .Name = "RaceID" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "RunSpeedModifier" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "RunAnimSpeedModifier" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "WalkAnimSpeedModifier" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "IdleAnimSpeedModifier" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "PortraitScale" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "PortraitHeight" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "PortraitFieldOfView" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "PortraitCameraPositionX" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "PortraitCameraPositionY" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "PortraitCameraPositionZ" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "PortraitCameraTargetX" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "PortraitCameraTargetY" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 14, &GarrUiAnimRaceInfoMeta::Instance, HOTFIX_SEL_GARR_UI_ANIM_RACE_INFO }; +}; + +struct GarrTalentLoadInfo +{ + static constexpr DB2FieldMeta Fields[15] = + { + {.IsSigned = false, .Type = FT_STRING, .Name = "Name" }, + {.IsSigned = false, .Type = FT_STRING, .Name = "Description" }, + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrTalentTreeID" }, + {.IsSigned = true, .Type = FT_BYTE, .Name = "Tier" }, + {.IsSigned = true, .Type = FT_BYTE, .Name = "UiOrder" }, + {.IsSigned = true, .Type = FT_INT, .Name = "IconFileDataID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "PlayerConditionID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrAbilityID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Flags" }, + {.IsSigned = true, .Type = FT_INT, .Name = "TalentType" }, + {.IsSigned = true, .Type = FT_INT, .Name = "PrerequisiteTalentID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "ResearchCostSource" }, + {.IsSigned = true, .Type = FT_INT, .Name = "ActiveDurationSecs" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrTalentSocketPropertiesID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 15, &GarrTalentMeta::Instance, HOTFIX_SEL_GARR_TALENT }; +}; + +struct GarrTalentRankLoadInfo +{ + static constexpr DB2FieldMeta Fields[18] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "Rank" }, + {.IsSigned = true, .Type = FT_INT, .Name = "PerkSpellID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "PerkPlayerConditionID" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "Points" }, + {.IsSigned = true, .Type = FT_INT, .Name = "ResearchCost" }, + {.IsSigned = true, .Type = FT_INT, .Name = "ResearchCostCurrencyTypesID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "ResearchGoldCost" }, + {.IsSigned = true, .Type = FT_INT, .Name = "ResearchDurationSecs" }, + {.IsSigned = true, .Type = FT_INT, .Name = "RespecCost" }, + {.IsSigned = true, .Type = FT_INT, .Name = "RespecCostCurrencyTypesID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "RespecGoldCost" }, + {.IsSigned = true, .Type = FT_INT, .Name = "RespecDurationSecs" }, + {.IsSigned = true, .Type = FT_INT, .Name = "AlternateResearchCost" }, + {.IsSigned = true, .Type = FT_INT, .Name = "AlternateResearchCostCurrencyTypesID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "AlternateResearchGoldCost" }, + {.IsSigned = true, .Type = FT_INT, .Name = "AlternateResearchDurationSecs" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrTalentID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 18, &GarrTalentRankMeta::Instance, HOTFIX_SEL_GARR_TALENT_RANK }; +}; + +struct GarrTalentCostLoadInfo +{ + static constexpr DB2FieldMeta Fields[9] = + { + {.IsSigned = false, .Type = FT_LONG, .Name = "MoneyQuantity" }, + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrTalentTreeID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrTalentID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "RankIndex" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrTalentRankID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "CostType" }, + {.IsSigned = true, .Type = FT_INT, .Name = "CurrencyTypesID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "CurrencyQuantity" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 9, &GarrTalentCostMeta::Instance, HOTFIX_SEL_GARR_TALENT_COST }; +}; + +struct GarrTalentResearchLoadInfo +{ + static constexpr DB2FieldMeta Fields[9] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GoldCost" }, + {.IsSigned = true, .Type = FT_INT, .Name = "CurrencyTypesID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "CurrencyTypesCost" }, + {.IsSigned = true, .Type = FT_INT, .Name = "DurationSecs" }, + {.IsSigned = true, .Type = FT_INT, .Name = "RespecGoldCost" }, + {.IsSigned = true, .Type = FT_INT, .Name = "RespecCurrencyTypesID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "RespecCurrencyTypesCost" }, + {.IsSigned = true, .Type = FT_INT, .Name = "RespecDurationSecs" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 9, &GarrTalentResearchMeta::Instance, HOTFIX_SEL_GARR_TALENT_RESEARCH }; +}; + +struct GarrTalentSocketPropertiesLoadInfo +{ + static constexpr DB2FieldMeta Fields[3] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrTalentSocketType" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrTalentSocketSubtype" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 3, &GarrTalentSocketPropertiesMeta::Instance, HOTFIX_SEL_GARR_TALENT_SOCKET_PROPERTIES }; +}; + +struct GarrTalentMapPOILoadInfo +{ + static constexpr DB2FieldMeta Fields[4] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "Position1" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "Position2" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrTalentID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 4, &GarrTalentMapPOIMeta::Instance, HOTFIX_SEL_GARR_TALENT_MAP_POI }; +}; + +struct GarrTalentRankGroupEntryLoadInfo +{ + static constexpr DB2FieldMeta Fields[3] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrTalentRankGroupID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrTalentRankID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 3, &GarrTalentRankGroupEntryMeta::Instance, HOTFIX_SEL_GARR_TALENT_RANK_GROUP_ENTRY }; +}; + +struct GarrTalentRankGroupResearchModLoadInfo +{ + static constexpr DB2FieldMeta Fields[5] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "ModifierType" }, + {.IsSigned = true, .Type = FT_INT, .Name = "PlayerConditionID" }, + {.IsSigned = false, .Type = FT_FLOAT, .Name = "ModifierValue" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GarrTalentRankGroupID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 5, &GarrTalentRankGroupResearchModMeta::Instance, HOTFIX_SEL_GARR_TALENT_RANK_GROUP_RESEARCH_MOD }; +}; + +struct GarrTalTreeXGarrTalResearchLoadInfo +{ + static constexpr DB2FieldMeta Fields[4] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrTalentTreeID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrTalentResearchID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "OrderIndex" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 4, &GarrTalTreeXGarrTalResearchMeta::Instance, HOTFIX_SEL_GARR_TAL_TREE_X_GARR_TAL_RESEARCH }; +}; + +struct GossipXGarrTalentTreesLoadInfo +{ + static constexpr DB2FieldMeta Fields[3] = + { + {.IsSigned = false, .Type = FT_INT, .Name = "ID" }, + {.IsSigned = true, .Type = FT_INT, .Name = "GarrTalentTreeID" }, + {.IsSigned = false, .Type = FT_INT, .Name = "GossipID" }, + }; + + static constexpr DB2LoadInfo Instance{ Fields, 3, &GossipXGarrTalentTreesMeta::Instance, HOTFIX_SEL_GOSSIP_X_GARR_TALENT_TREES }; +}; #endif // TRINITYCORE_DB2_LOAD_INFO_H diff --git a/src/server/game/DataStores/DB2Stores.cpp b/src/server/game/DataStores/DB2Stores.cpp index aa3d51bfa..439d84156 100644 --- a/src/server/game/DataStores/DB2Stores.cpp +++ b/src/server/game/DataStores/DB2Stores.cpp @@ -94,6 +94,8 @@ DB2Storage sCharBaseInfoStore("CharBaseInfo DB2Storage sCharTitlesStore("CharTitles.db2", &CharTitlesLoadInfo::Instance); DB2Storage sCharacterLoadoutStore("CharacterLoadout.db2", &CharacterLoadoutLoadInfo::Instance); DB2Storage sCharacterLoadoutItemStore("CharacterLoadoutItem.db2", &CharacterLoadoutItemLoadInfo::Instance); +DB2Storage sCharShipmentStore("CharShipment.db2", &CharShipmentLoadInfo::Instance); +DB2Storage sCharShipmentContainerStore("CharShipmentContainer.db2", &CharShipmentContainerLoadInfo::Instance); DB2Storage sChatChannelsStore("ChatChannels.db2", &ChatChannelsLoadInfo::Instance); DB2Storage sChrClassUIDisplayStore("ChrClassUIDisplay.db2", &ChrClassUiDisplayLoadInfo::Instance); DB2Storage sChrClassesStore("ChrClasses.db2", &ChrClassesLoadInfo::Instance); @@ -162,23 +164,66 @@ DB2Storage sGameObjectDisplayInfoStore("Gam DB2Storage sGameObjectLabelStore("GameObjectLabel.db2", &GameobjectLabelLoadInfo::Instance); DB2Storage sGameObjectsStore("GameObjects.db2", &GameobjectsLoadInfo::Instance); DB2Storage sGarrAbilityStore("GarrAbility.db2", &GarrAbilityLoadInfo::Instance); +DB2Storage sGarrAbilityCategoryStore("GarrAbilityCategory.db2", &GarrAbilityCategoryLoadInfo::Instance); +DB2Storage sGarrAbilityEffectStore("GarrAbilityEffect.db2", &GarrAbilityEffectLoadInfo::Instance); +DB2Storage sGarrAutoCombatantStore("GarrAutoCombatant.db2", &GarrAutoCombatantLoadInfo::Instance); +DB2Storage sGarrAutoSpellStore("GarrAutoSpell.db2", &GarrAutoSpellLoadInfo::Instance); +DB2Storage sGarrAutoSpellEffectStore("GarrAutoSpellEffect.db2", &GarrAutoSpellEffectLoadInfo::Instance); DB2Storage sGarrBuildingStore("GarrBuilding.db2", &GarrBuildingLoadInfo::Instance); +DB2Storage sGarrBuildingDoodadSetStore("GarrBuildingDoodadSet.db2", &GarrBuildingDoodadSetLoadInfo::Instance); DB2Storage sGarrBuildingPlotInstStore("GarrBuildingPlotInst.db2", &GarrBuildingPlotInstLoadInfo::Instance); DB2Storage sGarrClassSpecStore("GarrClassSpec.db2", &GarrClassSpecLoadInfo::Instance); +DB2Storage sGarrClassSpecPlayerCondStore("GarrClassSpecPlayerCond.db2", &GarrClassSpecPlayerCondLoadInfo::Instance); +DB2Storage sGarrEncounterStore("GarrEncounter.db2", &GarrEncounterLoadInfo::Instance); +DB2Storage sGarrEncounterSetXEncounterStore("GarrEncounterSetXEncounter.db2", &GarrEncounterSetXEncounterLoadInfo::Instance); +DB2Storage sGarrEncounterXMechanicStore("GarrEncounterXMechanic.db2", &GarrEncounterXMechanicLoadInfo::Instance); +DB2Storage sGarrFollItemSetMemberStore("GarrFollItemSetMember.db2", &GarrFollItemSetMemberLoadInfo::Instance); +DB2Storage sGarrFollSupportSpellStore("GarrFollSupportSpell.db2", &GarrFollSupportSpellLoadInfo::Instance); DB2Storage sGarrFollowerStore("GarrFollower.db2", &GarrFollowerLoadInfo::Instance); +DB2Storage sGarrFollowerLevelXPStore("GarrFollowerLevelXP.db2", &GarrFollowerLevelXPLoadInfo::Instance); +DB2Storage sGarrFollowerQualityStore("GarrFollowerQuality.db2", &GarrFollowerQualityLoadInfo::Instance); +DB2Storage sGarrFollowerTypeStore("GarrFollowerType.db2", &GarrFollowerTypeLoadInfo::Instance); +DB2Storage sGarrFollowerSetXFollowerStore("GarrFollowerSetXFollower.db2", &GarrFollowerSetXFollowerLoadInfo::Instance); +DB2Storage sGarrFollowerUICreatureStore("GarrFollowerUICreature.db2", &GarrFollowerUICreatureLoadInfo::Instance); DB2Storage sGarrFollowerXAbilityStore("GarrFollowerXAbility.db2", &GarrFollowerXAbilityLoadInfo::Instance); +DB2Storage sGarrItemLevelUpgradeDataStore("GarrItemLevelUpgradeData.db2", &GarrItemLevelUpgradeDataLoadInfo::Instance); +DB2Storage sGarrMechanicStore("GarrMechanic.db2", &GarrMechanicLoadInfo::Instance); +DB2Storage sGarrMechanicSetXMechanicStore("GarrMechanicSetXMechanic.db2", &GarrMechanicSetXMechanicLoadInfo::Instance); +DB2Storage sGarrMechanicTypeStore("GarrMechanicType.db2", &GarrMechanicTypeLoadInfo::Instance); DB2Storage sGarrMissionStore("GarrMission.db2", &GarrMissionLoadInfo::Instance); +DB2Storage sGarrMissionSetStore("GarrMissionSet.db2", &GarrMissionSetLoadInfo::Instance); +DB2Storage sGarrMissionTextureStore("GarrMissionTexture.db2", &GarrMissionTextureLoadInfo::Instance); +DB2Storage sGarrMissionTypeStore("GarrMissionType.db2", &GarrMissionTypeLoadInfo::Instance); +DB2Storage sGarrMissionXEncounterStore("GarrMissionXEncounter.db2", &GarrMissionXEncounterLoadInfo::Instance); +DB2Storage sGarrMissionXFollowerStore("GarrMissionXFollower.db2", &GarrMissionXFollowerLoadInfo::Instance); +DB2Storage sGarrMssnBonusAbilityStore("GarrMssnBonusAbility.db2", &GarrMssnBonusAbilityLoadInfo::Instance); DB2Storage sGarrPlotStore("GarrPlot.db2", &GarrPlotLoadInfo::Instance); DB2Storage sGarrPlotBuildingStore("GarrPlotBuilding.db2", &GarrPlotBuildingLoadInfo::Instance); DB2Storage sGarrPlotInstanceStore("GarrPlotInstance.db2", &GarrPlotInstanceLoadInfo::Instance); +DB2Storage sGarrPlotUICategoryStore("GarrPlotUICategory.db2", &GarrPlotUICategoryLoadInfo::Instance); DB2Storage sGarrSiteLevelStore("GarrSiteLevel.db2", &GarrSiteLevelLoadInfo::Instance); DB2Storage sGarrSiteLevelPlotInstStore("GarrSiteLevelPlotInst.db2", &GarrSiteLevelPlotInstLoadInfo::Instance); +DB2Storage sGarrSpecializationStore("GarrSpecialization.db2", &GarrSpecializationLoadInfo::Instance); +DB2Storage sGarrStringStore("GarrString.db2", &GarrStringLoadInfo::Instance); DB2Storage sGarrTalentTreeStore("GarrTalentTree.db2", &GarrTalentTreeLoadInfo::Instance); +DB2Storage sGarrTalentStore("GarrTalent.db2", &GarrTalentLoadInfo::Instance); +DB2Storage sGarrTalentRankStore("GarrTalentRank.db2", &GarrTalentRankLoadInfo::Instance); +DB2Storage sGarrTalentCostStore("GarrTalentCost.db2", &GarrTalentCostLoadInfo::Instance); +DB2Storage sGarrTalentResearchStore("GarrTalentResearch.db2", &GarrTalentResearchLoadInfo::Instance); +DB2Storage sGarrTalentSocketPropertiesStore("GarrTalentSocketProperties.db2", &GarrTalentSocketPropertiesLoadInfo::Instance); +DB2Storage sGarrTalentMapPOIStore("GarrTalentMapPOI.db2", &GarrTalentMapPOILoadInfo::Instance); +DB2Storage sGarrTalentRankGroupEntryStore("GarrTalentRankGroupEntry.db2", &GarrTalentRankGroupEntryLoadInfo::Instance); +DB2Storage sGarrTalentRankGroupResearchModStore("GarrTalentRankGroupResearchMod.db2", &GarrTalentRankGroupResearchModLoadInfo::Instance); +DB2Storage sGarrTalTreeXGarrTalResearchStore("GarrTalTreeXGarrTalResearch.db2", &GarrTalTreeXGarrTalResearchLoadInfo::Instance); +DB2Storage sGarrTypeStore("GarrType.db2", &GarrTypeLoadInfo::Instance); +DB2Storage sGarrUiAnimClassInfoStore("GarrUiAnimClassInfo.db2", &GarrUiAnimClassInfoLoadInfo::Instance); +DB2Storage sGarrUiAnimRaceInfoStore("GarrUiAnimRaceInfo.db2", &GarrUiAnimRaceInfoLoadInfo::Instance); DB2Storage sGemPropertiesStore("GemProperties.db2", &GemPropertiesLoadInfo::Instance); DB2Storage sGlobalCurveStore("GlobalCurve.db2", &GlobalCurveLoadInfo::Instance); DB2Storage sGlyphBindableSpellStore("GlyphBindableSpell.db2", &GlyphBindableSpellLoadInfo::Instance); DB2Storage sGlyphPropertiesStore("GlyphProperties.db2", &GlyphPropertiesLoadInfo::Instance); DB2Storage sGlyphRequiredSpecStore("GlyphRequiredSpec.db2", &GlyphRequiredSpecLoadInfo::Instance); +DB2Storage sGossipXGarrTalentTreesStore("GossipXGarrTalentTrees.db2", &GossipXGarrTalentTreesLoadInfo::Instance); DB2Storage sGossipNPCOptionStore("GossipNPCOption.db2", &GossipNpcOptionLoadInfo::Instance); DB2Storage sGuildColorBackgroundStore("GuildColorBackground.db2", &GuildColorBackgroundLoadInfo::Instance); DB2Storage sGuildColorBorderStore("GuildColorBorder.db2", &GuildColorBorderLoadInfo::Instance); @@ -782,6 +827,8 @@ uint32 DB2Manager::LoadStores(std::string const& dataPath, LocaleConstant defaul LOAD_DB2(sCharTitlesStore); LOAD_DB2(sCharacterLoadoutStore); LOAD_DB2(sCharacterLoadoutItemStore); + LOAD_DB2(sCharShipmentStore); + LOAD_DB2(sCharShipmentContainerStore); LOAD_DB2(sChatChannelsStore); LOAD_DB2(sChrClassUIDisplayStore); LOAD_DB2(sChrClassesStore); @@ -850,23 +897,66 @@ uint32 DB2Manager::LoadStores(std::string const& dataPath, LocaleConstant defaul LOAD_DB2(sGameObjectDisplayInfoStore); LOAD_DB2(sGameObjectLabelStore); LOAD_DB2(sGarrAbilityStore); + LOAD_DB2(sGarrAbilityCategoryStore); + LOAD_DB2(sGarrAbilityEffectStore); + LOAD_DB2(sGarrAutoCombatantStore); + LOAD_DB2(sGarrAutoSpellStore); + LOAD_DB2(sGarrAutoSpellEffectStore); LOAD_DB2(sGarrBuildingStore); + LOAD_DB2(sGarrBuildingDoodadSetStore); LOAD_DB2(sGarrBuildingPlotInstStore); LOAD_DB2(sGarrClassSpecStore); + LOAD_DB2(sGarrClassSpecPlayerCondStore); + LOAD_DB2(sGarrEncounterStore); + LOAD_DB2(sGarrEncounterSetXEncounterStore); + LOAD_DB2(sGarrEncounterXMechanicStore); + LOAD_DB2(sGarrFollItemSetMemberStore); + LOAD_DB2(sGarrFollSupportSpellStore); LOAD_DB2(sGarrFollowerStore); + LOAD_DB2(sGarrFollowerLevelXPStore); + LOAD_DB2(sGarrFollowerQualityStore); + LOAD_DB2(sGarrFollowerTypeStore); + LOAD_DB2(sGarrFollowerSetXFollowerStore); + LOAD_DB2(sGarrFollowerUICreatureStore); LOAD_DB2(sGarrFollowerXAbilityStore); + LOAD_DB2(sGarrItemLevelUpgradeDataStore); + LOAD_DB2(sGarrMechanicStore); + LOAD_DB2(sGarrMechanicSetXMechanicStore); + LOAD_DB2(sGarrMechanicTypeStore); LOAD_DB2(sGarrMissionStore); + LOAD_DB2(sGarrMissionSetStore); + LOAD_DB2(sGarrMissionTextureStore); + LOAD_DB2(sGarrMissionTypeStore); + LOAD_DB2(sGarrMissionXEncounterStore); + LOAD_DB2(sGarrMissionXFollowerStore); + LOAD_DB2(sGarrMssnBonusAbilityStore); LOAD_DB2(sGarrPlotStore); LOAD_DB2(sGarrPlotBuildingStore); LOAD_DB2(sGarrPlotInstanceStore); + LOAD_DB2(sGarrPlotUICategoryStore); LOAD_DB2(sGarrSiteLevelStore); LOAD_DB2(sGarrSiteLevelPlotInstStore); + LOAD_DB2(sGarrSpecializationStore); + LOAD_DB2(sGarrStringStore); LOAD_DB2(sGarrTalentTreeStore); + LOAD_DB2(sGarrTalentStore); + LOAD_DB2(sGarrTalentRankStore); + LOAD_DB2(sGarrTalentCostStore); + LOAD_DB2(sGarrTalentResearchStore); + LOAD_DB2(sGarrTalentSocketPropertiesStore); + LOAD_DB2(sGarrTalentMapPOIStore); + LOAD_DB2(sGarrTalentRankGroupEntryStore); + LOAD_DB2(sGarrTalentRankGroupResearchModStore); + LOAD_DB2(sGarrTalTreeXGarrTalResearchStore); + LOAD_DB2(sGarrTypeStore); + LOAD_DB2(sGarrUiAnimClassInfoStore); + LOAD_DB2(sGarrUiAnimRaceInfoStore); LOAD_DB2(sGemPropertiesStore); LOAD_DB2(sGlobalCurveStore); LOAD_DB2(sGlyphBindableSpellStore); LOAD_DB2(sGlyphPropertiesStore); LOAD_DB2(sGlyphRequiredSpecStore); + LOAD_DB2(sGossipXGarrTalentTreesStore); LOAD_DB2(sGossipNPCOptionStore); LOAD_DB2(sGuildColorBackgroundStore); LOAD_DB2(sGuildColorBorderStore); diff --git a/src/server/game/DataStores/DB2Stores.h b/src/server/game/DataStores/DB2Stores.h index af38e6d5c..80ee1acb8 100644 --- a/src/server/game/DataStores/DB2Stores.h +++ b/src/server/game/DataStores/DB2Stores.h @@ -75,6 +75,8 @@ TC_GAME_API extern DB2Storage sChallengeMo TC_GAME_API extern DB2Storage sCharTitlesStore; TC_GAME_API extern DB2Storage sCharacterLoadoutStore; TC_GAME_API extern DB2Storage sCharacterLoadoutItemStore; +TC_GAME_API extern DB2Storage sCharShipmentStore; +TC_GAME_API extern DB2Storage sCharShipmentContainerStore; TC_GAME_API extern DB2Storage sChatChannelsStore; TC_GAME_API extern DB2Storage sChrClassesStore; TC_GAME_API extern DB2Storage sChrCustomizationReqStore; @@ -125,20 +127,63 @@ TC_GAME_API extern DB2Storage sGameObjectA TC_GAME_API extern DB2Storage sGameObjectDisplayInfoStore; TC_GAME_API extern DB2Storage sGameObjectsStore; TC_GAME_API extern DB2Storage sGarrAbilityStore; +TC_GAME_API extern DB2Storage sGarrAbilityCategoryStore; +TC_GAME_API extern DB2Storage sGarrAbilityEffectStore; +TC_GAME_API extern DB2Storage sGarrAutoCombatantStore; +TC_GAME_API extern DB2Storage sGarrAutoSpellStore; +TC_GAME_API extern DB2Storage sGarrAutoSpellEffectStore; TC_GAME_API extern DB2Storage sGarrBuildingStore; +TC_GAME_API extern DB2Storage sGarrBuildingDoodadSetStore; TC_GAME_API extern DB2Storage sGarrBuildingPlotInstStore; TC_GAME_API extern DB2Storage sGarrClassSpecStore; +TC_GAME_API extern DB2Storage sGarrClassSpecPlayerCondStore; +TC_GAME_API extern DB2Storage sGarrEncounterStore; +TC_GAME_API extern DB2Storage sGarrEncounterSetXEncounterStore; +TC_GAME_API extern DB2Storage sGarrEncounterXMechanicStore; +TC_GAME_API extern DB2Storage sGarrFollItemSetMemberStore; +TC_GAME_API extern DB2Storage sGarrFollSupportSpellStore; TC_GAME_API extern DB2Storage sGarrFollowerStore; +TC_GAME_API extern DB2Storage sGarrFollowerLevelXPStore; +TC_GAME_API extern DB2Storage sGarrFollowerQualityStore; +TC_GAME_API extern DB2Storage sGarrFollowerTypeStore; +TC_GAME_API extern DB2Storage sGarrFollowerSetXFollowerStore; +TC_GAME_API extern DB2Storage sGarrFollowerUICreatureStore; TC_GAME_API extern DB2Storage sGarrFollowerXAbilityStore; +TC_GAME_API extern DB2Storage sGarrItemLevelUpgradeDataStore; +TC_GAME_API extern DB2Storage sGarrMechanicStore; +TC_GAME_API extern DB2Storage sGarrMechanicSetXMechanicStore; +TC_GAME_API extern DB2Storage sGarrMechanicTypeStore; TC_GAME_API extern DB2Storage sGarrMissionStore; +TC_GAME_API extern DB2Storage sGarrMissionSetStore; +TC_GAME_API extern DB2Storage sGarrMissionTextureStore; +TC_GAME_API extern DB2Storage sGarrMissionTypeStore; +TC_GAME_API extern DB2Storage sGarrMissionXEncounterStore; +TC_GAME_API extern DB2Storage sGarrMissionXFollowerStore; +TC_GAME_API extern DB2Storage sGarrMssnBonusAbilityStore; TC_GAME_API extern DB2Storage sGarrPlotStore; TC_GAME_API extern DB2Storage sGarrPlotBuildingStore; TC_GAME_API extern DB2Storage sGarrPlotInstanceStore; +TC_GAME_API extern DB2Storage sGarrPlotUICategoryStore; TC_GAME_API extern DB2Storage sGarrSiteLevelStore; TC_GAME_API extern DB2Storage sGarrSiteLevelPlotInstStore; +TC_GAME_API extern DB2Storage sGarrSpecializationStore; +TC_GAME_API extern DB2Storage sGarrStringStore; TC_GAME_API extern DB2Storage sGarrTalentTreeStore; +TC_GAME_API extern DB2Storage sGarrTalentStore; +TC_GAME_API extern DB2Storage sGarrTalentRankStore; +TC_GAME_API extern DB2Storage sGarrTalentCostStore; +TC_GAME_API extern DB2Storage sGarrTalentResearchStore; +TC_GAME_API extern DB2Storage sGarrTalentSocketPropertiesStore; +TC_GAME_API extern DB2Storage sGarrTalentMapPOIStore; +TC_GAME_API extern DB2Storage sGarrTalentRankGroupEntryStore; +TC_GAME_API extern DB2Storage sGarrTalentRankGroupResearchModStore; +TC_GAME_API extern DB2Storage sGarrTalTreeXGarrTalResearchStore; +TC_GAME_API extern DB2Storage sGarrTypeStore; +TC_GAME_API extern DB2Storage sGarrUiAnimClassInfoStore; +TC_GAME_API extern DB2Storage sGarrUiAnimRaceInfoStore; TC_GAME_API extern DB2Storage sGemPropertiesStore; TC_GAME_API extern DB2Storage sGlyphPropertiesStore; +TC_GAME_API extern DB2Storage sGossipXGarrTalentTreesStore; TC_GAME_API extern DB2Storage sGossipNPCOptionStore; TC_GAME_API extern DB2Storage sGuildColorBackgroundStore; TC_GAME_API extern DB2Storage sGuildColorBorderStore; diff --git a/src/server/game/DataStores/DB2Structure.h b/src/server/game/DataStores/DB2Structure.h index 984b256df..b4a71c557 100644 --- a/src/server/game/DataStores/DB2Structure.h +++ b/src/server/game/DataStores/DB2Structure.h @@ -1976,6 +1976,80 @@ struct GarrAbilityEntry int32 Flags; }; +struct GarrAbilityCategoryEntry +{ + uint32 ID; + LocalizedString Name; +}; + +struct GarrAbilityEffectEntry +{ + uint32 ID; + uint16 GarrAbilityID; + uint8 EffectType; + uint8 TargetMask; + int32 Amount; + float CombatWeightBase; + float CombatWeightMax; + float ActionValueFlat; + uint8 AbilityAction; + uint8 MiscValueA; + int32 ActionHours; + int32 ActionRecordID; +}; + +struct GarrAutoCombatantEntry +{ + uint32 ID; + int32 Attack; + int32 Health; + int32 MaxHealth; + int32 AutoAttackSpellID; + int32 Role; + int32 BoardIndex; + int32 GarrEncounterID; + int32 GarrAutoSpellID; + int32 Flags; +}; + +struct GarrAutoSpellEntry +{ + uint32 ID; + LocalizedString Name; + LocalizedString Description; + int32 Cooldown; + int32 Duration; + int32 SchoolMask; + int32 SpellVisualID; + int32 Flags; +}; + +struct GarrAutoSpellEffectEntry +{ + uint32 ID; + int32 GarrAutoSpellID; + uint8 EffectType; + uint8 Targets; + float Amount; + uint8 MiscType; + int32 MiscValue; + int32 Period; +}; + +struct GarrEncounterSetXEncounterEntry +{ + uint32 ID; + uint32 GarrEncounterID; + int32 GarrEncounterSetID; +}; + +struct GarrFollowerSetXFollowerEntry +{ + uint32 ID; + int32 GarrFollowerID; + int32 GarrFollowerSetID; +}; + struct GarrBuildingEntry { uint32 ID; @@ -2005,6 +2079,16 @@ struct GarrBuildingEntry int32 Flags; }; +struct GarrBuildingDoodadSetEntry +{ + uint32 ID; + uint8 GarrBuildingID; + uint8 HordeDoodadSetID; + uint8 AllianceDoodadSetID; + uint8 HordeAltDoodadSetID; + uint8 AllianceAltDoodadSetID; +}; + struct GarrBuildingPlotInstEntry { DBCPosition2D MapOffset; @@ -2026,6 +2110,38 @@ struct GarrClassSpecEntry int32 Flags; }; +struct GarrClassSpecPlayerCondEntry +{ + uint32 ID; + LocalizedString ClassSpec; + uint32 OrderIndex; + uint32 GarrClassSpecID; + int32 PlayerConditionID; + int32 GarrStringID; + uint8 Flags; +}; + +struct GarrEncounterEntry +{ + uint32 ID; + LocalizedString Name; + int32 CreatureID; + int32 CreatureDisplayInfoID; + uint32 UiAnimHeight; + float UiAnimScale; + float UiTextureScale; + int32 EnvGarrMechanicTypeID; + int32 GarrEncounterSetID; +}; + +struct GarrEncounterXMechanicEntry +{ + uint32 ID; + int32 GarrMechanicID; + uint8 GarrMechanicSetID; + int32 GarrEncounterID; +}; + struct GarrFollowerEntry { uint32 ID; @@ -2064,6 +2180,57 @@ struct GarrFollowerEntry int32 CovenantID; }; +struct GarrFollItemSetMemberEntry +{ + uint32 ID; + int32 GarrFollItemSetID; + uint8 ItemSlot; + uint16 ItemModifiedAppearanceID; + int32 GarrFollowerID; +}; + +struct GarrFollSupportSpellEntry +{ + uint32 ID; + int32 HordeSpellID; + int32 AllianceSpellID; + uint8 OrderIndex; + int32 GarrFollowerID; +}; + +struct GarrFollowerLevelXPEntry +{ + uint32 ID; + int8 FollowerLevel; + uint8 GarrFollowerTypeID; + uint16 XpToNextLevel; + uint16 ShipmentXP; +}; + +struct GarrFollowerQualityEntry +{ + uint32 ID; + int32 XpThreshold; + uint32 QualityItemID; + int8 Quality; + uint8 AbilityCount; + uint8 TraitCount; + uint16 GarrFollowerTypeID; + uint32 ClassSpecID; +}; + +struct GarrFollowerTypeEntry +{ + uint32 ID; + int8 GarrTypeID; + uint8 MaxFollowers; + uint8 MaxFollowerBuildingType; + uint16 MaxItemLevel; + uint8 LevelRangeBias; + uint8 ItemLevelRangeBias; + int32 Flags; +}; + struct GarrFollowerXAbilityEntry { uint32 ID; @@ -2073,6 +2240,50 @@ struct GarrFollowerXAbilityEntry uint32 GarrFollowerID; }; +struct GarrFollowerUICreatureEntry +{ + uint32 ID; + uint8 Race; + int8 Gender; + int32 CreatureID; + float Scale; + int32 FileDataID; + int32 GarrFollowerID; +}; + +struct GarrMechanicEntry +{ + uint32 ID; + int32 GarrMechanicTypeID; + float Factor; + int32 GarrAbilityID; +}; + +struct GarrMechanicTypeEntry +{ + uint32 ID; + LocalizedString Name; + LocalizedString Description; + int32 GarrAbilityCategoryID; + uint8 Category; +}; + +struct GarrMechanicSetXMechanicEntry +{ + uint32 ID; + int32 GarrMechanicID; + int32 GarrMechanicSetID; +}; + +struct GarrItemLevelUpgradeDataEntry +{ + uint32 ID; + int32 Operation; + int32 MinItemLevel; + int32 MaxItemLevel; + int8 FollowerTypeID; +}; + struct GarrMissionEntry { uint32 ID; @@ -2109,6 +2320,61 @@ struct GarrMissionEntry int32 AutoCombatantEnvCasterID; }; +struct GarrMissionSetEntry +{ + uint32 ID; + int8 GarrTypeID; + int32 MissionCostCurrencyTypesID; + int32 ContentTuningID; + int32 MapID; + int32 Flags; + int32 Priority; +}; + +struct GarrMissionTypeEntry +{ + uint32 ID; + LocalizedString Name; + uint16 UiTextureAtlasMemberID; + uint16 UiTextureKitID; +}; + +struct GarrMissionTextureEntry +{ + uint32 ID; + std::array Pos; + uint16 UiTextureAtlasMemberID; +}; + +struct GarrMissionXEncounterEntry +{ + uint32 ID; + uint32 GarrEncounterID; + uint32 GarrMissionSetEncounterID; + uint8 CombatWeightBase; + int8 CombatWeightMax; + int32 GarrMissionID; +}; + +struct GarrMissionXFollowerEntry +{ + uint32 ID; + int32 GarrFollowerID; + int32 GarrFollowerTypeID; + int8 BoardIndex; + int32 GarrMissionID; +}; + +struct GarrMssnBonusAbilityEntry +{ + uint32 ID; + uint8 GarrMssnBonusAbilityType; + uint8 MssnSort; + uint16 GarrAbilityID; + float Modifier; + int32 MissionSetID; +}; + struct GarrPlotEntry { uint32 ID; @@ -2134,6 +2400,13 @@ struct GarrPlotInstanceEntry uint8 GarrPlotID; }; +struct GarrPlotUICategoryEntry +{ + uint32 ID; + LocalizedString Name; + uint8 PlotType; +}; + struct GarrSiteLevelEntry { uint32 ID; @@ -2157,6 +2430,24 @@ struct GarrSiteLevelPlotInstEntry uint8 UiMarkerSize; }; +struct GarrSpecializationEntry +{ + uint32 ID; + LocalizedString Name; + LocalizedString Description; + uint8 GarrTypeID; + int32 IconFileDataID; + uint8 GarrFollowerTypeID; + int32 Param; + std::array Bonus; +}; + +struct GarrStringEntry +{ + uint32 ID; + LocalizedString Text; +}; + struct GarrTalentTreeEntry { uint32 ID; @@ -2174,6 +2465,153 @@ struct GarrTalentTreeEntry int32 CurrencyID; }; +struct GarrTalentEntry +{ + LocalizedString Name; + LocalizedString Description; + uint32 ID; + uint32 GarrTalentTreeID; + int8 Tier; + int8 UiOrder; + int32 IconFileDataID; + uint32 PlayerConditionID; + uint32 GarrAbilityID; + int32 Flags; + int32 TalentType; + int32 PrerequisiteTalentID; + int32 ResearchCostSource; + int32 ActiveDurationSecs; + int32 GarrTalentSocketPropertiesID; +}; + +struct GarrTalentRankEntry +{ + uint32 ID; + int32 Rank; + int32 PerkSpellID; + int32 PerkPlayerConditionID; + float Points; + int32 ResearchCost; + int32 ResearchCostCurrencyTypesID; + int32 ResearchGoldCost; + int32 ResearchDurationSecs; + int32 RespecCost; + int32 RespecCostCurrencyTypesID; + int32 RespecGoldCost; + int32 RespecDurationSecs; + int32 AlternateResearchCost; + int32 AlternateResearchCostCurrencyTypesID; + int32 AlternateResearchGoldCost; + int32 AlternateResearchDurationSecs; + uint32 GarrTalentID; +}; + +struct GarrTalentCostEntry +{ + uint64 MoneyQuantity; + uint32 ID; + uint32 GarrTalentTreeID; + int32 GarrTalentID; + int32 RankIndex; + int32 GarrTalentRankID; + int32 CostType; + int32 CurrencyTypesID; + int32 CurrencyQuantity; +}; + +struct GarrTalentResearchEntry +{ + uint32 ID; + int32 GoldCost; + int32 CurrencyTypesID; + int32 CurrencyTypesCost; + int32 DurationSecs; + int32 RespecGoldCost; + int32 RespecCurrencyTypesID; + int32 RespecCurrencyTypesCost; + int32 RespecDurationSecs; +}; + +struct GarrTalentSocketPropertiesEntry +{ + uint32 ID; + int32 GarrTalentSocketType; + int32 GarrTalentSocketSubtype; +}; + +struct GarrTalentMapPOIEntry +{ + uint32 ID; + std::array Position; + int32 GarrTalentID; +}; + +struct GarrTalentRankGroupEntryEntry +{ + uint32 ID; + int32 GarrTalentRankGroupID; + uint32 GarrTalentRankID; +}; + +struct GarrTalentRankGroupResearchModEntry +{ + uint32 ID; + int32 ModifierType; + int32 PlayerConditionID; + float ModifierValue; + uint32 GarrTalentRankGroupID; +}; + +struct GarrTalTreeXGarrTalResearchEntry +{ + uint32 ID; + int32 GarrTalentTreeID; + int32 GarrTalentResearchID; + int32 OrderIndex; +}; + +struct GarrTypeEntry +{ + int8 ID; + uint32 PrimaryCurrencyTypeID; + uint32 SecondaryCurrencyTypeID; + uint32 ExpansionID; + int32 Flags; + float AutoFollowerHealRate; + int32 MissionCostCurveID; + float AutoFollowerHealCostMult; + std::array MapIDs; +}; + +struct GarrUiAnimClassInfoEntry +{ + uint32 ID; + int32 ClassID; + uint8 IsFemale; + float WalkSpeed; + uint32 RunAnimID; + uint32 WalkAnimID; + uint32 IdleAnimID; +}; + +struct GarrUiAnimRaceInfoEntry +{ + uint32 ID; + uint8 RaceID; + float RunSpeedModifier; + float RunAnimSpeedModifier; + float WalkAnimSpeedModifier; + float IdleAnimSpeedModifier; + float PortraitScale; + float PortraitHeight; + float PortraitFieldOfView; + float PortraitCameraPositionX; + float PortraitCameraPositionY; + float PortraitCameraPositionZ; + float PortraitCameraTargetX; + float PortraitCameraTargetY; +}; + struct GemPropertiesEntry { uint32 ID; @@ -2211,6 +2649,13 @@ struct GlyphRequiredSpecEntry uint32 GlyphPropertiesID; }; +struct GossipXGarrTalentTreesEntry +{ + uint32 ID; + int32 GarrTalentTreeID; + int32 GossipID; +}; + struct GossipNPCOptionEntry { uint32 ID; @@ -5681,6 +6126,40 @@ struct WarbandSceneSourceInfoEntry int8 SourceType; }; +struct CharShipmentEntry +{ + uint32 ID; + uint16 ContainerID; + int32 TreasureID; + uint32 DummyItemID; + int32 Duration; + int32 SpellID; + uint32 OnCompleteSpellID; + uint8 MaxShipments; + uint16 GarrFollowerID; + int32 Flags; +}; + +struct CharShipmentContainerEntry +{ + uint32 ID; + LocalizedString Description; + LocalizedString PendingText; + uint16 UiTextureKitID; + int8 GarrTypeID; + uint8 GarrBuildingType; + uint8 BaseCapacity; + uint16 SmallDisplayInfoID; + uint16 MediumDisplayInfoID; + uint16 LargeDisplayInfoID; + int32 WorkingSpellVisualID; + uint32 CompleteSpellVisualID; + uint32 WorkingDisplayInfoID; + uint8 MediumThreshold; + uint8 LargeThreshold; + int8 Faction; + uint16 CrossFactionID; +}; //WowCommunity #pragma pack(pop) #endif diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index c1c68b0a0..cb8caede6 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -1072,6 +1072,10 @@ void Player::Update(uint32 p_time) m_nextSave -= p_time; } + // Update garrison timers (building completion, shipments, talent research, mission expiry) + for (auto& [type, garrison] : _garrisons) + garrison->Update(p_time); + //Handle Water/drowning HandleDrowning(p_time); @@ -19085,8 +19089,14 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_GARRISON_BLUEPRINTS), holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_GARRISON_BUILDINGS), holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_GARRISON_FOLLOWERS), - holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_GARRISON_FOLLOWER_ABILITIES))) - _garrison = std::move(garrison); + holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_GARRISON_FOLLOWER_ABILITIES), + holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_GARRISON_MISSIONS), + holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_GARRISON_SPECIALIZATIONS), + holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_GARRISON_SHIPMENTS), + holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_GARRISON_TALENTS), + holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_GARRISON_TROPHIES), + holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_GARRISON_ARCHIVED_MISSIONS))) + _garrisons[garrison->GetType()] = std::move(garrison); std::unique_ptr housing = std::make_unique(this); if (housing->LoadFromDB(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_HOUSING), @@ -21448,8 +21458,8 @@ void Player::SaveToDB(LoginDatabaseTransaction loginTransaction, CharacterDataba _SaveCUFProfiles(trans); _SavePlayerData(trans); _SaveCharacterBankTabSettings(trans); - if (_garrison) - _garrison->SaveToDB(trans); + for (auto const& [type, garrison] : _garrisons) + garrison->SaveToDB(trans); for (auto const& h : _housings) if (h) h->SaveToDB(trans); @@ -25869,8 +25879,8 @@ void Player::SendInitialPacketsAfterAddToMap() PhasingHandler::OnMapChange(this); - if (_garrison) - _garrison->SendRemoteInfo(); + for (auto const& [type, garrison] : _garrisons) + garrison->SendRemoteInfo(); // Housing state setup at neighborhood map entry. // @@ -26428,8 +26438,8 @@ void Player::DailyReset() m_DailyQuestChanged = false; m_lastDailyQuestTime = 0; - if (_garrison) - _garrison->ResetFollowerActivationLimit(); + for (auto const& [type, garrison] : _garrisons) + garrison->ResetFollowerActivationLimit(); FailCriteria(CriteriaFailEvent::DailyQuestsCleared, 0); } @@ -30954,18 +30964,28 @@ void Player::CreateGarrison(uint32 garrSiteId) { std::unique_ptr garrison(new Garrison(this)); if (garrison->Create(garrSiteId)) - _garrison = std::move(garrison); + _garrisons[garrison->GetType()] = std::move(garrison); } -void Player::DeleteGarrison() +void Player::DeleteGarrison(GarrisonType type) { - if (_garrison) + auto itr = _garrisons.find(type); + if (itr != _garrisons.end()) { - _garrison->Delete(); - _garrison.reset(); + itr->second->Delete(); + _garrisons.erase(itr); } } +Garrison* Player::GetGarrison(GarrisonType type) const +{ + auto itr = _garrisons.find(type); + if (itr != _garrisons.end()) + return itr->second.get(); + + return nullptr; +} + void Player::CreateHousing(ObjectGuid neighborhoodGuid, uint8 plotIndex) { std::unique_ptr housing(new Housing(this)); diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index aaf089b6d..9649d4bc7 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -79,6 +79,7 @@ class CinematicMgr; class Creature; class DynamicObject; class Garrison; +enum GarrisonType : int32; class Housing; class Group; class Guild; @@ -1020,6 +1021,12 @@ enum PlayerLoginQueryIndex PLAYER_LOGIN_QUERY_LOAD_GARRISON_BUILDINGS, PLAYER_LOGIN_QUERY_LOAD_GARRISON_FOLLOWERS, PLAYER_LOGIN_QUERY_LOAD_GARRISON_FOLLOWER_ABILITIES, + PLAYER_LOGIN_QUERY_LOAD_GARRISON_MISSIONS, + PLAYER_LOGIN_QUERY_LOAD_GARRISON_SPECIALIZATIONS, + PLAYER_LOGIN_QUERY_LOAD_GARRISON_SHIPMENTS, + PLAYER_LOGIN_QUERY_LOAD_GARRISON_TALENTS, + PLAYER_LOGIN_QUERY_LOAD_GARRISON_TROPHIES, + PLAYER_LOGIN_QUERY_LOAD_GARRISON_ARCHIVED_MISSIONS, PLAYER_LOGIN_QUERY_LOAD_TRAIT_ENTRIES, PLAYER_LOGIN_QUERY_LOAD_TRAIT_CONFIGS, PLAYER_LOGIN_QUERY_LOAD_DATA_ELEMENTS, @@ -2918,8 +2925,10 @@ class TC_GAME_API Player final : public Unit, public GridObject void UnlockReagentBank() { SetPlayerFlagEx(PLAYER_FLAGS_EX_REAGENT_BANK_UNLOCKED); } void CreateGarrison(uint32 garrSiteId); - void DeleteGarrison(); - Garrison* GetGarrison() const { return _garrison.get(); } + void DeleteGarrison(GarrisonType type = GarrisonType(2) /*GARRISON_TYPE_GARRISON*/); + Garrison* GetGarrison() const { return GetGarrison(GarrisonType(2) /*GARRISON_TYPE_GARRISON*/); } + Garrison* GetGarrison(GarrisonType type) const; + std::unordered_map> const& GetGarrisons() const { return _garrisons; } void CreateHousing(ObjectGuid neighborhoodGuid, uint8 plotIndex); void DeleteHousing(ObjectGuid neighborhoodGuid); @@ -3486,7 +3495,7 @@ class TC_GAME_API Player final : public Unit, public GridObject uint32 _activeCheats; - std::unique_ptr _garrison; + std::unordered_map> _garrisons; std::vector> _housings; bool _advancedCombatLoggingEnabled; diff --git a/src/server/game/Garrison/Garrison.cpp b/src/server/game/Garrison/Garrison.cpp index 6a9561646..524f14c94 100644 --- a/src/server/game/Garrison/Garrison.cpp +++ b/src/server/game/Garrison/Garrison.cpp @@ -16,27 +16,35 @@ */ #include "Garrison.h" +#include "Containers.h" #include "Creature.h" #include "DatabaseEnv.h" #include "DB2Stores.h" +#include "DB2Structure.h" #include "GameObject.h" #include "GameTime.h" +#include "GarrisonAutoCombat.h" #include "GarrisonMgr.h" +#include "Item.h" #include "Log.h" +#include "Mail.h" #include "Map.h" #include "MapManager.h" #include "ObjectMgr.h" #include "PhasingHandler.h" #include "Player.h" +#include "Random.h" #include "VehicleDefines.h" #include "advstd.h" -Garrison::Garrison(Player* owner) : _owner(owner), _siteLevel(nullptr), _followerActivationsRemainingToday(1) +Garrison::Garrison(Player* owner) : _owner(owner), _garrType(GARRISON_TYPE_GARRISON), _siteLevel(nullptr), _followerActivationsRemainingToday(1) { } bool Garrison::LoadFromDB(PreparedQueryResult garrison, PreparedQueryResult blueprints, PreparedQueryResult buildings, - PreparedQueryResult followers, PreparedQueryResult abilities) + PreparedQueryResult followers, PreparedQueryResult abilities, PreparedQueryResult missions, + PreparedQueryResult specializations, PreparedQueryResult shipments, PreparedQueryResult talents, + PreparedQueryResult trophies, PreparedQueryResult archivedMissions) { if (!garrison) return false; @@ -44,9 +52,20 @@ bool Garrison::LoadFromDB(PreparedQueryResult garrison, PreparedQueryResult blue Field* fields = garrison->Fetch(); _siteLevel = sGarrSiteLevelStore.LookupEntry(fields[0].GetUInt32()); _followerActivationsRemainingToday = fields[1].GetUInt32(); + _garrType = static_cast(fields[2].GetUInt32()); + _missionsStartedToday = fields[3].GetUInt32(); + _lastMissionStartDay = fields[4].GetUInt32(); if (!_siteLevel) return false; + // Reset daily mission counter if the day has changed + uint32 today = static_cast(GameTime::GetGameTime() / DAY); + if (today != _lastMissionStartDay) + { + _missionsStartedToday = 0; + _lastMissionStartDay = today; + } + InitializePlots(); if (blueprints) @@ -69,6 +88,8 @@ bool Garrison::LoadFromDB(PreparedQueryResult garrison, PreparedQueryResult blue uint32 buildingId = fields[1].GetUInt32(); time_t timeBuilt = fields[2].GetInt64(); bool active = fields[3].GetBool(); + uint32 currentGarSpecId = fields[4].GetUInt32(); + time_t timeSpecCooldown = fields[5].GetInt64(); Plot* plot = GetPlot(plotInstanceId); if (!plot) @@ -82,12 +103,86 @@ bool Garrison::LoadFromDB(PreparedQueryResult garrison, PreparedQueryResult blue plot->BuildingInfo.PacketInfo->GarrBuildingID = buildingId; plot->BuildingInfo.PacketInfo->TimeBuilt = timeBuilt; plot->BuildingInfo.PacketInfo->Active = active; + plot->BuildingInfo.PacketInfo->CurrentGarSpecID = currentGarSpecId; + plot->BuildingInfo.PacketInfo->TimeSpecCooldown = timeSpecCooldown; } while (buildings->NextRow()); } - // 0 1 2 3 4 5 6 7 8 9 - // SELECT dbId, followerId, quality, level, itemLevelWeapon, itemLevelArmor, xp, currentBuilding, currentMission, status FROM character_garrison_followers WHERE guid = ? + if (specializations) + { + do + { + fields = specializations->Fetch(); + if (sGarrSpecializationStore.LookupEntry(fields[0].GetUInt32())) + _knownSpecializations.insert(fields[0].GetUInt32()); + + } while (specializations->NextRow()); + } + + // 0 1 2 3 4 5 + // SELECT dbId, shipmentId, plotInstanceId, creationTime, duration, assignedFollowerDbId FROM character_garrison_shipments WHERE guid = ? + if (shipments) + { + do + { + fields = shipments->Fetch(); + Shipment& shipment = _shipments[fields[0].GetUInt64()]; + shipment.DbID = fields[0].GetUInt64(); + shipment.ShipmentRecID = fields[1].GetUInt32(); + shipment.PlotInstanceID = fields[2].GetUInt32(); + shipment.CreationTime = time_t(fields[3].GetInt64()); + shipment.Duration = fields[4].GetInt32(); + shipment.AssignedFollowerDBID = fields[5].GetUInt64(); + + } while (shipments->NextRow()); + } + + // 0 1 2 3 4 5 + // SELECT garrTalentId, `rank`, researchStartTime, flags, soulbindConduitId, soulbindConduitRank FROM character_garrison_talents WHERE guid = ? + if (talents) + { + do + { + fields = talents->Fetch(); + uint32 garrTalentID = fields[0].GetUInt32(); + Talent& talent = _talents[garrTalentID]; + talent.GarrTalentID = garrTalentID; + talent.Rank = fields[1].GetInt32(); + talent.ResearchStartTime = time_t(fields[2].GetInt64()); + talent.Flags = fields[3].GetInt32(); + talent.SoulbindConduitID = fields[4].GetInt32(); + talent.SoulbindConduitRank = fields[5].GetInt32(); + + } while (talents->NextRow()); + } + + // 0 + // SELECT trophyId FROM character_garrison_trophies WHERE guid = ? + if (trophies) + { + do + { + fields = trophies->Fetch(); + _trophies.insert(fields[0].GetUInt32()); + } while (trophies->NextRow()); + } + + // 0 1 + // SELECT garrType, missionRecID FROM character_garrison_archived_missions WHERE guid = ? + if (archivedMissions) + { + do + { + fields = archivedMissions->Fetch(); + uint32 garrType = fields[0].GetUInt32(); + if (garrType == static_cast(_garrType)) + _archivedMissions.push_back(fields[1].GetInt32()); + } while (archivedMissions->NextRow()); + } + + // 0 1 2 3 4 5 6 7 8 9 10 11 + // SELECT dbId, followerId, quality, level, itemLevelWeapon, itemLevelArmor, xp, currentBuilding, currentMission, status, durability, customName FROM character_garrison_followers WHERE guid = ? if (followers) { do @@ -111,6 +206,9 @@ bool Garrison::LoadFromDB(PreparedQueryResult garrison, PreparedQueryResult blue follower.PacketInfo.CurrentBuildingID = fields[7].GetUInt32(); follower.PacketInfo.CurrentMissionID = fields[8].GetUInt32(); follower.PacketInfo.FollowerStatus = fields[9].GetUInt32(); + follower.PacketInfo.Durability = fields[10].GetUInt32(); + follower.PacketInfo.CustomName = fields[11].GetString(); + follower.PacketInfo.ZoneSupportSpellID = sGarrisonMgr.GetFollowerZoneSupportSpell(followerId, GetFaction()); if (!sGarrBuildingStore.LookupEntry(follower.PacketInfo.CurrentBuildingID)) follower.PacketInfo.CurrentBuildingID = 0; @@ -139,6 +237,44 @@ bool Garrison::LoadFromDB(PreparedQueryResult garrison, PreparedQueryResult blue } } + // 0 1 2 3 4 5 6 7 8 9 + // SELECT dbId, guid, missionRecID, offerTime, offerDuration, startTime, travelDuration, missionDuration, missionState, successChance FROM character_garrison_missions WHERE guid = ? + if (missions) + { + do + { + fields = missions->Fetch(); + + uint64 dbId = fields[0].GetUInt64(); + uint32 missionRecID = fields[2].GetUInt32(); + + if (!sGarrMissionStore.LookupEntry(missionRecID)) + continue; + + if (_missionDbIdGenerator <= dbId) + _missionDbIdGenerator = dbId + 1; + + Mission& mission = _missions[dbId]; + mission.PacketInfo.DbID = dbId; + mission.PacketInfo.MissionRecID = missionRecID; + mission.PacketInfo.OfferTime = fields[3].GetInt64(); + mission.PacketInfo.OfferDuration = Seconds(fields[4].GetInt32()); + mission.PacketInfo.StartTime = fields[5].GetInt64(); + mission.PacketInfo.TravelDuration = Seconds(fields[6].GetInt32()); + mission.PacketInfo.MissionDuration = Seconds(fields[7].GetInt32()); + mission.PacketInfo.MissionState = fields[8].GetInt32(); + mission.PacketInfo.SuccessChance = fields[9].GetInt32(); + + GarrMissionEntry const* missionEntry = sGarrMissionStore.LookupEntry(missionRecID); + if (missionEntry) + mission.PacketInfo.MissionScalar = missionEntry->AutoMissionScalar; + + } while (missions->NextRow()); + } + + // Complete any talent research that finished while offline + CompleteAllTalentResearch(); + return true; } @@ -150,6 +286,9 @@ void Garrison::SaveToDB(CharacterDatabaseTransaction trans) stmt->setUInt64(0, _owner->GetGUID().GetCounter()); stmt->setUInt32(1, _siteLevel->ID); stmt->setUInt32(2, _followerActivationsRemainingToday); + stmt->setUInt32(3, static_cast(_garrType)); + stmt->setUInt32(4, _missionsStartedToday); + stmt->setUInt32(5, _lastMissionStartDay); trans->Append(stmt); for (uint32 building : _knownBuildings) @@ -171,10 +310,24 @@ void Garrison::SaveToDB(CharacterDatabaseTransaction trans) stmt->setUInt32(2, plot.BuildingInfo.PacketInfo->GarrBuildingID); stmt->setInt64(3, plot.BuildingInfo.PacketInfo->TimeBuilt); stmt->setBool(4, plot.BuildingInfo.PacketInfo->Active); + stmt->setUInt32(5, plot.BuildingInfo.PacketInfo->CurrentGarSpecID); + stmt->setInt64(6, plot.BuildingInfo.PacketInfo->TimeSpecCooldown); trans->Append(stmt); } } + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_GARRISON_SPECIALIZATIONS); + stmt->setUInt64(0, _owner->GetGUID().GetCounter()); + trans->Append(stmt); + + for (uint32 specId : _knownSpecializations) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_GARRISON_SPECIALIZATIONS); + stmt->setUInt64(0, _owner->GetGUID().GetCounter()); + stmt->setUInt32(1, specId); + trans->Append(stmt); + } + for (auto const& p : _followers) { Follower const& follower = p.second; @@ -191,6 +344,8 @@ void Garrison::SaveToDB(CharacterDatabaseTransaction trans) stmt->setUInt32(index++, follower.PacketInfo.CurrentBuildingID); stmt->setUInt32(index++, follower.PacketInfo.CurrentMissionID); stmt->setUInt32(index++, follower.PacketInfo.FollowerStatus); + stmt->setUInt32(index++, follower.PacketInfo.Durability); + stmt->setString(index++, follower.PacketInfo.CustomName); trans->Append(stmt); uint8 slot = 0; @@ -203,6 +358,78 @@ void Garrison::SaveToDB(CharacterDatabaseTransaction trans) trans->Append(stmt); } } + + for (auto const& p : _missions) + { + Mission const& mission = p.second; + uint8 index = 0; + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_GARRISON_MISSIONS); + stmt->setUInt64(index++, mission.PacketInfo.DbID); + stmt->setUInt64(index++, _owner->GetGUID().GetCounter()); + stmt->setUInt32(index++, mission.PacketInfo.MissionRecID); + stmt->setInt64(index++, mission.PacketInfo.OfferTime); + stmt->setInt32(index++, static_cast(Seconds(mission.PacketInfo.OfferDuration).count())); + stmt->setInt64(index++, mission.PacketInfo.StartTime); + stmt->setInt32(index++, static_cast(Seconds(mission.PacketInfo.TravelDuration).count())); + stmt->setInt32(index++, static_cast(Seconds(mission.PacketInfo.MissionDuration).count())); + stmt->setInt32(index++, mission.PacketInfo.MissionState); + stmt->setInt32(index++, mission.PacketInfo.SuccessChance); + trans->Append(stmt); + } + + for (auto const& p : _shipments) + { + Shipment const& shipment = p.second; + uint8 index = 0; + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_GARRISON_SHIPMENTS); + stmt->setUInt64(index++, shipment.DbID); + stmt->setUInt64(index++, _owner->GetGUID().GetCounter()); + stmt->setUInt32(index++, shipment.ShipmentRecID); + stmt->setUInt32(index++, shipment.PlotInstanceID); + stmt->setInt64(index++, shipment.CreationTime); + stmt->setInt32(index++, shipment.Duration); + stmt->setUInt64(index++, shipment.AssignedFollowerDBID); + trans->Append(stmt); + } + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_GARRISON_TALENTS); + stmt->setUInt64(0, _owner->GetGUID().GetCounter()); + trans->Append(stmt); + + for (auto const& [talentId, talent] : _talents) + { + uint8 index = 0; + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_GARRISON_TALENT); + stmt->setUInt64(index++, _owner->GetGUID().GetCounter()); + stmt->setUInt32(index++, talent.GarrTalentID); + stmt->setInt32(index++, talent.Rank); + stmt->setInt64(index++, talent.ResearchStartTime); + stmt->setInt32(index++, talent.Flags); + stmt->setInt32(index++, talent.SoulbindConduitID); + stmt->setInt32(index++, talent.SoulbindConduitRank); + trans->Append(stmt); + } + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_GARRISON_TROPHIES); + stmt->setUInt64(0, _owner->GetGUID().GetCounter()); + trans->Append(stmt); + + for (uint32 trophyId : _trophies) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_GARRISON_TROPHY); + stmt->setUInt64(0, _owner->GetGUID().GetCounter()); + stmt->setUInt32(1, trophyId); + trans->Append(stmt); + } + + for (int32 missionRecID : _archivedMissions) + { + stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHARACTER_GARRISON_ARCHIVED_MISSION); + stmt->setUInt64(0, _owner->GetGUID().GetCounter()); + stmt->setUInt32(1, static_cast(_garrType)); + stmt->setInt32(2, missionRecID); + trans->Append(stmt); + } } void Garrison::DeleteFromDB(ObjectGuid::LowType ownerGuid, CharacterDatabaseTransaction trans) @@ -222,6 +449,42 @@ void Garrison::DeleteFromDB(ObjectGuid::LowType ownerGuid, CharacterDatabaseTran stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_GARRISON_FOLLOWERS); stmt->setUInt64(0, ownerGuid); trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_GARRISON_MISSIONS); + stmt->setUInt64(0, ownerGuid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_GARRISON_SPECIALIZATIONS); + stmt->setUInt64(0, ownerGuid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_GARRISON_SHIPMENTS); + stmt->setUInt64(0, ownerGuid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_GARRISON_TALENTS); + stmt->setUInt64(0, ownerGuid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_GARRISON_TROPHIES); + stmt->setUInt64(0, ownerGuid); + trans->Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHARACTER_GARRISON_ARCHIVED_MISSIONS); + stmt->setUInt64(0, ownerGuid); + trans->Append(stmt); +} + +static GarrisonType GetGarrisonTypeFromSiteId(uint32 garrSiteId) +{ + switch (garrSiteId) + { + case 2: return GARRISON_TYPE_GARRISON; // WoD + case 71: return GARRISON_TYPE_CLASS_ORDER; // Legion + case 173: return GARRISON_TYPE_WAR_CAMPAIGN; // BfA + case 500: return GARRISON_TYPE_COVENANT; // Shadowlands + default: return GARRISON_TYPE_GARRISON; + } } bool Garrison::Create(uint32 garrSiteId) @@ -231,6 +494,7 @@ bool Garrison::Create(uint32 garrSiteId) return false; _siteLevel = siteLevel; + _garrType = GetGarrisonTypeFromSiteId(garrSiteId); InitializePlots(); @@ -283,6 +547,82 @@ void Garrison::InitializePlots() void Garrison::Upgrade() { + GarrSiteLevelEntry const* nextLevel = sGarrisonMgr.GetGarrSiteLevelEntry(_siteLevel->GarrSiteID, _siteLevel->GarrLevel + 1); + if (!nextLevel) + return; + + // Play upgrade cinematic before changing level + if (nextLevel->UpgradeMovieID) + _owner->SendMovieStart(nextLevel->UpgradeMovieID); + + // Save existing buildings keyed by plot instance ID + std::unordered_map existingBuildings; + for (auto const& p : _plots) + if (p.second.BuildingInfo.PacketInfo) + existingBuildings[p.first] = *p.second.BuildingInfo.PacketInfo; + + // Remove existing plot game objects from the map + Map* map = FindMap(); + if (map) + for (auto& p : _plots) + p.second.DeleteGameObject(map); + + // Advance to next site level + _siteLevel = nextLevel; + + // Re-initialize plots for the new level (this adds new plot slots) + _plots.clear(); + InitializePlots(); + + // Restore buildings that still exist in the new layout + for (auto const& [plotInstanceId, buildingInfo] : existingBuildings) + { + Plot* plot = GetPlot(plotInstanceId); + if (!plot) + continue; + + plot->BuildingInfo.PacketInfo = buildingInfo; + } + + // Spawn game objects for all plots on the map + if (map) + { + for (auto& p : _plots) + if (GameObject* go = p.second.CreateGameObject(map, GetFaction())) + map->AddToMap(go); + } + + // Update phasing for the new garrison level + PhasingHandler::OnConditionChange(_owner); + SendRemoteInfo(); +} + +void Garrison::Update(uint32 diff) +{ + _updateTimer += diff; + if (_updateTimer < GARRISON_UPDATE_INTERVAL) + return; + + _updateTimer -= GARRISON_UPDATE_INTERVAL; + + // Complete building constructions that have finished + for (auto& [plotInstanceId, plot] : _plots) + { + if (plot.BuildingInfo.PacketInfo && !plot.BuildingInfo.PacketInfo->Active) + { + if (plot.BuildingInfo.CanActivate()) + ActivateBuilding(plotInstanceId); + } + } + + // Complete shipments that have finished their timers + CompleteReadyShipments(); + + // Complete talent research that has finished + CompleteAllTalentResearch(); + + // Remove expired unclaimed missions + RemoveExpiredMissions(); } void Garrison::Enter() const @@ -388,7 +728,18 @@ void Garrison::PlaceBuilding(uint32 garrPlotInstanceId, uint32 garrBuildingId) { oldBuildingId = plot->BuildingInfo.PacketInfo->GarrBuildingID; if (sGarrBuildingStore.AssertEntry(oldBuildingId)->BuildingType != building->BuildingType) + { + // Send BuildingRemoved BEFORE ClearBuildingInfo (which sends PlotRemoved) + // so the client processes removal in the correct order + WorldPackets::Garrison::GarrisonBuildingRemoved buildingRemoved; + buildingRemoved.GarrTypeID = GetType(); + buildingRemoved.Result = GARRISON_SUCCESS; + buildingRemoved.GarrPlotInstanceID = garrPlotInstanceId; + buildingRemoved.GarrBuildingID = oldBuildingId; + _owner->SendDirectMessage(buildingRemoved.Write()); + plot->ClearBuildingInfo(GetType(), _owner); + } } plot->SetBuildingInfo(placeBuildingResult.BuildingInfo, _owner); @@ -401,12 +752,17 @@ void Garrison::PlaceBuilding(uint32 garrPlotInstanceId, uint32 garrBuildingId) if (oldBuildingId) { - WorldPackets::Garrison::GarrisonBuildingRemoved buildingRemoved; - buildingRemoved.GarrTypeID = GetType(); - buildingRemoved.Result = GARRISON_SUCCESS; - buildingRemoved.GarrPlotInstanceID = garrPlotInstanceId; - buildingRemoved.GarrBuildingID = oldBuildingId; - _owner->SendDirectMessage(buildingRemoved.Write()); + GarrBuildingEntry const* oldBuilding = sGarrBuildingStore.AssertEntry(oldBuildingId); + // Same-type upgrade: BuildingRemoved wasn't sent above, send it now + if (oldBuilding->BuildingType == building->BuildingType) + { + WorldPackets::Garrison::GarrisonBuildingRemoved buildingRemoved; + buildingRemoved.GarrTypeID = GetType(); + buildingRemoved.Result = GARRISON_SUCCESS; + buildingRemoved.GarrPlotInstanceID = garrPlotInstanceId; + buildingRemoved.GarrBuildingID = oldBuildingId; + _owner->SendDirectMessage(buildingRemoved.Write()); + } } _owner->UpdateCriteria(CriteriaType::PlaceGarrisonBuilding, garrBuildingId); @@ -488,12 +844,163 @@ void Garrison::ActivateBuilding(uint32 garrPlotInstanceId) } } +void Garrison::SwapBuildings(uint32 plotId1, uint32 plotId2) +{ + WorldPackets::Garrison::GarrisonSwapBuildingsResponse swapResult; + swapResult.Result = GARRISON_SUCCESS; + swapResult.PlotInstanceID1 = plotId1; + swapResult.PlotInstanceID2 = plotId2; + + Plot* plot1 = GetPlot(plotId1); + Plot* plot2 = GetPlot(plotId2); + + if (!plot1 || !plot2) + { + swapResult.Result = GARRISON_ERROR_INVALID_PLOT_INSTANCEID; + _owner->SendDirectMessage(swapResult.Write()); + return; + } + + if (!plot1->BuildingInfo.PacketInfo || !plot2->BuildingInfo.PacketInfo) + { + swapResult.Result = GARRISON_ERROR_NO_BUILDING; + _owner->SendDirectMessage(swapResult.Write()); + return; + } + + // Both plots must have the same PlotType to swap + if (plot1->PacketInfo.PlotType != plot2->PacketInfo.PlotType) + { + swapResult.Result = GARRISON_ERROR_INVALID_PLOT_BUILDING; + _owner->SendDirectMessage(swapResult.Write()); + return; + } + + Map* map = FindMap(); + + // Remove game objects from both plots + if (map) + { + plot1->DeleteGameObject(map); + plot2->DeleteGameObject(map); + } + + // Swap building info between the two plots + std::swap(plot1->BuildingInfo.PacketInfo, plot2->BuildingInfo.PacketInfo); + + // Update the GarrPlotInstanceID in each building's packet info to match the new plot + plot1->BuildingInfo.PacketInfo->GarrPlotInstanceID = plotId1; + plot2->BuildingInfo.PacketInfo->GarrPlotInstanceID = plotId2; + + // Recreate game objects + if (map) + { + if (GameObject* go1 = plot1->CreateGameObject(map, GetFaction())) + map->AddToMap(go1); + if (GameObject* go2 = plot2->CreateGameObject(map, GetFaction())) + map->AddToMap(go2); + } + + _owner->SendDirectMessage(swapResult.Write()); +} + +GarrBuildingEntry const* Garrison::GetActiveBuildingByType(uint32 buildingType) const +{ + for (auto const& [plotId, plot] : _plots) + { + if (!plot.BuildingInfo.PacketInfo) + continue; + if (!plot.BuildingInfo.PacketInfo->Active) + continue; + + GarrBuildingEntry const* building = sGarrBuildingStore.LookupEntry(plot.BuildingInfo.PacketInfo->GarrBuildingID); + if (building && building->BuildingType == buildingType) + return building; + } + return nullptr; +} + +uint32 Garrison::GetBonusFollowerSlots() const +{ + static constexpr uint32 BUILDING_TYPE_BARRACKS = 26; + + GarrBuildingEntry const* barracks = GetActiveBuildingByType(BUILDING_TYPE_BARRACKS); + if (!barracks) + return 0; + + // Barracks level 2: +5 follower slots, level 3: +5 more (total +10) + if (barracks->UpgradeLevel >= 3) + return 10; + if (barracks->UpgradeLevel >= 2) + return 5; + + return 0; +} + +void Garrison::LearnSpecialization(uint32 garrSpecId) +{ + GarrSpecializationEntry const* specEntry = sGarrSpecializationStore.LookupEntry(garrSpecId); + if (!specEntry) + return; + + if (specEntry->GarrTypeID != static_cast(GetType())) + return; + + if (_knownSpecializations.count(garrSpecId)) + return; + + _knownSpecializations.insert(garrSpecId); + SendBlueprintAndSpecializationData(); +} + +void Garrison::SetBuildingSpecialization(uint32 garrPlotInstanceId, uint32 garrSpecId) +{ + Plot* plot = GetPlot(garrPlotInstanceId); + if (!plot || !plot->BuildingInfo.PacketInfo || !plot->BuildingInfo.PacketInfo->Active) + return; + + if (garrSpecId != 0) + { + GarrSpecializationEntry const* specEntry = sGarrSpecializationStore.LookupEntry(garrSpecId); + if (!specEntry) + return; + + if (!HasSpecialization(garrSpecId)) + return; + + // Check cooldown + if (plot->BuildingInfo.PacketInfo->CurrentGarSpecID != 0 && + plot->BuildingInfo.PacketInfo->TimeSpecCooldown > GameTime::GetGameTime()) + return; + } + + plot->BuildingInfo.PacketInfo->CurrentGarSpecID = garrSpecId; + + // Set cooldown for changing specialization (1 day) + if (garrSpecId != 0) + plot->BuildingInfo.PacketInfo->TimeSpecCooldown = GameTime::GetGameTime() + DAY; +} + void Garrison::AddFollower(uint32 garrFollowerId) { WorldPackets::Garrison::GarrisonAddFollowerResult addFollowerResult; addFollowerResult.GarrTypeID = GetType(); GarrFollowerEntry const* followerEntry = sGarrFollowerStore.LookupEntry(garrFollowerId); - if (_followerIds.count(garrFollowerId) || !followerEntry) + if (!followerEntry) + { + addFollowerResult.Result = GARRISON_ERROR_FOLLOWER_EXISTS; + _owner->SendDirectMessage(addFollowerResult.Write()); + return; + } + + if (followerEntry->GarrTypeID != static_cast(GetType())) + { + addFollowerResult.Result = GARRISON_ERROR_INVALID_GARRISON; + _owner->SendDirectMessage(addFollowerResult.Write()); + return; + } + + if (_followerIds.count(garrFollowerId)) { addFollowerResult.Result = GARRISON_ERROR_FOLLOWER_EXISTS; _owner->SendDirectMessage(addFollowerResult.Write()); @@ -505,7 +1012,8 @@ void Garrison::AddFollower(uint32 garrFollowerId) Follower& follower = _followers[dbId]; follower.PacketInfo.DbID = dbId; follower.PacketInfo.GarrFollowerID = garrFollowerId; - follower.PacketInfo.Quality = followerEntry->Quality; // TODO: handle magic upgrades + // Initial quality from DB2; quality upgrades happen post-recruit via SPELL_EFFECT_SET_FOLLOWER_QUALITY (Spell::EffectSetFollowerQuality) + follower.PacketInfo.Quality = followerEntry->Quality; follower.PacketInfo.FollowerLevel = followerEntry->FollowerLevel; follower.PacketInfo.ItemLevelWeapon = followerEntry->ItemLevelWeapon; follower.PacketInfo.ItemLevelArmor = followerEntry->ItemLevelArmor; @@ -514,6 +1022,7 @@ void Garrison::AddFollower(uint32 garrFollowerId) follower.PacketInfo.CurrentMissionID = 0; follower.PacketInfo.AbilityID = sGarrisonMgr.RollFollowerAbilities(garrFollowerId, followerEntry, follower.PacketInfo.Quality, GetFaction(), true); follower.PacketInfo.FollowerStatus = 0; + follower.PacketInfo.ZoneSupportSpellID = sGarrisonMgr.GetFollowerZoneSupportSpell(garrFollowerId, GetFaction()); addFollowerResult.Follower = follower.PacketInfo; _owner->SendDirectMessage(addFollowerResult.Write()); @@ -521,6 +1030,38 @@ void Garrison::AddFollower(uint32 garrFollowerId) _owner->UpdateCriteria(CriteriaType::RecruitGarrisonFollower, follower.PacketInfo.DbID); } +void Garrison::AddTroop(uint32 garrFollowerId, uint32 durability) +{ + WorldPackets::Garrison::GarrisonAddFollowerResult addFollowerResult; + addFollowerResult.GarrTypeID = GetType(); + GarrFollowerEntry const* followerEntry = sGarrFollowerStore.LookupEntry(garrFollowerId); + if (!followerEntry) + { + addFollowerResult.Result = GARRISON_ERROR_INVALID_FOLLOWER; + _owner->SendDirectMessage(addFollowerResult.Write()); + return; + } + + uint64 dbId = sGarrisonMgr.GenerateFollowerDbId(); + Follower& follower = _followers[dbId]; + follower.PacketInfo.DbID = dbId; + follower.PacketInfo.GarrFollowerID = garrFollowerId; + follower.PacketInfo.Quality = followerEntry->Quality; + follower.PacketInfo.FollowerLevel = followerEntry->FollowerLevel; + follower.PacketInfo.ItemLevelWeapon = followerEntry->ItemLevelWeapon; + follower.PacketInfo.ItemLevelArmor = followerEntry->ItemLevelArmor; + follower.PacketInfo.Xp = 0; + follower.PacketInfo.Durability = durability; + follower.PacketInfo.CurrentBuildingID = 0; + follower.PacketInfo.CurrentMissionID = 0; + follower.PacketInfo.AbilityID = sGarrisonMgr.RollFollowerAbilities(garrFollowerId, followerEntry, follower.PacketInfo.Quality, GetFaction(), true); + follower.PacketInfo.FollowerStatus = FOLLOWER_STATUS_TROOP; + follower.PacketInfo.ZoneSupportSpellID = sGarrisonMgr.GetFollowerZoneSupportSpell(garrFollowerId, GetFaction()); + + addFollowerResult.Follower = follower.PacketInfo; + _owner->SendDirectMessage(addFollowerResult.Write()); +} + Garrison::Follower const* Garrison::GetFollower(uint64 dbId) const { auto itr = _followers.find(dbId); @@ -530,12 +1071,23 @@ Garrison::Follower const* Garrison::GetFollower(uint64 dbId) const return nullptr; } +Garrison::Follower const* Garrison::GetFollowerByEntry(uint32 garrFollowerId) const +{ + for (auto const& [dbId, follower] : _followers) + if (follower.PacketInfo.GarrFollowerID == garrFollowerId) + return &follower; + + return nullptr; +} + void Garrison::BuildInfoPacket(WorldPackets::Garrison::GarrisonInfo& garrison) const { garrison.GarrTypeID = GetType(); garrison.GarrSiteID = _siteLevel->GarrSiteID; garrison.GarrSiteLevelID = _siteLevel->ID; garrison.NumFollowerActivationsRemaining = _followerActivationsRemainingToday; + garrison.NumMissionsStartedToday = _missionsStartedToday; + garrison.ArchivedMissions = _archivedMissions; for (auto& p : _plots) { Plot const& plot = p.second; @@ -546,6 +1098,42 @@ void Garrison::BuildInfoPacket(WorldPackets::Garrison::GarrisonInfo& garrison) c for (auto const& p : _followers) garrison.Followers.push_back(&p.second.PacketInfo); + + // Missions: inline Encounters/Rewards/OvermaxRewards must be empty in the + // GarrisonInfo mission structs — the rewards go ONLY in the garrison-level + // parallel arrays. The client reads both, so writing them in both places + // causes a packet desync (double data). We build temporary copies with + // the inline vectors cleared. + _infoMissions.clear(); + _infoMissions.reserve(_missions.size()); + for (auto const& p : _missions) + { + auto& copy = _infoMissions.emplace_back(p.second.PacketInfo); + garrison.MissionRewards.push_back(copy.Rewards); + garrison.MissionOvermaxRewards.push_back(copy.OvermaxRewards); + copy.Encounters.clear(); + copy.Rewards.clear(); + copy.OvermaxRewards.clear(); + garrison.Missions.push_back(©); + garrison.CanStartMission.push_back(true); + } + + for (auto const& [talentId, talent] : _talents) + { + WorldPackets::Garrison::GarrisonTalent garrisonTalent; + garrisonTalent.GarrTalentID = talent.GarrTalentID; + garrisonTalent.Rank = talent.Rank; + garrisonTalent.ResearchStartTime = time_t(talent.ResearchStartTime); + garrisonTalent.Flags = talent.Flags; + if (talent.SoulbindConduitID != 0) + { + WorldPackets::Garrison::GarrisonTalentSocketData socket; + socket.SoulbindConduitID = talent.SoulbindConduitID; + socket.SoulbindConduitRank = talent.SoulbindConduitRank; + garrisonTalent.Socket = socket; + } + garrison.Talents.push_back(garrisonTalent); + } } void Garrison::SendRemoteInfo() const @@ -571,6 +1159,7 @@ void Garrison::SendBlueprintAndSpecializationData() WorldPackets::Garrison::GarrisonRequestBlueprintAndSpecializationDataResult data; data.GarrTypeID = GetType(); data.BlueprintsKnown = &_knownBuildings; + data.SpecializationsKnown = &_knownSpecializations; _owner->SendDirectMessage(data.Write()); } @@ -590,6 +1179,1632 @@ void Garrison::SendMapData(Player* receiver) const receiver->SendDirectMessage(mapData.Write()); } +void Garrison::SendMissionStartConditionUpdate() const +{ + WorldPackets::Garrison::GarrisonMissionStartConditionUpdate update; + update.MissionRecIDs.reserve(_missions.size()); + update.CanStartMission.reserve(_missions.size()); + + for (auto const& [dbId, mission] : _missions) + { + update.MissionRecIDs.push_back(mission.PacketInfo.MissionRecID); + update.CanStartMission.push_back(mission.PacketInfo.MissionState == 0); + } + + _owner->SendDirectMessage(update.Write()); +} + +void Garrison::SendDeleteExpiredMissionsResult() const +{ + WorldPackets::Garrison::DeleteExpiredMissionsResult result; + result.GarrTypeID = static_cast(_garrType); + result.Result = GARRISON_SUCCESS; + result.Succeeded = true; + result.LegionUnkBit = true; + // RemovedMissions is empty — expired missions are already cleaned up by RemoveExpiredMissions() + _owner->SendDirectMessage(result.Write()); +} + +void Garrison::SendTroopQualityRefresh() const +{ + // Sniff-confirmed: server sends GarrisonFollowerChangedQuality for each troop + // at login BEFORE the main GetGarrisonInfoResult packet + for (auto const& [dbId, follower] : _followers) + { + if (follower.PacketInfo.FollowerStatus & FOLLOWER_STATUS_TROOP) + { + WorldPackets::Garrison::GarrisonFollowerChangedQuality changedQuality; + changedQuality.OldFollower = follower.PacketInfo; + changedQuality.Follower = follower.PacketInfo; + _owner->SendDirectMessage(changedQuality.Write()); + } + } +} + +// ============================================================ +// Follower management +// ============================================================ + +Garrison::Follower* Garrison::GetFollower(uint64 dbId) +{ + auto itr = _followers.find(dbId); + if (itr != _followers.end()) + return &itr->second; + + return nullptr; +} + +void Garrison::RemoveFollower(uint64 dbId) +{ + WorldPackets::Garrison::GarrisonRemoveFollowerResult removeFollowerResult; + removeFollowerResult.GarrTypeID = GetType(); + removeFollowerResult.Result = GARRISON_SUCCESS; + + Follower const* follower = GetFollower(dbId); + if (!follower) + { + removeFollowerResult.Result = GARRISON_ERROR_INVALID_FOLLOWER; + _owner->SendDirectMessage(removeFollowerResult.Write()); + return; + } + + if (follower->PacketInfo.CurrentMissionID != 0) + { + removeFollowerResult.Result = GARRISON_ERROR_FOLLOWER_ALREADY_ON_MISSION; + _owner->SendDirectMessage(removeFollowerResult.Write()); + return; + } + + removeFollowerResult.FollowerDBID = dbId; + removeFollowerResult.Destroyed = 1; + _followerIds.erase(follower->PacketInfo.GarrFollowerID); + _followers.erase(dbId); + _owner->SendDirectMessage(removeFollowerResult.Write()); +} + +void Garrison::SetFollowerFavorite(uint64 dbId, bool favorite) +{ + WorldPackets::Garrison::GarrisonFollowerChangedFlags result; + result.Result = GARRISON_SUCCESS; + + Follower* follower = GetFollower(dbId); + if (!follower) + { + result.Result = GARRISON_ERROR_INVALID_FOLLOWER; + _owner->SendDirectMessage(result.Write()); + return; + } + + // Troops cannot be favorited + if (follower->PacketInfo.FollowerStatus & FOLLOWER_STATUS_TROOP) + { + result.Result = GARRISON_ERROR_INVALID_FOLLOWER; + _owner->SendDirectMessage(result.Write()); + return; + } + + if (favorite) + follower->PacketInfo.FollowerStatus |= FOLLOWER_STATUS_FAVORITE; + else + follower->PacketInfo.FollowerStatus &= ~FOLLOWER_STATUS_FAVORITE; + + result.Follower = follower->PacketInfo; + _owner->SendDirectMessage(result.Write()); +} + +void Garrison::SetFollowerInactive(uint64 dbId, bool inactive) +{ + WorldPackets::Garrison::GarrisonFollowerChangedFlags result; + result.Result = GARRISON_SUCCESS; + + Follower* follower = GetFollower(dbId); + if (!follower) + { + result.Result = GARRISON_ERROR_INVALID_FOLLOWER; + _owner->SendDirectMessage(result.Write()); + return; + } + + // Troops cannot be activated/deactivated + if (follower->PacketInfo.FollowerStatus & FOLLOWER_STATUS_TROOP) + { + result.Result = GARRISON_ERROR_INVALID_FOLLOWER; + _owner->SendDirectMessage(result.Write()); + return; + } + + if (follower->PacketInfo.CurrentMissionID != 0) + { + result.Result = GARRISON_ERROR_FOLLOWER_ALREADY_ON_MISSION; + _owner->SendDirectMessage(result.Write()); + return; + } + + if (inactive) + { + follower->PacketInfo.FollowerStatus |= FOLLOWER_STATUS_INACTIVE; + } + else + { + // Check champion activation cap before activating + GarrFollowerTypeEntry const* followerType = sGarrisonMgr.GetFollowerTypeForGarrType(static_cast(GetType())); + if (followerType && followerType->MaxFollowers > 0) + { + uint32 activeCount = 0; + for (auto const& p : _followers) + { + if (!(p.second.PacketInfo.FollowerStatus & FOLLOWER_STATUS_INACTIVE) + && !(p.second.PacketInfo.FollowerStatus & FOLLOWER_STATUS_TROOP)) + ++activeCount; + } + + if (activeCount >= followerType->MaxFollowers + GetBonusFollowerSlots()) + { + result.Result = GARRISON_ERROR_FOLLOWER_SOFT_CAP_EXCEEDED; + _owner->SendDirectMessage(result.Write()); + return; + } + } + + // Activating a follower costs one daily activation + if (_followerActivationsRemainingToday == 0) + { + result.Result = GARRISON_ERROR_FOLLOWER_ACTIVATION_UNAVAILABLE; + _owner->SendDirectMessage(result.Write()); + return; + } + + follower->PacketInfo.FollowerStatus &= ~FOLLOWER_STATUS_INACTIVE; + --_followerActivationsRemainingToday; + } + + result.Follower = follower->PacketInfo; + _owner->SendDirectMessage(result.Write()); +} + +void Garrison::RenameFollower(uint64 dbId, std::string const& name) +{ + WorldPackets::Garrison::GarrisonRenameFollowerResult result; + result.Result = GARRISON_SUCCESS; + + Follower* follower = GetFollower(dbId); + if (!follower) + { + result.Result = GARRISON_ERROR_INVALID_FOLLOWER; + _owner->SendDirectMessage(result.Write()); + return; + } + + // Troops cannot be renamed + if (follower->PacketInfo.FollowerStatus & FOLLOWER_STATUS_TROOP) + { + result.Result = GARRISON_ERROR_INVALID_FOLLOWER; + result.Follower = follower->PacketInfo; + _owner->SendDirectMessage(result.Write()); + return; + } + + follower->PacketInfo.CustomName = name; + result.Follower = follower->PacketInfo; + _owner->SendDirectMessage(result.Write()); +} + +void Garrison::AssignFollowerToBuilding(uint64 dbId, uint32 plotInstanceId) +{ + WorldPackets::Garrison::GarrisonAssignFollowerToBuildingResult result; + result.Result = GARRISON_SUCCESS; + + Follower* follower = GetFollower(dbId); + if (!follower) + { + result.Result = GARRISON_ERROR_INVALID_FOLLOWER; + _owner->SendDirectMessage(result.Write()); + return; + } + + Plot* plot = GetPlot(plotInstanceId); + if (!plot || !plot->BuildingInfo.PacketInfo || !plot->BuildingInfo.PacketInfo->Active) + { + result.Result = GARRISON_ERROR_INVALID_PLOT_INSTANCEID; + _owner->SendDirectMessage(result.Write()); + return; + } + + if (follower->PacketInfo.CurrentMissionID != 0) + { + result.Result = GARRISON_ERROR_FOLLOWER_ALREADY_ON_MISSION; + _owner->SendDirectMessage(result.Write()); + return; + } + + result.FollowerDBID = dbId; + result.PlotInstanceID = plotInstanceId; + follower->PacketInfo.CurrentBuildingID = plot->BuildingInfo.PacketInfo->GarrBuildingID; + _owner->SendDirectMessage(result.Write()); +} + +void Garrison::RemoveFollowerFromBuilding(uint64 dbId) +{ + WorldPackets::Garrison::GarrisonRemoveFollowerFromBuildingResult result; + result.Result = GARRISON_SUCCESS; + + Follower* follower = GetFollower(dbId); + if (!follower) + { + result.Result = GARRISON_ERROR_INVALID_FOLLOWER; + _owner->SendDirectMessage(result.Write()); + return; + } + + result.FollowerDBID = dbId; + follower->PacketInfo.CurrentBuildingID = 0; + _owner->SendDirectMessage(result.Write()); +} + +// ============================================================ +// Mission management +// ============================================================ + +void Garrison::PopulateMissionData(Mission& mission, GarrMissionEntry const* missionEntry) const +{ + // Populate encounters from DB2 + if (std::vector const* missionEncounters = sGarrisonMgr.GetMissionEncounters(missionEntry->ID)) + { + for (GarrMissionXEncounterEntry const* missionEncounter : *missionEncounters) + { + GarrEncounterEntry const* encounterEntry = sGarrEncounterStore.LookupEntry(missionEncounter->GarrEncounterID); + if (!encounterEntry) + continue; + + WorldPackets::Garrison::GarrisonEncounter encounter; + encounter.GarrEncounterID = encounterEntry->ID; + + // Populate mechanics for this encounter + if (std::vector const* mechanics = sGarrisonMgr.GetEncounterMechanics(encounterEntry->ID)) + { + for (GarrMechanicEntry const* mechanic : *mechanics) + encounter.Mechanics.push_back(mechanic->GarrMechanicTypeID); + } + + // Also add the encounter's environment mechanic type if it has one + if (encounterEntry->EnvGarrMechanicTypeID != 0) + encounter.Mechanics.push_back(encounterEntry->EnvGarrMechanicTypeID); + + // Populate auto-combat data from combatant linked to this encounter + if (GarrAutoCombatantEntry const* combatant = sGarrisonMgr.GetAutoCombatantForEncounter(encounterEntry->ID)) + { + encounter.GarrAutoCombatantID = combatant->ID; + encounter.Health = combatant->Health; + encounter.MaxHealth = combatant->MaxHealth; + encounter.Attack = combatant->Attack; + encounter.BoardIndex = static_cast(combatant->BoardIndex); + } + + mission.PacketInfo.Encounters.push_back(std::move(encounter)); + } + } + + // Populate rewards from OvermaxRewardPackID (used for both base and bonus rewards) + // WoD garrison missions use the RewardPack system + if (missionEntry->OvermaxRewardPackID != 0) + { + // Items from RewardPackXItem + if (std::vector const* items = sDB2Manager.GetRewardPackItemsByRewardID(missionEntry->OvermaxRewardPackID)) + { + for (RewardPackXItemEntry const* item : *items) + { + WorldPackets::Garrison::GarrisonMissionReward reward; + reward.ItemID = item->ItemID; + reward.ItemQuantity = item->ItemQuantity; + mission.PacketInfo.OvermaxRewards.push_back(std::move(reward)); + } + } + + // Currency from RewardPackXCurrencyType + if (std::vector const* currencies = sDB2Manager.GetRewardPackCurrencyTypesByRewardID(missionEntry->OvermaxRewardPackID)) + { + for (RewardPackXCurrencyTypeEntry const* currency : *currencies) + { + WorldPackets::Garrison::GarrisonMissionReward reward; + reward.CurrencyID = currency->CurrencyTypeID; + reward.CurrencyQuantity = currency->Quantity; + mission.PacketInfo.OvermaxRewards.push_back(std::move(reward)); + } + } + + // Gold from RewardPack + if (RewardPackEntry const* pack = sRewardPackStore.LookupEntry(missionEntry->OvermaxRewardPackID)) + { + if (pack->Money > 0) + { + WorldPackets::Garrison::GarrisonMissionReward reward; + reward.CurrencyID = 0; // Gold + reward.CurrencyQuantity = pack->Money; + mission.PacketInfo.OvermaxRewards.push_back(std::move(reward)); + } + } + } + + // Base rewards: follower XP is always included, plus any from GarrMissionSetID-linked packs + // For WoD garrisons, the primary reward is follower XP + whatever the mission offers + if (missionEntry->BaseFollowerXP > 0) + { + WorldPackets::Garrison::GarrisonMissionReward reward; + reward.FollowerXP = missionEntry->BaseFollowerXP; + mission.PacketInfo.Rewards.push_back(std::move(reward)); + } + + // Add currency/gold from mission cost currency as a reward if mission uses a reward pack + // (many missions grant garrison resources as primary reward) + if (missionEntry->MissionCostCurrencyTypesID != 0 && missionEntry->MissionCost > 0) + { + // The reward is typically more than the cost, but varies per mission + // For now, base rewards are whatever the OvermaxRewards specify (this is correct for WoD) + } +} + +void Garrison::AddMission(uint32 garrMissionId) +{ + GarrMissionEntry const* missionEntry = sGarrMissionStore.LookupEntry(garrMissionId); + if (!missionEntry) + return; + + // Don't add duplicate missions + if (_activeMissionRecIDs.count(garrMissionId)) + return; + + uint64 dbId = GenerateMissionDbId(); + Mission& mission = _missions[dbId]; + mission.PacketInfo.DbID = dbId; + mission.PacketInfo.MissionRecID = garrMissionId; + mission.PacketInfo.OfferTime = GameTime::GetGameTime(); + mission.PacketInfo.OfferDuration = Seconds(missionEntry->OfferDuration); + mission.PacketInfo.StartTime = time_t(2288912640); + mission.PacketInfo.TravelDuration = Seconds(missionEntry->TravelDuration); + mission.PacketInfo.MissionDuration = Seconds(missionEntry->MissionDuration); + mission.PacketInfo.MissionState = 0; // Offered + mission.PacketInfo.SuccessChance = 0; + mission.PacketInfo.Flags = missionEntry->Flags; + mission.PacketInfo.MissionScalar = missionEntry->AutoMissionScalar; + + // Populate encounters and rewards from DB2 data + PopulateMissionData(mission, missionEntry); + + _activeMissionRecIDs.insert(garrMissionId); + + WorldPackets::Garrison::GarrisonAddMissionResult addMissionResult; + addMissionResult.GarrTypeID = missionEntry->GarrTypeID; + addMissionResult.Result = GARRISON_SUCCESS; + addMissionResult.State = 0; + addMissionResult.Mission = mission.PacketInfo; + addMissionResult.CanStartMission = true; + _owner->SendDirectMessage(addMissionResult.Write()); +} + +Garrison::Mission const* Garrison::GetMission(uint64 dbId) const +{ + auto itr = _missions.find(dbId); + if (itr != _missions.end()) + return &itr->second; + + return nullptr; +} + +Garrison::Mission* Garrison::GetMission(uint64 dbId) +{ + auto itr = _missions.find(dbId); + if (itr != _missions.end()) + return &itr->second; + + return nullptr; +} + +Garrison::Mission const* Garrison::GetMissionByRecID(uint32 missionRecID) const +{ + for (auto const& p : _missions) + if (static_cast(p.second.PacketInfo.MissionRecID) == missionRecID) + return &p.second; + + return nullptr; +} + +Garrison::Mission* Garrison::GetMissionByRecID(uint32 missionRecID) +{ + for (auto& p : _missions) + if (static_cast(p.second.PacketInfo.MissionRecID) == missionRecID) + return &p.second; + + return nullptr; +} + +int32 Garrison::CalculateSuccessChance(uint32 missionRecID, std::vector const& followerDBIDs) const +{ + GarrMissionEntry const* missionEntry = sGarrMissionStore.LookupEntry(missionRecID); + if (!missionEntry) + return 0; + + int32 successChance = missionEntry->BaseCompletionChance; + + // Collect all follower counter abilities + std::unordered_set counteredMechanicCategories; + int32 totalFollowerLevel = 0; + int32 totalFollowerItemLevel = 0; + uint32 followerCount = 0; + + for (uint64 followerDbId : followerDBIDs) + { + Follower const* follower = GetFollower(followerDbId); + if (!follower) + continue; + + ++followerCount; + totalFollowerLevel += follower->PacketInfo.FollowerLevel; + totalFollowerItemLevel += follower->GetItemLevel(); + + // Check each follower ability against mission mechanics + for (GarrAbilityEntry const* ability : follower->PacketInfo.AbilityID) + { + if (!ability || (ability->Flags & GARRISON_ABILITY_FLAG_TRAIT)) + continue; // Skip traits, only counter abilities matter + + // Check against each encounter's mechanics + Mission const* missionData = GetMissionByRecID(missionRecID); + if (missionData) + { + for (auto const& encounter : missionData->PacketInfo.Encounters) + { + for (int32 mechanicTypeID : encounter.Mechanics) + { + GarrMechanicTypeEntry const* mechanicType = sGarrisonMgr.GetMechanicType(mechanicTypeID); + if (mechanicType && sGarrisonMgr.DoesAbilityCounterMechanic(ability, mechanicType)) + counteredMechanicCategories.insert(mechanicType->GarrAbilityCategoryID); + } + } + } + } + } + + if (followerCount == 0) + return 0; + + // Count total mechanics across all encounters + uint32 totalMechanics = 0; + Mission const* mission = GetMissionByRecID(missionRecID); + if (mission) + { + for (auto const& encounter : mission->PacketInfo.Encounters) + totalMechanics += static_cast(encounter.Mechanics.size()); + } + + // Each countered mechanic adds a bonus proportional to mission complexity + // For a typical mission with 3 mechanics, each counter is worth ~10-15% + if (totalMechanics > 0) + { + uint32 countered = static_cast(counteredMechanicCategories.size()); + float counterBonus = (static_cast(countered) / static_cast(totalMechanics)) * 45.0f; + successChance += static_cast(counterBonus); + } + + // Level difference penalty (only for under-leveled followers) + int32 avgFollowerLevel = totalFollowerLevel / static_cast(followerCount); + int32 levelDiff = avgFollowerLevel - static_cast(missionEntry->TargetLevel); + if (levelDiff < 0) + successChance += levelDiff * 3; // -3% per level below target + + // Item level bonus for missions with item level requirements + if (missionEntry->TargetItemLevel > 0 && followerCount > 0) + { + int32 avgFollowerItemLevel = totalFollowerItemLevel / static_cast(followerCount); + int32 iLvlDiff = avgFollowerItemLevel - static_cast(missionEntry->TargetItemLevel); + if (iLvlDiff > 0) + successChance += std::min(iLvlDiff / 10, 10); // +1% per 10 iLvl above target, capped at +10% + } + + return std::clamp(successChance, 0, 100); +} + +GarrisonError Garrison::StartMission(uint32 missionRecID, std::vector const& followerDBIDs) +{ + GarrMissionEntry const* missionEntry = sGarrMissionStore.LookupEntry(missionRecID); + if (!missionEntry) + return GARRISON_ERROR_INVALID_MISSION; + + Mission* mission = GetMissionByRecID(missionRecID); + if (!mission) + return GARRISON_ERROR_INVALID_MISSION; + + if (mission->PacketInfo.MissionState != 0) + return GARRISON_ERROR_ALREADY_ON_MISSION; + + if (followerDBIDs.size() > missionEntry->MaxFollowers) + return GARRISON_ERROR_MISSION_SIZE_INVALID; + + if (followerDBIDs.empty()) + return GARRISON_ERROR_MISSION_SIZE_INVALID; + + // Validate all followers + for (uint64 followerDbId : followerDBIDs) + { + Follower const* follower = GetFollower(followerDbId); + if (!follower) + return GARRISON_ERROR_INVALID_FOLLOWER; + + if (follower->PacketInfo.CurrentMissionID != 0) + return GARRISON_ERROR_FOLLOWER_ALREADY_ON_MISSION; + + if (follower->PacketInfo.FollowerStatus & FOLLOWER_STATUS_INACTIVE) + return GARRISON_ERROR_FOLLOWER_INACTIVE; + } + + // Check required followers (GarrMissionXFollower.db2) + if (std::vector const* requiredFollowers = sGarrisonMgr.GetMissionRequiredFollowers(missionRecID)) + { + for (GarrMissionXFollowerEntry const* required : *requiredFollowers) + { + bool found = false; + for (uint64 followerDbId : followerDBIDs) + { + if (Follower const* follower = GetFollower(followerDbId)) + { + if (static_cast(follower->PacketInfo.GarrFollowerID) == required->GarrFollowerID) + { + found = true; + break; + } + } + } + if (!found) + return GARRISON_ERROR_MISSION_MISSING_REQUIRED_FOLLOWER; + } + } + + // Deduct mission cost + if (missionEntry->MissionCost > 0) + { + if (missionEntry->MissionCostCurrencyTypesID != 0) + { + if (!_owner->HasCurrency(missionEntry->MissionCostCurrencyTypesID, missionEntry->MissionCost)) + return GARRISON_ERROR_NOT_ENOUGH_CURRENCY; + _owner->RemoveCurrency(missionEntry->MissionCostCurrencyTypesID, missionEntry->MissionCost, CurrencyDestroyReason::Garrison); + } + else + { + if (!_owner->HasEnoughMoney(uint64(missionEntry->MissionCost) * GOLD)) + return GARRISON_ERROR_NOT_ENOUGH_GOLD; + _owner->ModifyMoney(-int64(missionEntry->MissionCost) * GOLD, false); + } + } + + // Assign followers to mission + mission->CurrentFollowerDBIDs = followerDBIDs; + for (uint64 followerDbId : followerDBIDs) + { + if (Follower* follower = GetFollower(followerDbId)) + follower->PacketInfo.CurrentMissionID = missionRecID; + } + + // Calculate success chance using encounter-based mechanic system + int32 successChance = CalculateSuccessChance(missionRecID, followerDBIDs); + + mission->PacketInfo.MissionState = 1; // In Progress + mission->PacketInfo.StartTime = GameTime::GetGameTime(); + mission->PacketInfo.SuccessChance = successChance; + + // Track missions started today (daily reset based on day boundary) + uint32 today = static_cast(GameTime::GetGameTime() / DAY); + if (today != _lastMissionStartDay) + { + _missionsStartedToday = 0; + _lastMissionStartDay = today; + } + ++_missionsStartedToday; + + return GARRISON_SUCCESS; +} + +GarrisonError Garrison::CompleteMission(uint32 missionRecID) +{ + Mission* mission = GetMissionByRecID(missionRecID); + if (!mission) + return GARRISON_ERROR_INVALID_MISSION; + + if (mission->PacketInfo.MissionState != 1) // Not in progress + return GARRISON_ERROR_NOT_ON_MISSION; + + // Check if mission duration has elapsed + time_t now = GameTime::GetGameTime(); + time_t missionEnd = time_t(mission->PacketInfo.StartTime) + + Seconds(mission->PacketInfo.TravelDuration).count() + + Seconds(mission->PacketInfo.MissionDuration).count(); + + if (now < missionEnd) + return GARRISON_ERROR_MISSION_NOT_COMPLETE; + + mission->PacketInfo.MissionState = 2; // Completed + return GARRISON_SUCCESS; +} + +GarrisonError Garrison::ClaimMissionReward(uint32 missionRecID) +{ + Mission* mission = GetMissionByRecID(missionRecID); + if (!mission) + return GARRISON_ERROR_INVALID_MISSION; + + if (mission->PacketInfo.MissionState != 2 && mission->PacketInfo.MissionState != 3) + return GARRISON_ERROR_MISSION_NOT_COMPLETE; + + GarrMissionEntry const* missionEntry = sGarrMissionStore.LookupEntry(missionRecID); + if (!missionEntry) + return GARRISON_ERROR_INVALID_MISSION; + + // Determine success: use auto-combat simulation for adventure missions, + // otherwise roll against calculated success chance + bool succeeded = false; + bool isAutoCombatMission = false; + + for (auto const& encounter : mission->PacketInfo.Encounters) + { + if (encounter.GarrAutoCombatantID != 0) + { + isAutoCombatMission = true; + break; + } + } + + if (isAutoCombatMission) + { + // Build player units from assigned followers + std::vector playerUnits; + int8 boardIdx = 0; + for (uint64 followerDbId : mission->CurrentFollowerDBIDs) + { + if (Follower const* follower = GetFollower(followerDbId)) + { + AutoCombatCombatant unit = GarrisonAutoCombat::BuildFollowerCombatant( + follower->PacketInfo.FollowerLevel, follower->PacketInfo.Quality, + follower->PacketInfo.ItemLevelWeapon, follower->PacketInfo.ItemLevelArmor, + follower->PacketInfo.BoardIndex >= 0 ? follower->PacketInfo.BoardIndex : boardIdx, + followerDbId); + playerUnits.push_back(std::move(unit)); + ++boardIdx; + } + } + + // Build enemy units from mission encounters + std::vector enemyUnits; + for (auto const& encounter : mission->PacketInfo.Encounters) + { + if (encounter.GarrAutoCombatantID == 0) + continue; + + GarrAutoCombatantEntry const* combatant = + sGarrisonMgr.GetAutoCombatant(encounter.GarrAutoCombatantID); + if (!combatant) + continue; + + enemyUnits.push_back(GarrisonAutoCombat::BuildEnemyCombatant(combatant)); + } + + AutoCombatResult combatResult = + GarrisonAutoCombat::SimulateCombat(playerUnits, enemyUnits); + succeeded = combatResult.PlayerWon; + + // combatResult.CombatLog is fully populated (rounds + per-target events) but + // not serialized into SMSG_GARRISON_COMPLETE_MISSION_RESULT here. The wire + // format for the auto-combat transcript (JamGarrisonAutoMissionRoundInfo / + // EventInfo / CombatantInfo) is brief Open Q #2 and not yet decoded — the + // client likely expects a nested rounds[].events[].combatants[].spell[] tree. + // SL Adventures replay UI will not show round-by-round detail until this is + // wired; mission outcome (succeeded/failed) is still reported correctly. + + TC_LOG_DEBUG("garrison", "Auto-combat for mission %u: %s in %d rounds", + missionRecID, succeeded ? "WON" : "LOST", combatResult.TotalRounds); + } + else + { + succeeded = static_cast(urand(0, 99)) < mission->PacketInfo.SuccessChance; + } + + // Award follower XP (awarded regardless of success) and handle troop durability + std::vector troopsToRemove; + uint32 followerXP = missionEntry->BaseFollowerXP; + for (uint64 followerDbId : mission->CurrentFollowerDBIDs) + { + if (Follower* follower = GetFollower(followerDbId)) + { + follower->PacketInfo.CurrentMissionID = 0; + + // Troops lose 1 durability per mission + if (follower->PacketInfo.FollowerStatus & FOLLOWER_STATUS_TROOP) + { + if (follower->PacketInfo.Durability > 0) + --follower->PacketInfo.Durability; + + if (follower->PacketInfo.Durability == 0) + troopsToRemove.push_back(followerDbId); + } + + if (followerXP > 0 + && !(follower->PacketInfo.FollowerStatus & FOLLOWER_STATUS_NO_XP_GAIN) + && !(follower->PacketInfo.FollowerStatus & FOLLOWER_STATUS_TROOP)) + { + // Capture old state before XP modification + WorldPackets::Garrison::GarrisonFollower oldFollowerState = follower->PacketInfo; + + GarrFollowerEntry const* followerEntry = sGarrFollowerStore.LookupEntry(follower->PacketInfo.GarrFollowerID); + uint8 followerTypeID = followerEntry ? followerEntry->GarrFollowerTypeID : FOLLOWER_TYPE_GARRISON; + + follower->PacketInfo.Xp += followerXP; + + // Level progression using GarrFollowerLevelXP DB2 + GarrFollowerLevelXPEntry const* levelXP = sGarrisonMgr.GetFollowerLevelXP(followerTypeID, follower->PacketInfo.FollowerLevel); + while (levelXP && levelXP->XpToNextLevel > 0 && follower->PacketInfo.Xp >= levelXP->XpToNextLevel) + { + follower->PacketInfo.Xp -= levelXP->XpToNextLevel; + follower->PacketInfo.FollowerLevel++; + levelXP = sGarrisonMgr.GetFollowerLevelXP(followerTypeID, follower->PacketInfo.FollowerLevel); + } + + // At max level, excess XP converts to quality (iLvl) progression + if (!levelXP || levelXP->XpToNextLevel == 0) + { + GarrFollowerQualityEntry const* qualityEntry = sGarrisonMgr.GetFollowerQuality(followerTypeID, follower->PacketInfo.Quality); + while (qualityEntry && qualityEntry->XpThreshold > 0 && follower->PacketInfo.Xp >= static_cast(qualityEntry->XpThreshold)) + { + follower->PacketInfo.Xp -= qualityEntry->XpThreshold; + follower->PacketInfo.Quality++; + + // Re-roll abilities for new quality tier + if (followerEntry) + { + follower->PacketInfo.AbilityID = sGarrisonMgr.RollFollowerAbilities( + follower->PacketInfo.GarrFollowerID, followerEntry, + follower->PacketInfo.Quality, GetFaction(), false); + } + + qualityEntry = sGarrisonMgr.GetFollowerQuality(followerTypeID, follower->PacketInfo.Quality); + } + + // Cap XP at 0 if no further progression is possible + if (!qualityEntry || qualityEntry->XpThreshold == 0) + follower->PacketInfo.Xp = 0; + } + + // Send follower XP update with old and new state + WorldPackets::Garrison::GarrisonFollowerChangedXP followerXPUpdate; + followerXPUpdate.Result = GARRISON_SUCCESS; + followerXPUpdate.TotalXp = followerXP; + followerXPUpdate.OldFollower = oldFollowerState; + followerXPUpdate.Follower = follower->PacketInfo; + _owner->SendDirectMessage(followerXPUpdate.Write()); + } + } + } + + // Remove troops that have exhausted their durability + for (uint64 troopDbId : troopsToRemove) + RemoveFollower(troopDbId); + + // Award mission rewards if succeeded + if (succeeded) + { + // Award base rewards + for (auto const& reward : mission->PacketInfo.Rewards) + { + if (reward.ItemID > 0 && reward.ItemQuantity > 0) + { + ItemPosCountVec dest; + if (_owner->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, reward.ItemID, reward.ItemQuantity) == EQUIP_ERR_OK) + { + if (Item* item = _owner->StoreNewItem(dest, reward.ItemID, true)) + _owner->SendNewItem(item, reward.ItemQuantity, true, false); + } + else + { + // Mail overflow items + MailDraft draft("Garrison Mission Reward", "A reward from a completed garrison mission."); + if (Item* item = Item::CreateItem(reward.ItemID, reward.ItemQuantity, ItemContext::NONE, _owner)) + { + item->SaveToDB(CharacterDatabaseTransaction(nullptr)); + draft.AddItem(item); + draft.SendMailTo(CharacterDatabaseTransaction(nullptr), MailReceiver(_owner), MailSender(MAIL_CREATURE, 0)); + } + } + } + if (reward.CurrencyID > 0 && reward.CurrencyQuantity > 0) + _owner->AddCurrency(reward.CurrencyID, reward.CurrencyQuantity, CurrencyGainSource::GarrisonMissionReward); + } + + // Check if bonus roll was done (state 3) and award overmax rewards + if (mission->PacketInfo.MissionState == 3) + { + for (auto const& reward : mission->PacketInfo.OvermaxRewards) + { + if (reward.ItemID > 0 && reward.ItemQuantity > 0) + { + ItemPosCountVec dest; + if (_owner->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, reward.ItemID, reward.ItemQuantity) == EQUIP_ERR_OK) + { + if (Item* item = _owner->StoreNewItem(dest, reward.ItemID, true)) + _owner->SendNewItem(item, reward.ItemQuantity, true, false); + } + else + { + MailDraft draft("Garrison Mission Bonus", "A bonus reward from a garrison mission."); + if (Item* item = Item::CreateItem(reward.ItemID, reward.ItemQuantity, ItemContext::NONE, _owner)) + { + item->SaveToDB(CharacterDatabaseTransaction(nullptr)); + draft.AddItem(item); + draft.SendMailTo(CharacterDatabaseTransaction(nullptr), MailReceiver(_owner), MailSender(MAIL_CREATURE, 0)); + } + } + } + if (reward.CurrencyID > 0 && reward.CurrencyQuantity > 0) + _owner->AddCurrency(reward.CurrencyID, reward.CurrencyQuantity, CurrencyGainSource::GarrisonMissionReward); + } + } + } + + // Salvage Yard is a WoD-only building (BuildingType 35) that has no equivalent in + // Class Order Halls / War Campaign / Covenant. Guard the WoD-specific reward path here + // — the equivalent expansion-specific bonus reward mechanics live in their own scripts. + if (GetType() == GARRISON_TYPE_GARRISON) + { + static constexpr uint32 BUILDING_TYPE_SALVAGE_YARD = 35; + static constexpr uint32 ITEM_SALVAGE_CRATE_SMALL = 114116; // Bag of Salvaged Goods (level 1) + static constexpr uint32 ITEM_SALVAGE_CRATE_MEDIUM = 114119; // Crate of Salvage (level 2) + static constexpr uint32 ITEM_SALVAGE_CRATE_LARGE = 114120; // Big Crate of Salvage (level 3) + + GarrBuildingEntry const* salvageYard = GetActiveBuildingByType(BUILDING_TYPE_SALVAGE_YARD); + if (salvageYard) + { + uint32 salvageItemId = 0; + if (salvageYard->UpgradeLevel >= 3) + salvageItemId = ITEM_SALVAGE_CRATE_LARGE; + else if (salvageYard->UpgradeLevel >= 2) + salvageItemId = ITEM_SALVAGE_CRATE_MEDIUM; + else + salvageItemId = ITEM_SALVAGE_CRATE_SMALL; + + if (salvageItemId) + { + ItemPosCountVec dest; + if (_owner->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, salvageItemId, 1) == EQUIP_ERR_OK) + { + if (Item* item = _owner->StoreNewItem(dest, salvageItemId, true)) + _owner->SendNewItem(item, 1, true, false); + } + else + { + MailDraft draft("Salvage", "Salvage from a garrison mission."); + if (Item* item = Item::CreateItem(salvageItemId, 1, ItemContext::NONE, _owner)) + { + item->SaveToDB(CharacterDatabaseTransaction(nullptr)); + draft.AddItem(item); + draft.SendMailTo(CharacterDatabaseTransaction(nullptr), MailReceiver(_owner), MailSender(MAIL_CREATURE, 0)); + } + } + } + } + } + + // Archive the completed mission + _archivedMissions.push_back(static_cast(missionRecID)); + + // Remove mission from active list + _activeMissionRecIDs.erase(missionRecID); + for (auto itr = _missions.begin(); itr != _missions.end(); ++itr) + { + if (static_cast(itr->second.PacketInfo.MissionRecID) == missionRecID) + { + _missions.erase(itr); + break; + } + } + + return GARRISON_SUCCESS; +} + +GarrisonError Garrison::MissionBonusRoll(uint32 missionRecID) +{ + Mission* mission = GetMissionByRecID(missionRecID); + if (!mission) + return GARRISON_ERROR_INVALID_MISSION; + + if (mission->PacketInfo.MissionState != 2) + return GARRISON_ERROR_MISSION_NOT_COMPLETE; + + // The bonus roll uses the same success chance as the mission + // If the roll succeeds, the overmax rewards will be given when claiming + bool bonusSucceeded = static_cast(urand(0, 99)) < mission->PacketInfo.SuccessChance; + + mission->PacketInfo.MissionState = bonusSucceeded ? 3 : 2; + // State 3 = bonus rolled successfully (overmax rewards will be awarded) + // Keep state 2 if bonus failed (only base rewards will be awarded on claim) + + return GARRISON_SUCCESS; +} + +void Garrison::RemoveMission(uint32 missionRecID) +{ + for (auto itr = _missions.begin(); itr != _missions.end(); ++itr) + { + if (static_cast(itr->second.PacketInfo.MissionRecID) == missionRecID) + { + // Unassign followers + for (uint64 followerDbId : itr->second.CurrentFollowerDBIDs) + if (Follower* follower = GetFollower(followerDbId)) + follower->PacketInfo.CurrentMissionID = 0; + + _activeMissionRecIDs.erase(missionRecID); + _missions.erase(itr); + + // Notify the client that the mission was removed + WorldPackets::Garrison::GarrisonDeleteMissionResult deleteMissionResult; + deleteMissionResult.Result = GARRISON_SUCCESS; + deleteMissionResult.MissionRecID = missionRecID; + deleteMissionResult.GarrTypeID = GetType(); + _owner->SendDirectMessage(deleteMissionResult.Write()); + break; + } + } +} + +bool Garrison::IsAutoCombatMission(Mission const& mission) const +{ + for (auto const& encounter : mission.PacketInfo.Encounters) + if (encounter.GarrAutoCombatantID != 0) + return true; + return false; +} + +void Garrison::RemoveExpiredMissions() +{ + time_t now = GameTime::GetGameTime(); + std::vector expiredMissions; + + for (auto const& p : _missions) + { + // Only check offered missions (not in-progress or completed) + if (p.second.PacketInfo.MissionState != 0) + continue; + + // Check if offer has expired + if (Seconds(p.second.PacketInfo.OfferDuration).count() > 0) + { + time_t offerEnd = time_t(p.second.PacketInfo.OfferTime) + Seconds(p.second.PacketInfo.OfferDuration).count(); + if (now >= offerEnd) + expiredMissions.push_back(p.second.PacketInfo.MissionRecID); + } + } + + for (uint32 missionRecID : expiredMissions) + RemoveMission(missionRecID); +} + +void Garrison::GenerateAvailableMissions() +{ + if (!_siteLevel) + return; + + // Use the garrison's own type for mission lookups + int8 garrTypeID = static_cast(GetType()); + + std::vector const* availableMissions = sGarrisonMgr.GetMissionsByGarrType(garrTypeID); + if (!availableMissions || availableMissions->empty()) + return; + + // Remove expired offers first + RemoveExpiredMissions(); + + // Count current offered missions (not in-progress or completed) + uint32 currentOffered = 0; + for (auto const& p : _missions) + if (p.second.PacketInfo.MissionState == 0) + ++currentOffered; + + // Target: up to 15 available missions at a time + static constexpr uint32 MAX_AVAILABLE_MISSIONS = 15; + if (currentOffered >= MAX_AVAILABLE_MISSIONS) + { + _lastMissionGenerationTime = GameTime::GetGameTime(); + return; + } + + uint32 missionsToGenerate = MAX_AVAILABLE_MISSIONS - currentOffered; + + // Get average follower level for filtering + int32 avgFollowerLevel = 0; + uint32 followerCount = 0; + for (auto const& p : _followers) + { + if (!(p.second.PacketInfo.FollowerStatus & FOLLOWER_STATUS_INACTIVE)) + { + avgFollowerLevel += p.second.PacketInfo.FollowerLevel; + ++followerCount; + } + } + + if (followerCount > 0) + avgFollowerLevel /= static_cast(followerCount); + else + avgFollowerLevel = 90; // Default for no followers + + // Build eligible mission pool + std::vector eligibleMissions; + for (GarrMissionEntry const* mission : *availableMissions) + { + // Skip missions already active + if (_activeMissionRecIDs.count(mission->ID)) + continue; + + // Filter by follower type matching this garrison's primary follower type + if (mission->GarrFollowerTypeID != sGarrisonMgr.GetPrimaryFollowerType(garrTypeID)) + continue; + + // Filter by target level (within +-5 levels of average follower level) + int32 levelDiff = std::abs(avgFollowerLevel - static_cast(mission->TargetLevel)); + if (levelDiff > 5) + continue; + + // Skip missions requiring more followers than available + uint32 availableFollowers = 0; + for (auto const& p : _followers) + { + if (p.second.PacketInfo.CurrentMissionID == 0 + && !(p.second.PacketInfo.FollowerStatus & FOLLOWER_STATUS_INACTIVE)) + ++availableFollowers; + } + + if (mission->MaxFollowers > availableFollowers) + continue; + + // Skip missions with 0 duration (usually internal/debug) + if (mission->MissionDuration == 0) + continue; + + eligibleMissions.push_back(mission); + } + + // Inn/Tavern level 2+ unlocks WoD treasure missions. Inn (BuildingType 34) is WoD-only; + // Class Order Halls / War Campaign / Covenants have their own bonus-mission generators + // outside this code path. + if (GetType() == GARRISON_TYPE_GARRISON) + { + static constexpr uint32 BUILDING_TYPE_INN = 34; + GarrBuildingEntry const* inn = GetActiveBuildingByType(BUILDING_TYPE_INN); + if (inn && inn->UpgradeLevel >= 2) + { + // Re-scan pool for treasure-flagged missions (Flags & 0x10 = treasure mission) + // that were excluded by level range, and add some with relaxed constraints + for (GarrMissionEntry const* mission : *availableMissions) + { + if (_activeMissionRecIDs.count(mission->ID)) + continue; + if (mission->GarrFollowerTypeID != sGarrisonMgr.GetPrimaryFollowerType(garrTypeID)) + continue; + if (mission->MissionDuration == 0) + continue; + // Treasure missions have flag 0x10 + if (!(mission->Flags & 0x10)) + continue; + // Don't duplicate + bool alreadyEligible = false; + for (GarrMissionEntry const* existing : eligibleMissions) + { + if (existing->ID == mission->ID) + { + alreadyEligible = true; + break; + } + } + if (!alreadyEligible) + eligibleMissions.push_back(mission); + } + } + } + + // Random selection from eligible pool + if (eligibleMissions.size() > missionsToGenerate) + { + Trinity::Containers::RandomResize(eligibleMissions, missionsToGenerate); + } + + for (GarrMissionEntry const* missionEntry : eligibleMissions) + AddMission(missionEntry->ID); + + _lastMissionGenerationTime = GameTime::GetGameTime(); +} + +uint64 Garrison::GenerateMissionDbId() +{ + return _missionDbIdGenerator++; +} + +// ============================================================ +// Recruitment +// ============================================================ + +void Garrison::SetRecruitmentPreferences(uint32 abilityId, uint32 traitId) +{ + // Validate ability if specified + if (abilityId) + { + GarrAbilityEntry const* ability = sGarrAbilityStore.LookupEntry(abilityId); + if (!ability || (ability->Flags & GARRISON_ABILITY_FLAG_TRAIT)) + { + TC_LOG_DEBUG("garrison", "Garrison::SetRecruitmentPreferences: Invalid ability {} (must be counter, not trait)", abilityId); + return; + } + } + + // Validate trait if specified + if (traitId) + { + GarrAbilityEntry const* trait = sGarrAbilityStore.LookupEntry(traitId); + if (!trait || !(trait->Flags & GARRISON_ABILITY_FLAG_TRAIT)) + { + TC_LOG_DEBUG("garrison", "Garrison::SetRecruitmentPreferences: Invalid trait {} (must have TRAIT flag)", traitId); + return; + } + } + + _recruitmentPreferenceAbilityId = abilityId; + _recruitmentPreferenceTraitId = traitId; + + TC_LOG_DEBUG("garrison", "Garrison::SetRecruitmentPreferences: Player {} set ability={}, trait={}", + _owner->GetGUID().ToString().c_str(), abilityId, traitId); +} + +void Garrison::GenerateRecruits(uint32 faction) +{ + _availableRecruits.clear(); + + // Find all followers of the garrison type that the player doesn't already have + std::vector eligibleFollowers; + for (GarrFollowerEntry const* follower : sGarrFollowerStore) + { + if (follower->GarrFollowerTypeID != sGarrisonMgr.GetPrimaryFollowerType(static_cast(GetType()))) + continue; + + // Skip followers the player already has + if (_followerIds.count(follower->ID)) + continue; + + // Skip unique followers that are faction-specific + if (follower->Flags & GARRISON_FOLLOWER_FLAG_UNIQUE) + continue; + + eligibleFollowers.push_back(follower); + } + + if (eligibleFollowers.empty()) + return; + + // Pick up to 3 random followers + Trinity::Containers::RandomResize(eligibleFollowers, std::min(3, eligibleFollowers.size())); + + auto buildRecruit = [&](GarrFollowerEntry const* followerEntry) -> WorldPackets::Garrison::GarrisonFollower + { + WorldPackets::Garrison::GarrisonFollower recruit; + recruit.DbID = 0; // Not yet in DB + recruit.GarrFollowerID = followerEntry->ID; + recruit.Quality = urand(1, 3); // Uncommon to Rare + recruit.FollowerLevel = std::max(90, _owner->GetLevel() - 5); + recruit.ItemLevelWeapon = 600; + recruit.ItemLevelArmor = 600; + recruit.CurrentMissionID = 0; + recruit.CurrentBuildingID = 0; + recruit.FollowerStatus = 0; + recruit.Xp = 0; + recruit.Durability = 5; + + // Roll abilities based on quality + std::list abilities = sGarrisonMgr.RollFollowerAbilities( + followerEntry->ID, followerEntry, recruit.Quality, faction, true); + + for (GarrAbilityEntry const* ability : abilities) + recruit.AbilityID.push_back(ability); + + return recruit; + }; + + for (GarrFollowerEntry const* followerEntry : eligibleFollowers) + _availableRecruits.push_back(buildRecruit(followerEntry)); + + // Apply recruitment preferences: guarantee the first recruit has the preferred ability/trait + // This mimics the Blizzlike behavior where the Inn/Tavern NPC allows the player to choose + // a specific counter or trait and one of the 3 recruits is guaranteed to have it + if ((_recruitmentPreferenceAbilityId || _recruitmentPreferenceTraitId) && !_availableRecruits.empty()) + { + WorldPackets::Garrison::GarrisonFollower& preferredRecruit = _availableRecruits[0]; + + // Re-roll abilities for the first recruit, forcing the preferred ones + preferredRecruit.AbilityID.clear(); + GarrFollowerEntry const* followerEntry = sGarrFollowerStore.LookupEntry(preferredRecruit.GarrFollowerID); + if (followerEntry) + { + std::list abilities = sGarrisonMgr.RollFollowerAbilities( + followerEntry->ID, followerEntry, preferredRecruit.Quality, faction, true); + + // Check if preferred ability is already in the rolled set; if not, replace the first non-forced counter + if (_recruitmentPreferenceAbilityId) + { + GarrAbilityEntry const* preferredAbility = sGarrAbilityStore.LookupEntry(_recruitmentPreferenceAbilityId); + if (preferredAbility) + { + bool found = false; + for (GarrAbilityEntry const* a : abilities) + { + if (a->ID == _recruitmentPreferenceAbilityId) + { + found = true; + break; + } + } + if (!found) + { + // Replace first counter (non-trait, non-forced) with the preferred one + for (auto it = abilities.begin(); it != abilities.end(); ++it) + { + if (!((*it)->Flags & GARRISON_ABILITY_FLAG_TRAIT) && + !((*it)->Flags & GARRISON_ABILITY_FLAG_CANNOT_REMOVE)) + { + *it = preferredAbility; + break; + } + } + } + } + } + + // Check if preferred trait is already in the rolled set; if not, replace the first non-forced trait + if (_recruitmentPreferenceTraitId) + { + GarrAbilityEntry const* preferredTrait = sGarrAbilityStore.LookupEntry(_recruitmentPreferenceTraitId); + if (preferredTrait) + { + bool found = false; + for (GarrAbilityEntry const* a : abilities) + { + if (a->ID == _recruitmentPreferenceTraitId) + { + found = true; + break; + } + } + if (!found) + { + // Replace first trait (non-forced) with the preferred one + for (auto it = abilities.begin(); it != abilities.end(); ++it) + { + if (((*it)->Flags & GARRISON_ABILITY_FLAG_TRAIT) && + !((*it)->Flags & GARRISON_ABILITY_FLAG_CANNOT_REMOVE)) + { + *it = preferredTrait; + break; + } + } + } + } + } + + for (GarrAbilityEntry const* ability : abilities) + preferredRecruit.AbilityID.push_back(ability); + } + } + + // Clear preferences after generating recruits (one-time use per recruitment cycle) + _recruitmentPreferenceAbilityId = 0; + _recruitmentPreferenceTraitId = 0; +} + +GarrisonError Garrison::RecruitFollower(uint32 garrFollowerID) +{ + // Find the recruit in available recruits + auto itr = std::find_if(_availableRecruits.begin(), _availableRecruits.end(), + [garrFollowerID](WorldPackets::Garrison::GarrisonFollower const& f) { + return f.GarrFollowerID == garrFollowerID; + }); + + if (itr == _availableRecruits.end()) + return GARRISON_ERROR_INVALID_AVAILABLE_RECRUIT; + + // Check if already recruited + if (_followerIds.count(garrFollowerID)) + return GARRISON_ERROR_FOLLOWER_ALREADY_RECRUITED; + + // Add the follower (this uses the normal AddFollower which handles DB, packet, etc.) + AddFollower(garrFollowerID); + + // Clear recruits after one is chosen + _availableRecruits.clear(); + + return GARRISON_SUCCESS; +} + +void Garrison::HealAllFollowers() +{ + for (auto& p : _followers) + { + p.second.PacketInfo.Health = static_cast(p.second.PacketInfo.Durability); + p.second.PacketInfo.FollowerStatus &= ~FOLLOWER_STATUS_EXHAUSTED; + } +} + +void Garrison::SendAllFollowerUpdates() +{ + for (auto const& p : _followers) + { + WorldPackets::Garrison::GarrisonUpdateFollower updateFollower; + updateFollower.Result = GARRISON_SUCCESS; + updateFollower.Follower = p.second.PacketInfo; + _owner->SendDirectMessage(updateFollower.Write()); + } +} + +void Garrison::FinishMission(uint32 garrMissionRecID) +{ + Mission* mission = GetMissionByRecID(garrMissionRecID); + if (!mission) + return; + + // Only finish missions that are in progress + if (mission->PacketInfo.MissionState != 1) + return; + + mission->PacketInfo.MissionState = 2; // Completed + mission->PacketInfo.SuccessChance = 100; // Instant complete = guaranteed success +} + +void Garrison::FinishShipment(uint32 plotInstanceId) +{ + // Find the oldest incomplete shipment for this plot and complete it + Shipment* oldest = nullptr; + for (auto& [dbId, shipment] : _shipments) + { + if (shipment.PlotInstanceID == plotInstanceId && !shipment.IsReady()) + { + if (!oldest || shipment.CreationTime < oldest->CreationTime) + oldest = &shipment; + } + } + + if (oldest) + oldest->CreationTime = time_t(0); // Set creation time to epoch so IsReady() returns true +} + +void Garrison::SetFollowerQuality(uint64 dbId, uint32 quality) +{ + Follower* follower = GetFollower(dbId); + if (!follower) + return; + + // Capture old state before quality modification + WorldPackets::Garrison::GarrisonFollower oldFollowerState = follower->PacketInfo; + + follower->PacketInfo.Quality = quality; + + // Re-roll abilities for the new quality tier + GarrFollowerEntry const* followerEntry = sGarrFollowerStore.LookupEntry(follower->PacketInfo.GarrFollowerID); + if (followerEntry) + { + follower->PacketInfo.AbilityID = sGarrisonMgr.RollFollowerAbilities( + follower->PacketInfo.GarrFollowerID, followerEntry, + quality, GetFaction(), false); + } + + WorldPackets::Garrison::GarrisonFollowerChangedQuality changedQuality; + changedQuality.OldFollower = oldFollowerState; + changedQuality.Follower = follower->PacketInfo; + _owner->SendDirectMessage(changedQuality.Write()); +} + +void Garrison::SetFollowerLevel(uint64 dbId, uint32 level) +{ + Follower* follower = GetFollower(dbId); + if (!follower) + return; + + follower->PacketInfo.FollowerLevel = level; + follower->PacketInfo.Xp = 0; + + WorldPackets::Garrison::GarrisonUpdateFollower updateFollower; + updateFollower.Result = GARRISON_SUCCESS; + updateFollower.Follower = follower->PacketInfo; + _owner->SendDirectMessage(updateFollower.Write()); +} + +void Garrison::AddFollowerXP(uint64 dbId, uint32 xp) +{ + Follower* follower = GetFollower(dbId); + if (!follower) + return; + + if (follower->PacketInfo.FollowerStatus & FOLLOWER_STATUS_NO_XP_GAIN) + return; + + if (follower->PacketInfo.FollowerStatus & FOLLOWER_STATUS_TROOP) + return; + + // Capture old state before XP modification + WorldPackets::Garrison::GarrisonFollower oldFollowerState = follower->PacketInfo; + + GarrFollowerEntry const* followerEntry = sGarrFollowerStore.LookupEntry(follower->PacketInfo.GarrFollowerID); + uint8 followerTypeID = followerEntry ? followerEntry->GarrFollowerTypeID : FOLLOWER_TYPE_GARRISON; + + follower->PacketInfo.Xp += xp; + + // Level progression using GarrFollowerLevelXP DB2 + GarrFollowerLevelXPEntry const* levelXP = sGarrisonMgr.GetFollowerLevelXP(followerTypeID, follower->PacketInfo.FollowerLevel); + while (levelXP && levelXP->XpToNextLevel > 0 && follower->PacketInfo.Xp >= levelXP->XpToNextLevel) + { + follower->PacketInfo.Xp -= levelXP->XpToNextLevel; + follower->PacketInfo.FollowerLevel++; + levelXP = sGarrisonMgr.GetFollowerLevelXP(followerTypeID, follower->PacketInfo.FollowerLevel); + } + + // At max level, excess XP converts to quality progression + if (!levelXP || levelXP->XpToNextLevel == 0) + { + GarrFollowerQualityEntry const* qualityEntry = sGarrisonMgr.GetFollowerQuality(followerTypeID, follower->PacketInfo.Quality); + while (qualityEntry && qualityEntry->XpThreshold > 0 && follower->PacketInfo.Xp >= static_cast(qualityEntry->XpThreshold)) + { + follower->PacketInfo.Xp -= qualityEntry->XpThreshold; + follower->PacketInfo.Quality++; + + if (followerEntry) + { + follower->PacketInfo.AbilityID = sGarrisonMgr.RollFollowerAbilities( + follower->PacketInfo.GarrFollowerID, followerEntry, + follower->PacketInfo.Quality, GetFaction(), false); + } + + qualityEntry = sGarrisonMgr.GetFollowerQuality(followerTypeID, follower->PacketInfo.Quality); + } + + if (!qualityEntry || qualityEntry->XpThreshold == 0) + follower->PacketInfo.Xp = 0; + } + + WorldPackets::Garrison::GarrisonFollowerChangedXP followerXPUpdate; + followerXPUpdate.Result = GARRISON_SUCCESS; + followerXPUpdate.TotalXp = xp; + followerXPUpdate.OldFollower = oldFollowerState; + followerXPUpdate.Follower = follower->PacketInfo; + _owner->SendDirectMessage(followerXPUpdate.Write()); +} + +void Garrison::LearnFollowerAbility(uint64 dbId, uint32 abilityId) +{ + Follower* follower = GetFollower(dbId); + if (!follower) + return; + + GarrAbilityEntry const* ability = sGarrAbilityStore.LookupEntry(abilityId); + if (!ability) + return; + + // Check if follower already has this ability + if (follower->HasAbility(abilityId)) + return; + + follower->PacketInfo.AbilityID.push_back(ability); + + WorldPackets::Garrison::GarrisonUpdateFollower updateFollower; + updateFollower.Result = GARRISON_SUCCESS; + updateFollower.Follower = follower->PacketInfo; + _owner->SendDirectMessage(updateFollower.Write()); +} + +void Garrison::RandomizeFollowerAbilities(uint64 dbId) +{ + Follower* follower = GetFollower(dbId); + if (!follower) + return; + + GarrFollowerEntry const* followerEntry = sGarrFollowerStore.LookupEntry(follower->PacketInfo.GarrFollowerID); + if (!followerEntry) + return; + + follower->PacketInfo.AbilityID = sGarrisonMgr.RollFollowerAbilities( + follower->PacketInfo.GarrFollowerID, followerEntry, + follower->PacketInfo.Quality, GetFaction(), false); + + WorldPackets::Garrison::GarrisonUpdateFollower updateFollower; + updateFollower.Result = GARRISON_SUCCESS; + updateFollower.Follower = follower->PacketInfo; + _owner->SendDirectMessage(updateFollower.Write()); +} + +void Garrison::EndBuildingConstruction(uint32 garrPlotInstanceId) +{ + Plot* plot = GetPlot(garrPlotInstanceId); + if (!plot || !plot->BuildingInfo.PacketInfo) + return; + + if (plot->BuildingInfo.PacketInfo->Active) + return; + + // Set time built to the past so CanActivate() returns true + plot->BuildingInfo.PacketInfo->TimeBuilt = GameTime::GetGameTime() - DAY; + ActivateBuilding(garrPlotInstanceId); +} + +void Garrison::SetGarrisonCacheSize(uint32 size) +{ + _garrisonCacheSize = size; +} + +Garrison::Follower* Garrison::GetFollowerByGarrFollowerID(uint32 garrFollowerID) +{ + for (auto& [dbId, follower] : _followers) + if (follower.PacketInfo.GarrFollowerID == garrFollowerID) + return &follower; + return nullptr; +} + +GarrisonError Garrison::UpgradeFollowerItemLevel(uint64 dbId, int32 amount, int32 slot, GarrItemLevelUpgradeDataEntry const* upgradeData) +{ + Follower* follower = GetFollower(dbId); + if (!follower) + return GARRISON_ERROR_INVALID_FOLLOWER; + + GarrFollowerEntry const* followerEntry = sGarrFollowerStore.LookupEntry(follower->PacketInfo.GarrFollowerID); + if (!followerEntry) + return GARRISON_ERROR_INVALID_FOLLOWER; + + // Capture old state before item level modification + WorldPackets::Garrison::GarrisonFollower oldFollowerState = follower->PacketInfo; + + // Get item level cap from GarrFollowerType + uint16 maxItemLevel = 0; + GarrFollowerTypeEntry const* followerType = sGarrisonMgr.GetFollowerTypeForGarrType(followerEntry->GarrTypeID); + if (followerType) + maxItemLevel = followerType->MaxItemLevel; + + int32 minILevel = 0; + int32 maxILevel = maxItemLevel > 0 ? maxItemLevel : 675; // WoD default cap + + if (upgradeData) + { + minILevel = upgradeData->MinItemLevel; + if (upgradeData->MaxItemLevel > 0) + maxILevel = std::min(maxILevel, upgradeData->MaxItemLevel); + } + + auto applyUpgrade = [&](uint32& currentLevel) + { + if (upgradeData && upgradeData->Operation == 0) + { + // Operation 0 = set to specific value + currentLevel = static_cast(std::clamp(amount, minILevel, maxILevel)); + } + else + { + // Operation 1 or default = add delta + if (static_cast(currentLevel) < minILevel) + return; // Below minimum, item doesn't apply + int32 newLevel = static_cast(currentLevel) + amount; + currentLevel = static_cast(std::clamp(newLevel, 0, maxILevel)); + } + }; + + if (slot == 0 || slot < 0 || slot > 1) + applyUpgrade(follower->PacketInfo.ItemLevelWeapon); + if (slot == 1 || slot < 0 || slot > 1) + applyUpgrade(follower->PacketInfo.ItemLevelArmor); + + // Send item level change with old and new state + WorldPackets::Garrison::GarrisonFollowerChangedItemLevel changedItemLevel; + changedItemLevel.Result = GARRISON_SUCCESS; + changedItemLevel.OldFollower = oldFollowerState; + changedItemLevel.Follower = follower->PacketInfo; + _owner->SendDirectMessage(changedItemLevel.Write()); + + return GARRISON_SUCCESS; +} + Map* Garrison::FindMap() const { return sMapMgr->FindMap(_siteLevel->MapID, _owner->GetGUID().GetCounter()); @@ -831,3 +3046,496 @@ bool Garrison::Follower::HasAbility(uint32 garrAbilityId) const { return advstd::ranges::contains(PacketInfo.AbilityID, garrAbilityId, &GarrAbilityEntry::ID); } + +// ============================================================ +// Shipment sub-struct +// ============================================================ + +bool Garrison::Shipment::IsReady() const +{ + return CreationTime + Duration <= GameTime::GetGameTime(); +} + +// ============================================================ +// Shipment management +// ============================================================ + +GarrisonError Garrison::CreateShipment(ObjectGuid npcGUID, uint32 count) +{ + uint32 plotInstanceId = FindPlotInstanceForNpc(npcGUID); + if (!plotInstanceId) + return GARRISON_ERROR_INVALID_PLOT_INSTANCEID; + + Plot* plot = GetPlot(plotInstanceId); + if (!plot || !plot->BuildingInfo.PacketInfo || !plot->BuildingInfo.PacketInfo->Active) + return GARRISON_ERROR_BUILDING_NOT_ACTIVE; + + GarrBuildingEntry const* building = sGarrBuildingStore.LookupEntry(plot->BuildingInfo.PacketInfo->GarrBuildingID); + if (!building) + return GARRISON_ERROR_NO_BUILDING; + + CharShipmentContainerEntry const* container = sGarrisonMgr.GetShipmentContainerForBuilding(building->BuildingType); + if (!container) + return GARRISON_ERROR_INTERNAL_ERROR; + + std::vector const* shipmentEntries = sGarrisonMgr.GetShipmentsForContainer(container->ID); + if (!shipmentEntries || shipmentEntries->empty()) + return GARRISON_ERROR_INTERNAL_ERROR; + + // Use first shipment for the container + CharShipmentEntry const* shipmentEntry = shipmentEntries->front(); + + // Count existing shipments for this plot + uint32 existingCount = 0; + for (auto const& p : _shipments) + if (p.second.PlotInstanceID == plotInstanceId) + ++existingCount; + + uint32 maxShipments = container->BaseCapacity; + if (shipmentEntry->MaxShipments > 0) + maxShipments = std::min(maxShipments, static_cast(shipmentEntry->MaxShipments)); + + for (uint32 i = 0; i < count; ++i) + { + if (existingCount >= maxShipments) + break; + + uint64 dbId = sGarrisonMgr.GenerateShipmentDbId(); + Shipment& shipment = _shipments[dbId]; + shipment.DbID = dbId; + shipment.ShipmentRecID = shipmentEntry->ID; + shipment.PlotInstanceID = plotInstanceId; + shipment.CreationTime = GameTime::GetGameTime(); + shipment.Duration = shipmentEntry->Duration; + shipment.AssignedFollowerDBID = 0; + + // Check if a follower is assigned to this building + for (auto const& followerPair : _followers) + { + if (followerPair.second.PacketInfo.CurrentBuildingID == plot->BuildingInfo.PacketInfo->GarrBuildingID) + { + shipment.AssignedFollowerDBID = followerPair.second.PacketInfo.DbID; + break; + } + } + + ++existingCount; + + WorldPackets::Garrison::CreateShipmentResponse response; + response.ShipmentID = shipment.DbID; + response.ShipmentRecID = 0; // Sniff confirms Blizzard sends 0 here + response.Result = GARRISON_SUCCESS; + _owner->SendDirectMessage(response.Write()); + } + + return GARRISON_SUCCESS; +} + +void Garrison::CompleteShipment(uint64 dbId) +{ + auto itr = _shipments.find(dbId); + if (itr == _shipments.end()) + return; + + Shipment& shipment = itr->second; + + // Cast completion spell if defined + CharShipmentEntry const* shipmentEntry = sCharShipmentStore.LookupEntry(shipment.ShipmentRecID); + if (shipmentEntry && shipmentEntry->OnCompleteSpellID) + _owner->CastSpell(_owner, shipmentEntry->OnCompleteSpellID, true); + + WorldPackets::Garrison::CompleteShipmentResponse response; + response.ShipmentID = dbId; + response.Result = GARRISON_SUCCESS; + _owner->SendDirectMessage(response.Write()); + + _shipments.erase(itr); +} + +void Garrison::CompleteReadyShipments() +{ + std::vector readyShipments; + for (auto const& p : _shipments) + if (p.second.IsReady()) + readyShipments.push_back(p.first); + + for (uint64 dbId : readyShipments) + CompleteShipment(dbId); +} + +std::vector Garrison::GetShipmentsForPlot(uint32 plotInstanceId) const +{ + std::vector result; + for (auto const& p : _shipments) + if (p.second.PlotInstanceID == plotInstanceId) + result.push_back(&p.second); + + return result; +} + +std::vector Garrison::GetAllShipments() const +{ + std::vector result; + result.reserve(_shipments.size()); + for (auto const& p : _shipments) + result.push_back(&p.second); + + return result; +} + +uint32 Garrison::GetBuildingTypeForPlot(uint32 plotInstanceId) const +{ + Plot const* plot = GetPlot(plotInstanceId); + if (!plot || !plot->BuildingInfo.PacketInfo) + return 0; + + GarrBuildingEntry const* building = sGarrBuildingStore.LookupEntry(plot->BuildingInfo.PacketInfo->GarrBuildingID); + if (!building) + return 0; + + return building->BuildingType; +} + +uint32 Garrison::FindPlotInstanceForNpc(ObjectGuid npcGUID) const +{ + // Find which plot this NPC belongs to by checking building spawns + for (auto const& p : _plots) + { + Plot const& plot = p.second; + if (plot.BuildingInfo.Spawns.count(npcGUID) > 0) + return p.first; + } + + return 0; +} + +void Garrison::SendShipmentInfo(ObjectGuid npcGUID) +{ + WorldPackets::Garrison::GetShipmentInfoResponse response; + + uint32 plotInstanceId = FindPlotInstanceForNpc(npcGUID); + if (!plotInstanceId) + { + _owner->SendDirectMessage(response.Write()); + return; + } + + Plot* plot = GetPlot(plotInstanceId); + if (!plot || !plot->BuildingInfo.PacketInfo || !plot->BuildingInfo.PacketInfo->Active) + { + _owner->SendDirectMessage(response.Write()); + return; + } + + GarrBuildingEntry const* building = sGarrBuildingStore.LookupEntry(plot->BuildingInfo.PacketInfo->GarrBuildingID); + if (!building) + { + _owner->SendDirectMessage(response.Write()); + return; + } + + CharShipmentContainerEntry const* container = sGarrisonMgr.GetShipmentContainerForBuilding(building->BuildingType); + if (!container) + { + _owner->SendDirectMessage(response.Write()); + return; + } + + std::vector const* shipmentEntries = sGarrisonMgr.GetShipmentsForContainer(container->ID); + if (!shipmentEntries || shipmentEntries->empty()) + { + _owner->SendDirectMessage(response.Write()); + return; + } + + response.Success = true; + response.ShipmentID = container->ID; + response.MaxShipments = container->BaseCapacity; + response.PlotInstanceID = plotInstanceId; + + std::vector plotShipments = GetShipmentsForPlot(plotInstanceId); + response.Shipments.reserve(plotShipments.size()); + for (Shipment const* shipment : plotShipments) + { + WorldPackets::Garrison::CharacterShipment& packetShipment = response.Shipments.emplace_back(); + packetShipment.ShipmentRecID = shipment->ShipmentRecID; + packetShipment.ShipmentID = shipment->DbID; + packetShipment.AssignedFollowerDBID = shipment->AssignedFollowerDBID; + packetShipment.CreationTime = shipment->CreationTime; + packetShipment.ShipmentDuration = shipment->Duration; + packetShipment.BuildingTypeID = building->BuildingType; + packetShipment.GarrTypeID = static_cast(GetType()); + } + + _owner->SendDirectMessage(response.Write()); +} + +void Garrison::SendLandingPageShipments() +{ + WorldPackets::Garrison::GetLandingPageShipmentsResponse response; + response.GarrTypeID = static_cast(GetType()); + + for (auto const& p : _shipments) + { + Shipment const& shipment = p.second; + WorldPackets::Garrison::CharacterShipment& packetShipment = response.Shipments.emplace_back(); + packetShipment.ShipmentRecID = shipment.ShipmentRecID; + packetShipment.ShipmentID = shipment.DbID; + packetShipment.AssignedFollowerDBID = shipment.AssignedFollowerDBID; + packetShipment.CreationTime = shipment.CreationTime; + packetShipment.ShipmentDuration = shipment.Duration; + packetShipment.BuildingTypeID = GetBuildingTypeForPlot(shipment.PlotInstanceID); + packetShipment.GarrTypeID = static_cast(GetType()); + } + + _owner->SendDirectMessage(response.Write()); +} + +// ============================================================ +// Talent helpers +// ============================================================ + +bool Garrison::Talent::IsResearching() const +{ + return ResearchStartTime != 0; +} + +bool Garrison::Talent::IsResearchComplete() const +{ + if (!IsResearching()) + return false; + + std::vector const* ranks = sGarrisonMgr.GetTalentRanksForTalent(GarrTalentID); + if (!ranks || Rank >= static_cast(ranks->size())) + return false; + + GarrTalentRankEntry const* rankEntry = (*ranks)[Rank]; + return GameTime::GetGameTime() >= ResearchStartTime + rankEntry->ResearchDurationSecs; +} + +// ============================================================ +// Talent management +// ============================================================ + +Garrison::Talent const* Garrison::GetTalent(uint32 garrTalentID) const +{ + auto itr = _talents.find(garrTalentID); + if (itr != _talents.end()) + return &itr->second; + + return nullptr; +} + +void Garrison::CompleteAllTalentResearch() +{ + for (auto& [talentId, talent] : _talents) + { + if (!talent.IsResearching()) + continue; + + if (!talent.IsResearchComplete()) + continue; + + talent.Rank++; + talent.ResearchStartTime = 0; + + TC_LOG_DEBUG("garrison", "Garrison::CompleteAllTalentResearch: Player {} talent {} completed research to rank {}", + _owner->GetGUID().ToString().c_str(), talentId, talent.Rank); + } +} + +uint32 Garrison::LearnTalent(uint32 garrTalentID, bool isTemporary) +{ + GarrTalentEntry const* talentEntry = sGarrTalentStore.LookupEntry(garrTalentID); + if (!talentEntry) + return GARRISON_ERROR_INVALID_TALENT; + + // Verify the talent tree belongs to this garrison type + GarrTalentTreeEntry const* treeEntry = sGarrTalentTreeStore.LookupEntry(talentEntry->GarrTalentTreeID); + if (!treeEntry || treeEntry->GarrTypeID != static_cast(GetType())) + return GARRISON_ERROR_INVALID_TALENT; + + // Check if already learned + auto itr = _talents.find(garrTalentID); + if (itr != _talents.end()) + return GARRISON_ERROR_INVALID_TALENT; + + // Check prerequisite talent + if (talentEntry->PrerequisiteTalentID) + { + auto prereqItr = _talents.find(talentEntry->PrerequisiteTalentID); + if (prereqItr == _talents.end() || prereqItr->second.Rank < 1) + return GARRISON_ERROR_INVALID_TALENT; + } + + // Learn the talent at rank 0 (researching to rank 1 happens via ResearchTalent) + Talent& talent = _talents[garrTalentID]; + talent.GarrTalentID = garrTalentID; + talent.Rank = 0; + talent.ResearchStartTime = 0; + talent.Flags = isTemporary ? GARRISON_TALENT_FLAG_TEMPORARY : GARRISON_TALENT_FLAG_NONE; + talent.SoulbindConduitID = 0; + talent.SoulbindConduitRank = 0; + + // Send result + WorldPackets::Garrison::GarrisonResearchTalentResult result; + result.Result = GARRISON_SUCCESS; + result.GarrTypeID = static_cast(GetType()); + result.Talent.GarrTalentID = talent.GarrTalentID; + result.Talent.Rank = talent.Rank; + result.Talent.ResearchStartTime = time_t(talent.ResearchStartTime); + result.Talent.Flags = talent.Flags; + _owner->SendDirectMessage(result.Write()); + + return GARRISON_SUCCESS; +} + +uint32 Garrison::ResearchTalent(uint32 garrTalentID) +{ + GarrTalentEntry const* talentEntry = sGarrTalentStore.LookupEntry(garrTalentID); + if (!talentEntry) + return GARRISON_ERROR_INVALID_TALENT; + + // Verify the talent tree belongs to this garrison type + GarrTalentTreeEntry const* treeEntry = sGarrTalentTreeStore.LookupEntry(talentEntry->GarrTalentTreeID); + if (!treeEntry || treeEntry->GarrTypeID != static_cast(GetType())) + return GARRISON_ERROR_INVALID_TALENT; + + // Must already be learned (or learn it now if not) + auto itr = _talents.find(garrTalentID); + if (itr == _talents.end()) + { + // Auto-learn if not yet learned + Talent& talent = _talents[garrTalentID]; + talent.GarrTalentID = garrTalentID; + talent.Rank = 0; + talent.ResearchStartTime = 0; + talent.Flags = 0; + talent.SoulbindConduitID = 0; + talent.SoulbindConduitRank = 0; + itr = _talents.find(garrTalentID); + } + + Talent& talent = itr->second; + + // Check if already researching any talent in this tree + for (auto const& [id, t] : _talents) + { + if (id == garrTalentID) + continue; + + if (t.IsResearching()) + { + GarrTalentEntry const* otherTalent = sGarrTalentStore.LookupEntry(id); + if (otherTalent && otherTalent->GarrTalentTreeID == talentEntry->GarrTalentTreeID) + return GARRISON_ERROR_ALREADY_RESEARCHING_TALENT; + } + } + + // Check if this talent is already researching + if (talent.IsResearching()) + return GARRISON_ERROR_ALREADY_RESEARCHING_TALENT; + + // Get the rank entry for the next rank + std::vector const* ranks = sGarrisonMgr.GetTalentRanksForTalent(garrTalentID); + if (!ranks || talent.Rank >= static_cast(ranks->size())) + return GARRISON_ERROR_INVALID_TALENT; + + GarrTalentRankEntry const* rankEntry = (*ranks)[talent.Rank]; + + // Deduct currency cost + if (rankEntry->ResearchCostCurrencyTypesID && rankEntry->ResearchCost > 0) + { + if (!_owner->HasCurrency(rankEntry->ResearchCostCurrencyTypesID, rankEntry->ResearchCost)) + return GARRISON_ERROR_NOT_ENOUGH_CURRENCY; + + _owner->RemoveCurrency(rankEntry->ResearchCostCurrencyTypesID, rankEntry->ResearchCost, CurrencyDestroyReason::Garrison); + } + + // Deduct gold cost + if (rankEntry->ResearchGoldCost > 0) + { + if (!_owner->HasEnoughMoney(uint64(rankEntry->ResearchGoldCost) * GOLD)) + return GARRISON_ERROR_NOT_ENOUGH_GOLD; + + _owner->ModifyMoney(-int64(uint64(rankEntry->ResearchGoldCost) * GOLD)); + } + + // Start research + talent.ResearchStartTime = GameTime::GetGameTime(); + + // If research is instant (duration 0), complete immediately + if (rankEntry->ResearchDurationSecs <= 0) + { + talent.Rank++; + talent.ResearchStartTime = 0; + } + + // Send result + WorldPackets::Garrison::GarrisonResearchTalentResult result; + result.Result = GARRISON_SUCCESS; + result.GarrTypeID = static_cast(GetType()); + result.Talent.GarrTalentID = talent.GarrTalentID; + result.Talent.Rank = talent.Rank; + result.Talent.ResearchStartTime = time_t(talent.ResearchStartTime); + result.Talent.Flags = talent.Flags; + if (talent.SoulbindConduitID != 0) + { + WorldPackets::Garrison::GarrisonTalentSocketData socket; + socket.SoulbindConduitID = talent.SoulbindConduitID; + socket.SoulbindConduitRank = talent.SoulbindConduitRank; + result.Talent.Socket = socket; + } + _owner->SendDirectMessage(result.Write()); + + return GARRISON_SUCCESS; +} + +uint32 Garrison::SocketTalent(uint32 garrTalentID, int32 soulbindConduitID, int32 soulbindConduitRank) +{ + GarrTalentEntry const* talentEntry = sGarrTalentStore.LookupEntry(garrTalentID); + if (!talentEntry) + return GARRISON_ERROR_INVALID_TALENT; + + // Must have socket properties + if (!talentEntry->GarrTalentSocketPropertiesID) + return GARRISON_ERROR_INVALID_TALENT; + + auto itr = _talents.find(garrTalentID); + if (itr == _talents.end()) + return GARRISON_ERROR_INVALID_TALENT; + + Talent& talent = itr->second; + talent.SoulbindConduitID = soulbindConduitID; + talent.SoulbindConduitRank = soulbindConduitRank; + + // Send result + WorldPackets::Garrison::GarrisonResearchTalentResult result; + result.Result = GARRISON_SUCCESS; + result.GarrTypeID = static_cast(GetType()); + result.Talent.GarrTalentID = talent.GarrTalentID; + result.Talent.Rank = talent.Rank; + result.Talent.ResearchStartTime = time_t(talent.ResearchStartTime); + result.Talent.Flags = talent.Flags; + WorldPackets::Garrison::GarrisonTalentSocketData socket; + socket.SoulbindConduitID = talent.SoulbindConduitID; + socket.SoulbindConduitRank = talent.SoulbindConduitRank; + result.Talent.Socket = socket; + _owner->SendDirectMessage(result.Write()); + + return GARRISON_SUCCESS; +} + +// ============================================================ +// Trophy system +// ============================================================ + +void Garrison::AddTrophy(uint32 trophyID) +{ + _trophies.insert(trophyID); +} + +void Garrison::RemoveTrophy(uint32 trophyID) +{ + _trophies.erase(trophyID); +} diff --git a/src/server/game/Garrison/Garrison.h b/src/server/game/Garrison/Garrison.h index 65c08e5ab..f363f0fa0 100644 --- a/src/server/game/Garrison/Garrison.h +++ b/src/server/game/Garrison/Garrison.h @@ -29,9 +29,12 @@ class GameObject; class Map; class Player; +struct GarrBuildingEntry; +struct GarrItemLevelUpgradeDataEntry; +struct GarrMissionEntry; struct GarrSiteLevelEntry; -enum GarrisonType +enum GarrisonType : int32 { GARRISON_TYPE_GARRISON = 2, GARRISON_TYPE_CLASS_ORDER = 3, @@ -60,7 +63,7 @@ enum GarrisonFollowerType FOLLOWER_TYPE_GARRISON = 1, FOLLOWER_TYPE_SHIPYARD = 2, FOLLOWER_TYPE_CLASS_ORDER = 4, - FOLLOWER_TYPE_WAR_CAMPAIGN = 11, + FOLLOWER_TYPE_WAR_CAMPAIGN = 22, FOLLOWER_TYPE_COVENANT = 123 }; @@ -215,10 +218,52 @@ public: WorldPackets::Garrison::GarrisonFollower PacketInfo; }; + struct Mission + { + WorldPackets::Garrison::GarrisonMission PacketInfo; + std::vector CurrentFollowerDBIDs; + }; + + struct Shipment + { + uint64 DbID = 0; + uint32 ShipmentRecID = 0; + uint32 PlotInstanceID = 0; + time_t CreationTime = 0; + int32 Duration = 0; + uint64 AssignedFollowerDBID = 0; + + bool IsReady() const; + }; + + // Bit values for Garrison::Talent::Flags. Exact wire bits unverified by sniff; + // the Temporary bit is server-only and routed back into the wire Flags value. + enum GarrisonTalentFlags : int32 + { + GARRISON_TALENT_FLAG_NONE = 0x0, + GARRISON_TALENT_FLAG_TEMPORARY = 0x1, // Legion class-hall one-shot/temporary talent + }; + + struct Talent + { + uint32 GarrTalentID = 0; + int32 Rank = 0; + time_t ResearchStartTime = 0; + int32 Flags = 0; + int32 SoulbindConduitID = 0; + int32 SoulbindConduitRank = 0; + + bool IsResearching() const; + bool IsResearchComplete() const; + bool IsTemporary() const { return (Flags & GARRISON_TALENT_FLAG_TEMPORARY) != 0; } + }; + explicit Garrison(Player* owner); bool LoadFromDB(PreparedQueryResult garrison, PreparedQueryResult blueprints, PreparedQueryResult buildings, - PreparedQueryResult followers, PreparedQueryResult abilities); + PreparedQueryResult followers, PreparedQueryResult abilities, PreparedQueryResult missions, + PreparedQueryResult specializations, PreparedQueryResult shipments, PreparedQueryResult talents, + PreparedQueryResult trophies, PreparedQueryResult archivedMissions); void SaveToDB(CharacterDatabaseTransaction trans); static void DeleteFromDB(ObjectGuid::LowType ownerGuid, CharacterDatabaseTransaction trans); @@ -226,12 +271,13 @@ public: void Delete(); void Upgrade(); + void Update(uint32 diff); void Enter() const; void Leave() const; static constexpr GarrisonFactionIndex GetFaction(Team team) { return team == HORDE ? GARRISON_FACTION_INDEX_HORDE : GARRISON_FACTION_INDEX_ALLIANCE; } GarrisonFactionIndex GetFaction() const; - GarrisonType GetType() const { return GARRISON_TYPE_GARRISON; } + GarrisonType GetType() const { return _garrType; } GarrSiteLevelEntry const* GetSiteLevel() const { return _siteLevel; } // Plots @@ -246,10 +292,26 @@ public: void PlaceBuilding(uint32 garrPlotInstanceId, uint32 garrBuildingId); void CancelBuildingConstruction(uint32 garrPlotInstanceId); void ActivateBuilding(uint32 garrPlotInstanceId); + void SwapBuildings(uint32 plotId1, uint32 plotId2); + + // Specializations + void LearnSpecialization(uint32 garrSpecId); + bool HasSpecialization(uint32 garrSpecId) const { return _knownSpecializations.find(garrSpecId) != _knownSpecializations.end(); } + void SetBuildingSpecialization(uint32 garrPlotInstanceId, uint32 garrSpecId); // Followers void AddFollower(uint32 garrFollowerId); + void AddTroop(uint32 garrFollowerId, uint32 durability); Follower const* GetFollower(uint64 dbId) const; + Follower* GetFollower(uint64 dbId); + Follower const* GetFollowerByEntry(uint32 garrFollowerId) const; + void RemoveFollower(uint64 dbId); + void SetFollowerFavorite(uint64 dbId, bool favorite); + void SetFollowerInactive(uint64 dbId, bool inactive); + void RenameFollower(uint64 dbId, std::string const& name); + void AssignFollowerToBuilding(uint64 dbId, uint32 plotInstanceId); + void RemoveFollowerFromBuilding(uint64 dbId); + std::unordered_map const& GetFollowerMap() const { return _followers; } template uint32 CountFollowers(Predicate&& predicate) const { @@ -261,10 +323,84 @@ public: return count; } + // Missions + void AddMission(uint32 garrMissionId); + Mission const* GetMission(uint64 dbId) const; + Mission* GetMission(uint64 dbId); + Mission const* GetMissionByRecID(uint32 missionRecID) const; + Mission* GetMissionByRecID(uint32 missionRecID); + GarrisonError StartMission(uint32 missionRecID, std::vector const& followerDBIDs); + GarrisonError CompleteMission(uint32 missionRecID); + GarrisonError ClaimMissionReward(uint32 missionRecID); + GarrisonError MissionBonusRoll(uint32 missionRecID); + void RemoveMission(uint32 missionRecID); + void GenerateAvailableMissions(); + uint64 GenerateMissionDbId(); + int32 CalculateSuccessChance(uint32 missionRecID, std::vector const& followerDBIDs) const; + void PopulateMissionData(Mission& mission, GarrMissionEntry const* missionEntry) const; + bool IsAutoCombatMission(Mission const& mission) const; + void RemoveExpiredMissions(); + uint32 GetAndIncrementSessionMissionCount() { return _sessionMissionCount++; } + void SendDeleteExpiredMissionsResult() const; + + // Recruitment + void SetRecruitmentPreferences(uint32 abilityId, uint32 traitId); + void GenerateRecruits(uint32 faction); + GarrisonError RecruitFollower(uint32 garrFollowerID); + std::vector const& GetAvailableRecruits() const { return _availableRecruits; } + + // Follower healing + void HealAllFollowers(); + void SendAllFollowerUpdates(); + + // Spell-driven operations + void FinishMission(uint32 garrMissionRecID); + void FinishShipment(uint32 plotInstanceId); + void SetFollowerQuality(uint64 dbId, uint32 quality); + void SetFollowerLevel(uint64 dbId, uint32 level); + void AddFollowerXP(uint64 dbId, uint32 xp); + void LearnFollowerAbility(uint64 dbId, uint32 abilityId); + void RandomizeFollowerAbilities(uint64 dbId); + void EndBuildingConstruction(uint32 garrPlotInstanceId); + void SetGarrisonCacheSize(uint32 size); + Follower* GetFollowerByGarrFollowerID(uint32 garrFollowerID); + GarrisonError UpgradeFollowerItemLevel(uint64 dbId, int32 amount, int32 slot, GarrItemLevelUpgradeDataEntry const* upgradeData = nullptr); + + // Building-specific helpers + GarrBuildingEntry const* GetActiveBuildingByType(uint32 buildingType) const; + uint32 GetBonusFollowerSlots() const; + + // Shipments (work orders) + GarrisonError CreateShipment(ObjectGuid npcGUID, uint32 count); + void CompleteShipment(uint64 dbId); + void CompleteReadyShipments(); + std::vector GetShipmentsForPlot(uint32 plotInstanceId) const; + std::vector GetAllShipments() const; + void SendShipmentInfo(ObjectGuid npcGUID); + void SendLandingPageShipments(); + uint32 GetBuildingTypeForPlot(uint32 plotInstanceId) const; + uint32 FindPlotInstanceForNpc(ObjectGuid npcGUID) const; + + // Talent system + uint32 LearnTalent(uint32 garrTalentID, bool isTemporary); + uint32 ResearchTalent(uint32 garrTalentID); + uint32 SocketTalent(uint32 garrTalentID, int32 soulbindConduitID, int32 soulbindConduitRank); + Talent const* GetTalent(uint32 garrTalentID) const; + std::unordered_map const& GetAllTalents() const { return _talents; } + void CompleteAllTalentResearch(); + + // Trophy system + void AddTrophy(uint32 trophyID); + void RemoveTrophy(uint32 trophyID); + bool HasTrophy(uint32 trophyID) const { return _trophies.count(trophyID) > 0; } + std::unordered_set const& GetTrophies() const { return _trophies; } + void BuildInfoPacket(WorldPackets::Garrison::GarrisonInfo& garrison) const; void SendRemoteInfo() const; void SendBlueprintAndSpecializationData(); void SendMapData(Player* receiver) const; + void SendMissionStartConditionUpdate() const; + void SendTroopQualityRefresh() const; void ResetFollowerActivationLimit() { _followerActivationsRemainingToday = 1; } @@ -274,13 +410,41 @@ private: GarrisonError CheckBuildingPlacement(uint32 garrPlotInstanceId, uint32 garrBuildingId) const; GarrisonError CheckBuildingRemoval(uint32 garrPlotInstanceId) const; Player* _owner; + GarrisonType _garrType; GarrSiteLevelEntry const* _siteLevel; uint32 _followerActivationsRemainingToday; + uint32 _updateTimer = 0; + uint32 _garrisonCacheSize = 500; + static constexpr uint32 GARRISON_UPDATE_INTERVAL = 60000; // 60 seconds std::unordered_map _plots; std::unordered_set _knownBuildings; + std::unordered_set _knownSpecializations; std::unordered_map _followers; std::unordered_set _followerIds; + std::unordered_map _missions; + uint64 _missionDbIdGenerator = 1; + time_t _lastMissionGenerationTime = 0; + std::unordered_set _activeMissionRecIDs; + uint32 _sessionMissionCount = 0; + uint32 _missionsStartedToday = 0; + uint32 _lastMissionStartDay = 0; // days since epoch, for daily reset detection + std::vector _archivedMissions; + + // Recruitment + std::vector _availableRecruits; + uint32 _recruitmentPreferenceAbilityId = 0; + uint32 _recruitmentPreferenceTraitId = 0; + + // Shipments + std::unordered_map _shipments; + std::unordered_map _talents; + + // Trophies + std::unordered_set _trophies; + + // Temporary storage for BuildInfoPacket (mission copies with cleared inline rewards) + mutable std::vector _infoMissions; }; #endif // Garrison_h__ diff --git a/src/server/game/Garrison/GarrisonAutoCombat.cpp b/src/server/game/Garrison/GarrisonAutoCombat.cpp new file mode 100644 index 000000000..aaaf50ac9 --- /dev/null +++ b/src/server/game/Garrison/GarrisonAutoCombat.cpp @@ -0,0 +1,657 @@ +/* + * 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 . + */ + +#include "GarrisonAutoCombat.h" +#include "DB2Stores.h" +#include "GarrisonMgr.h" +#include "Log.h" +#include "Random.h" +#include + +int32 AutoCombatCombatant::GetEffectiveAttack() const +{ + int32 attack = BaseAttack; + for (auto const& mod : AttackModifiers) + attack += mod.Amount; + return std::max(attack, 0); +} + +void AutoCombatCombatant::TickPeriodicEffects(AutoCombatRound& round) +{ + for (auto it = PeriodicEffects.begin(); it != PeriodicEffects.end();) + { + if (it->RemainingTicks <= 0) + { + it = PeriodicEffects.erase(it); + continue; + } + + AutoCombatEvent event; + event.CasterBoardIndex = it->SourceBoardIndex; + event.TargetBoardIndex = BoardIndex; + event.SpellID = it->SpellID; + event.Amount = it->Amount; + + if (it->IsDamage) + { + event.EffectType = AUTO_COMBAT_EFFECT_DOT; + int32 dmg = it->Amount; + if (ShieldAmount > 0) + { + int32 absorbed = std::min(ShieldAmount, dmg); + ShieldAmount -= absorbed; + dmg -= absorbed; + } + CurrentHealth -= dmg; + if (CurrentHealth < 0) + CurrentHealth = 0; + } + else + { + event.EffectType = AUTO_COMBAT_EFFECT_HOT; + CurrentHealth = std::min(CurrentHealth + it->Amount, MaxHealth); + } + + round.Events.push_back(event); + --it->RemainingTicks; + + if (it->RemainingTicks <= 0) + it = PeriodicEffects.erase(it); + else + ++it; + } +} + +void AutoCombatCombatant::TickCooldowns() +{ + for (auto it = SpellCooldowns.begin(); it != SpellCooldowns.end();) + { + --it->second; + if (it->second <= 0) + it = SpellCooldowns.erase(it); + else + ++it; + } +} + +void AutoCombatCombatant::TickModifiers() +{ + for (auto it = AttackModifiers.begin(); it != AttackModifiers.end();) + { + --it->RemainingRounds; + if (it->RemainingRounds <= 0) + it = AttackModifiers.erase(it); + else + ++it; + } +} + +AutoCombatResult GarrisonAutoCombat::SimulateCombat( + std::vector& playerUnits, + std::vector& enemyUnits) +{ + AutoCombatResult result; + + if (playerUnits.empty() || enemyUnits.empty()) + { + result.PlayerWon = !playerUnits.empty(); + return result; + } + + // Sort all combatants by board index for turn order + std::vector turnOrder; + for (auto& unit : playerUnits) + turnOrder.push_back(&unit); + for (auto& unit : enemyUnits) + turnOrder.push_back(&unit); + + std::sort(turnOrder.begin(), turnOrder.end(), + [](AutoCombatCombatant const* a, AutoCombatCombatant const* b) + { + return a->BoardIndex < b->BoardIndex; + }); + + for (int32 roundNum = 1; roundNum <= MAX_ROUNDS; ++roundNum) + { + AutoCombatRound round; + round.RoundNumber = roundNum; + + // Process periodic effects at round start + for (auto* combatant : turnOrder) + { + if (combatant->IsAlive()) + combatant->TickPeriodicEffects(round); + } + + // Check for deaths after periodic effects + if (!IsTeamAlive(playerUnits) || !IsTeamAlive(enemyUnits)) + { + result.CombatLog.push_back(std::move(round)); + break; + } + + // Each combatant takes a turn in board index order + for (auto* combatant : turnOrder) + { + if (!combatant->IsAlive()) + continue; + + if (combatant->IsPlayerSide) + ProcessTurn(*combatant, playerUnits, enemyUnits, round); + else + ProcessTurn(*combatant, enemyUnits, playerUnits, round); + + // Check if either side is eliminated after each turn + if (!IsTeamAlive(playerUnits) || !IsTeamAlive(enemyUnits)) + break; + } + + // Tick cooldowns and buff durations at end of round + for (auto* combatant : turnOrder) + { + if (combatant->IsAlive()) + { + combatant->TickCooldowns(); + combatant->TickModifiers(); + } + } + + result.CombatLog.push_back(std::move(round)); + + if (!IsTeamAlive(playerUnits) || !IsTeamAlive(enemyUnits)) + break; + } + + result.PlayerWon = IsTeamAlive(playerUnits); + result.TotalRounds = static_cast(result.CombatLog.size()); + + return result; +} + +AutoCombatCombatant GarrisonAutoCombat::BuildFollowerCombatant( + uint32 followerLevel, uint32 quality, uint32 itemLevelWeapon, + uint32 itemLevelArmor, int8 boardIndex, uint64 followerDbID) +{ + AutoCombatCombatant combatant; + + // No DB2 publishes follower auto-combat stats directly (GarrAutoCombatant only + // covers enemies via GarrEncounterID). The formula below is an empirical scaling + // calibrated so that quality/level-equivalent followers fall within the 1000-3000 + // HP range typical of GarrAutoCombatant enemy entries; tune against sniffed + // adventure outcomes when available. + uint32 avgItemLevel = (itemLevelWeapon + itemLevelArmor) / 2; + + combatant.MaxHealth = 1000 + static_cast(followerLevel) * 100 + + static_cast(quality) * 200 + + static_cast(avgItemLevel) * 5; + combatant.CurrentHealth = combatant.MaxHealth; + combatant.BaseAttack = 50 + static_cast(followerLevel) * 10 + + static_cast(quality) * 20 + + static_cast(avgItemLevel) * 2; + combatant.BoardIndex = boardIndex; + combatant.IsPlayerSide = true; + combatant.FollowerDbID = followerDbID; + combatant.Role = AUTO_COMBAT_ROLE_DPS; + + return combatant; +} + +AutoCombatCombatant GarrisonAutoCombat::BuildEnemyCombatant( + GarrAutoCombatantEntry const* entry) +{ + AutoCombatCombatant combatant; + + combatant.AutoCombatantID = entry->ID; + combatant.CurrentHealth = entry->Health; + combatant.MaxHealth = entry->MaxHealth; + combatant.BaseAttack = entry->Attack; + combatant.BoardIndex = static_cast(entry->BoardIndex); + combatant.Role = entry->Role; + combatant.AutoAttackSpellID = entry->AutoAttackSpellID; + combatant.PrimarySpellID = entry->GarrAutoSpellID; + combatant.Flags = entry->Flags; + combatant.IsPlayerSide = false; + + return combatant; +} + +void GarrisonAutoCombat::ProcessTurn( + AutoCombatCombatant& combatant, + std::vector& allies, + std::vector& enemies, + AutoCombatRound& round) +{ + if (!combatant.IsAlive()) + return; + + // Healers prioritize healing wounded allies + if (combatant.Role == AUTO_COMBAT_ROLE_HEALER) + { + AutoCombatCombatant* woundedAlly = FindLowestHPAlive(allies); + if (woundedAlly && woundedAlly->CurrentHealth < woundedAlly->MaxHealth) + { + // Try primary heal spell if available and not on cooldown + if (combatant.PrimarySpellID != 0 + && combatant.SpellCooldowns.find(combatant.PrimarySpellID) == combatant.SpellCooldowns.end()) + { + ResolveSpell(combatant, combatant.PrimarySpellID, allies, enemies, round); + return; + } + } + } + + // Try primary spell if available and not on cooldown + if (combatant.PrimarySpellID != 0 + && combatant.SpellCooldowns.find(combatant.PrimarySpellID) == combatant.SpellCooldowns.end()) + { + ResolveSpell(combatant, combatant.PrimarySpellID, allies, enemies, round); + return; + } + + // Fall back to auto-attack + if (combatant.AutoAttackSpellID != 0) + { + ResolveSpell(combatant, combatant.AutoAttackSpellID, allies, enemies, round); + return; + } + + // Last resort: basic attack on first alive enemy + AutoCombatCombatant* target = nullptr; + for (auto& enemy : enemies) + { + if (enemy.IsAlive()) + { + target = &enemy; + break; + } + } + + if (target) + { + int32 damage = combatant.GetEffectiveAttack(); + ApplyDamage(*target, damage, combatant.BoardIndex, 0, + AUTO_COMBAT_EFFECT_DAMAGE, round); + } +} + +void GarrisonAutoCombat::ResolveSpell( + AutoCombatCombatant& caster, int32 spellID, + std::vector& allies, + std::vector& enemies, + AutoCombatRound& round) +{ + GarrAutoSpellEntry const* spell = sGarrAutoSpellStore.LookupEntry(spellID); + if (!spell) + { + // Unknown spell — fall back to basic attack + for (auto& enemy : enemies) + { + if (enemy.IsAlive()) + { + ApplyDamage(enemy, caster.GetEffectiveAttack(), + caster.BoardIndex, 0, AUTO_COMBAT_EFFECT_DAMAGE, round); + break; + } + } + return; + } + + // Get spell effects + std::vector const* effects = + sGarrisonMgr.GetAutoSpellEffects(spellID); + + if (!effects || effects->empty()) + { + // Spell with no effects — treat as single-target damage using caster attack + for (auto& enemy : enemies) + { + if (enemy.IsAlive()) + { + ApplyDamage(enemy, caster.GetEffectiveAttack(), + caster.BoardIndex, spellID, AUTO_COMBAT_EFFECT_DAMAGE, round); + break; + } + } + } + else + { + // Resolve each effect + for (GarrAutoSpellEffectEntry const* effect : *effects) + { + std::vector targets = + SelectTargets(caster, effect->Targets, allies, enemies); + + for (AutoCombatCombatant* target : targets) + { + if (target && target->IsAlive()) + ResolveEffect(caster, effect, *target, spellID, round); + } + } + } + + // Apply cooldown + if (spell->Cooldown > 0) + caster.SpellCooldowns[spellID] = spell->Cooldown; +} + +void GarrisonAutoCombat::ResolveEffect( + AutoCombatCombatant& caster, GarrAutoSpellEffectEntry const* effect, + AutoCombatCombatant& target, uint32 spellID, + AutoCombatRound& round) +{ + // Scale effect amount by caster attack + int32 amount = static_cast(effect->Amount); + if (amount == 0) + amount = caster.GetEffectiveAttack(); + + switch (effect->EffectType) + { + case AUTO_COMBAT_EFFECT_DAMAGE: + ApplyDamage(target, amount, caster.BoardIndex, spellID, + AUTO_COMBAT_EFFECT_DAMAGE, round); + break; + + case AUTO_COMBAT_EFFECT_HEAL: + ApplyHealing(target, amount, caster.BoardIndex, spellID, round); + break; + + case AUTO_COMBAT_EFFECT_BUFF_ATTACK: + { + AutoCombatAttackModifier mod; + mod.Amount = amount; + mod.RemainingRounds = effect->Period > 0 ? effect->Period : 3; + target.AttackModifiers.push_back(mod); + + AutoCombatEvent event; + event.CasterBoardIndex = caster.BoardIndex; + event.TargetBoardIndex = target.BoardIndex; + event.SpellID = spellID; + event.Amount = amount; + event.EffectType = AUTO_COMBAT_EFFECT_BUFF_ATTACK; + round.Events.push_back(event); + break; + } + + case AUTO_COMBAT_EFFECT_DEBUFF_ATTACK: + { + AutoCombatAttackModifier mod; + mod.Amount = -amount; + mod.RemainingRounds = effect->Period > 0 ? effect->Period : 3; + target.AttackModifiers.push_back(mod); + + AutoCombatEvent event; + event.CasterBoardIndex = caster.BoardIndex; + event.TargetBoardIndex = target.BoardIndex; + event.SpellID = spellID; + event.Amount = -amount; + event.EffectType = AUTO_COMBAT_EFFECT_DEBUFF_ATTACK; + round.Events.push_back(event); + break; + } + + case AUTO_COMBAT_EFFECT_SHIELD: + { + target.ShieldAmount += amount; + + AutoCombatEvent event; + event.CasterBoardIndex = caster.BoardIndex; + event.TargetBoardIndex = target.BoardIndex; + event.SpellID = spellID; + event.Amount = amount; + event.EffectType = AUTO_COMBAT_EFFECT_SHIELD; + round.Events.push_back(event); + break; + } + + case AUTO_COMBAT_EFFECT_AOE_DAMAGE: + // Already handled by target selection returning all enemies + ApplyDamage(target, amount, caster.BoardIndex, spellID, + AUTO_COMBAT_EFFECT_AOE_DAMAGE, round); + break; + + case AUTO_COMBAT_EFFECT_HOT: + { + AutoCombatPeriodicEffect periodic; + periodic.SpellID = spellID; + periodic.Amount = amount; + periodic.RemainingTicks = effect->Period > 0 ? effect->Period : 3; + periodic.IsDamage = false; + periodic.SourceBoardIndex = caster.BoardIndex; + target.PeriodicEffects.push_back(periodic); + + AutoCombatEvent event; + event.CasterBoardIndex = caster.BoardIndex; + event.TargetBoardIndex = target.BoardIndex; + event.SpellID = spellID; + event.Amount = amount; + event.EffectType = AUTO_COMBAT_EFFECT_HOT; + round.Events.push_back(event); + break; + } + + case AUTO_COMBAT_EFFECT_DOT: + { + AutoCombatPeriodicEffect periodic; + periodic.SpellID = spellID; + periodic.Amount = amount; + periodic.RemainingTicks = effect->Period > 0 ? effect->Period : 3; + periodic.IsDamage = true; + periodic.SourceBoardIndex = caster.BoardIndex; + target.PeriodicEffects.push_back(periodic); + + AutoCombatEvent event; + event.CasterBoardIndex = caster.BoardIndex; + event.TargetBoardIndex = target.BoardIndex; + event.SpellID = spellID; + event.Amount = amount; + event.EffectType = AUTO_COMBAT_EFFECT_DOT; + round.Events.push_back(event); + break; + } + + default: + TC_LOG_DEBUG("garrison", "GarrisonAutoCombat: Unknown effect type %u for spell %u", + effect->EffectType, spellID); + break; + } +} + +std::vector GarrisonAutoCombat::SelectTargets( + AutoCombatCombatant& caster, uint8 targetType, + std::vector& allies, + std::vector& enemies) +{ + std::vector targets; + + switch (targetType) + { + case AUTO_COMBAT_TARGET_SELF: + targets.push_back(&caster); + break; + + case AUTO_COMBAT_TARGET_SINGLE_ENEMY: + { + // Target first alive enemy (by board index) + for (auto& enemy : enemies) + { + if (enemy.IsAlive()) + { + targets.push_back(&enemy); + break; + } + } + break; + } + + case AUTO_COMBAT_TARGET_ALL_ENEMIES: + for (auto& enemy : enemies) + if (enemy.IsAlive()) + targets.push_back(&enemy); + break; + + case AUTO_COMBAT_TARGET_SINGLE_ALLY: + { + // Prefer wounded ally + AutoCombatCombatant* wounded = FindLowestHPAlive(allies); + if (wounded) + targets.push_back(wounded); + break; + } + + case AUTO_COMBAT_TARGET_ALL_ALLIES: + for (auto& ally : allies) + if (ally.IsAlive()) + targets.push_back(&ally); + break; + + case AUTO_COMBAT_TARGET_LOWEST_HP_ALLY: + { + AutoCombatCombatant* lowest = FindLowestHPAlive(allies); + if (lowest) + targets.push_back(lowest); + break; + } + + case AUTO_COMBAT_TARGET_HIGHEST_HP_ENEMY: + { + AutoCombatCombatant* highest = FindHighestHPAlive(enemies); + if (highest) + targets.push_back(highest); + break; + } + + case AUTO_COMBAT_TARGET_RANDOM_ENEMY: + { + std::vector alive; + for (auto& enemy : enemies) + if (enemy.IsAlive()) + alive.push_back(&enemy); + if (!alive.empty()) + targets.push_back(alive[urand(0, static_cast(alive.size()) - 1)]); + break; + } + + default: + // Fall back to single enemy + for (auto& enemy : enemies) + { + if (enemy.IsAlive()) + { + targets.push_back(&enemy); + break; + } + } + break; + } + + return targets; +} + +void GarrisonAutoCombat::ApplyDamage( + AutoCombatCombatant& target, int32 amount, + int8 casterBoardIndex, uint32 spellID, uint8 effectType, + AutoCombatRound& round) +{ + int32 damage = amount; + + // Absorb shields first + if (target.ShieldAmount > 0) + { + int32 absorbed = std::min(target.ShieldAmount, damage); + target.ShieldAmount -= absorbed; + damage -= absorbed; + } + + target.CurrentHealth -= damage; + if (target.CurrentHealth < 0) + target.CurrentHealth = 0; + + AutoCombatEvent event; + event.CasterBoardIndex = casterBoardIndex; + event.TargetBoardIndex = target.BoardIndex; + event.SpellID = spellID; + event.Amount = amount; + event.EffectType = effectType; + round.Events.push_back(event); +} + +void GarrisonAutoCombat::ApplyHealing( + AutoCombatCombatant& target, int32 amount, + int8 casterBoardIndex, uint32 spellID, + AutoCombatRound& round) +{ + target.CurrentHealth = std::min(target.CurrentHealth + amount, target.MaxHealth); + + AutoCombatEvent event; + event.CasterBoardIndex = casterBoardIndex; + event.TargetBoardIndex = target.BoardIndex; + event.SpellID = spellID; + event.Amount = amount; + event.EffectType = AUTO_COMBAT_EFFECT_HEAL; + round.Events.push_back(event); +} + +bool GarrisonAutoCombat::IsTeamAlive(std::vector const& team) +{ + for (auto const& combatant : team) + if (combatant.IsAlive()) + return true; + return false; +} + +AutoCombatCombatant* GarrisonAutoCombat::FindLowestHPAlive(std::vector& team) +{ + AutoCombatCombatant* lowest = nullptr; + float lowestPct = 2.0f; + + for (auto& combatant : team) + { + if (!combatant.IsAlive()) + continue; + + float pct = static_cast(combatant.CurrentHealth) / + static_cast(std::max(combatant.MaxHealth, 1)); + + if (pct < lowestPct) + { + lowestPct = pct; + lowest = &combatant; + } + } + + return lowest; +} + +AutoCombatCombatant* GarrisonAutoCombat::FindHighestHPAlive(std::vector& team) +{ + AutoCombatCombatant* highest = nullptr; + int32 highestHP = -1; + + for (auto& combatant : team) + { + if (!combatant.IsAlive()) + continue; + + if (combatant.CurrentHealth > highestHP) + { + highestHP = combatant.CurrentHealth; + highest = &combatant; + } + } + + return highest; +} diff --git a/src/server/game/Garrison/GarrisonAutoCombat.h b/src/server/game/Garrison/GarrisonAutoCombat.h new file mode 100644 index 000000000..7da4f339f --- /dev/null +++ b/src/server/game/Garrison/GarrisonAutoCombat.h @@ -0,0 +1,180 @@ +/* + * 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 GarrisonAutoCombat_h__ +#define GarrisonAutoCombat_h__ + +#include "Define.h" +#include +#include + +struct GarrAutoCombatantEntry; +struct GarrAutoSpellEntry; +struct GarrAutoSpellEffectEntry; + +enum AutoCombatEffectType : uint8 +{ + AUTO_COMBAT_EFFECT_DAMAGE = 0, + AUTO_COMBAT_EFFECT_HEAL = 1, + AUTO_COMBAT_EFFECT_BUFF_ATTACK = 2, + AUTO_COMBAT_EFFECT_DEBUFF_ATTACK = 3, + AUTO_COMBAT_EFFECT_SHIELD = 4, + AUTO_COMBAT_EFFECT_AOE_DAMAGE = 5, + AUTO_COMBAT_EFFECT_HOT = 6, + AUTO_COMBAT_EFFECT_DOT = 7, + AUTO_COMBAT_EFFECT_MAX +}; + +enum AutoCombatTargetType : uint8 +{ + AUTO_COMBAT_TARGET_SELF = 0, + AUTO_COMBAT_TARGET_SINGLE_ENEMY = 1, + AUTO_COMBAT_TARGET_ALL_ENEMIES = 2, + AUTO_COMBAT_TARGET_SINGLE_ALLY = 3, + AUTO_COMBAT_TARGET_ALL_ALLIES = 4, + AUTO_COMBAT_TARGET_LOWEST_HP_ALLY = 5, + AUTO_COMBAT_TARGET_HIGHEST_HP_ENEMY = 6, + AUTO_COMBAT_TARGET_RANDOM_ENEMY = 7, + AUTO_COMBAT_TARGET_MAX +}; + +enum AutoCombatRole : int32 +{ + AUTO_COMBAT_ROLE_DPS = 0, + AUTO_COMBAT_ROLE_TANK = 1, + AUTO_COMBAT_ROLE_HEALER = 2, + AUTO_COMBAT_ROLE_SUPPORT = 3 +}; + +struct AutoCombatPeriodicEffect +{ + uint32 SpellID = 0; + int32 Amount = 0; + int32 RemainingTicks = 0; + bool IsDamage = true; + int8 SourceBoardIndex = -1; +}; + +struct AutoCombatAttackModifier +{ + int32 Amount = 0; + int32 RemainingRounds = 0; +}; + +struct TC_GAME_API AutoCombatCombatant +{ + uint32 AutoCombatantID = 0; + int32 CurrentHealth = 0; + int32 MaxHealth = 0; + int32 BaseAttack = 0; + int8 BoardIndex = -1; + int32 Role = AUTO_COMBAT_ROLE_DPS; + int32 AutoAttackSpellID = 0; + int32 PrimarySpellID = 0; + int32 Flags = 0; + bool IsPlayerSide = false; + uint64 FollowerDbID = 0; + + int32 ShieldAmount = 0; + std::map SpellCooldowns; + std::vector PeriodicEffects; + std::vector AttackModifiers; + + bool IsAlive() const { return CurrentHealth > 0; } + int32 GetEffectiveAttack() const; + void TickPeriodicEffects(struct AutoCombatRound& round); + void TickCooldowns(); + void TickModifiers(); +}; + +struct AutoCombatEvent +{ + int8 CasterBoardIndex = -1; + int8 TargetBoardIndex = -1; + uint32 SpellID = 0; + int32 Amount = 0; + uint8 EffectType = 0; +}; + +struct AutoCombatRound +{ + int32 RoundNumber = 0; + std::vector Events; +}; + +struct TC_GAME_API AutoCombatResult +{ + bool PlayerWon = false; + int32 TotalRounds = 0; + std::vector CombatLog; +}; + +class TC_GAME_API GarrisonAutoCombat +{ +public: + static constexpr int32 MAX_ROUNDS = 20; + + static AutoCombatResult SimulateCombat( + std::vector& playerUnits, + std::vector& enemyUnits); + + static AutoCombatCombatant BuildFollowerCombatant( + uint32 followerLevel, uint32 quality, uint32 itemLevelWeapon, + uint32 itemLevelArmor, int8 boardIndex, uint64 followerDbID); + + static AutoCombatCombatant BuildEnemyCombatant( + GarrAutoCombatantEntry const* entry); + +private: + static void ProcessTurn( + AutoCombatCombatant& combatant, + std::vector& allies, + std::vector& enemies, + AutoCombatRound& round); + + static void ResolveSpell( + AutoCombatCombatant& caster, int32 spellID, + std::vector& allies, + std::vector& enemies, + AutoCombatRound& round); + + static void ResolveEffect( + AutoCombatCombatant& caster, GarrAutoSpellEffectEntry const* effect, + AutoCombatCombatant& target, uint32 spellID, + AutoCombatRound& round); + + static std::vector SelectTargets( + AutoCombatCombatant& caster, uint8 targetType, + std::vector& allies, + std::vector& enemies); + + static void ApplyDamage( + AutoCombatCombatant& target, int32 amount, + int8 casterBoardIndex, uint32 spellID, uint8 effectType, + AutoCombatRound& round); + + static void ApplyHealing( + AutoCombatCombatant& target, int32 amount, + int8 casterBoardIndex, uint32 spellID, + AutoCombatRound& round); + + static bool IsTeamAlive(std::vector const& team); + static AutoCombatCombatant* FindLowestHPAlive(std::vector& team); + static AutoCombatCombatant* FindHighestHPAlive(std::vector& team); +}; + +#endif // GarrisonAutoCombat_h__ diff --git a/src/server/game/Garrison/GarrisonMap.cpp b/src/server/game/Garrison/GarrisonMap.cpp index ab7e04e25..6d8689ec5 100644 --- a/src/server/game/Garrison/GarrisonMap.cpp +++ b/src/server/game/Garrison/GarrisonMap.cpp @@ -16,9 +16,12 @@ */ #include "GarrisonMap.h" +#include "Creature.h" +#include "DB2Stores.h" #include "DBCEnums.h" #include "GameObject.h" #include "Garrison.h" +#include "Group.h" #include "Log.h" #include "ObjectAccessor.h" #include "ObjectGridLoader.h" @@ -95,9 +98,92 @@ void GarrisonGridLoader::Visit(GameObjectMapType& m) } } -void GarrisonGridLoader::Visit(CreatureMapType& /*m*/) +void GarrisonGridLoader::Visit(CreatureMapType& m) { + if (!i_garrison) + return; + CellCoord cellCoord = i_cell.GetCellCoord(); + GarrisonFactionIndex faction = i_garrison->GetFaction(); + std::vector plots = i_garrison->GetPlots(); + + if (plots.empty()) + return; + + uint32 plotCount = static_cast(plots.size()); + uint32 followerIndex = 0; + + for (auto const& [dbId, follower] : i_garrison->GetFollowerMap()) + { + // Skip followers currently on missions + if (follower.PacketInfo.CurrentMissionID != 0) + continue; + + // Skip inactive followers + if (follower.PacketInfo.FollowerStatus & FOLLOWER_STATUS_INACTIVE) + continue; + + GarrFollowerEntry const* followerEntry = sGarrFollowerStore.LookupEntry(follower.PacketInfo.GarrFollowerID); + if (!followerEntry) + continue; + + // Only spawn followers matching this garrison type + if (followerEntry->GarrTypeID != static_cast(i_garrison->GetType())) + continue; + + uint32 creatureId = faction == GARRISON_FACTION_INDEX_HORDE ? + followerEntry->HordeCreatureID : followerEntry->AllianceCreatureID; + if (!creatureId) + continue; + + // Determine spawn position + Position spawnPos; + bool positionFound = false; + + if (follower.PacketInfo.CurrentBuildingID != 0) + { + // Follower is assigned to a building - spawn near that building's plot + for (Garrison::Plot* plot : plots) + { + if (plot->BuildingInfo.PacketInfo && + plot->BuildingInfo.PacketInfo->GarrBuildingID == follower.PacketInfo.CurrentBuildingID) + { + spawnPos = plot->PacketInfo.PlotPos.Pos; + spawnPos.RelocateOffset({ 3.0f, 3.0f, 0.0f, 0.0f }); + positionFound = true; + break; + } + } + } + + if (!positionFound) + { + // Unassigned follower - scatter around plots + Garrison::Plot* plot = plots[followerIndex % plotCount]; + spawnPos = plot->PacketInfo.PlotPos.Pos; + // Distribute followers in a circle around the plot center + float angle = static_cast(followerIndex % 8) * (float(M_PI) / 4.0f); + float dist = 5.0f + 2.0f * (followerIndex / 8); + spawnPos.m_positionX += dist * std::cos(angle); + spawnPos.m_positionY += dist * std::sin(angle); + } + + ++followerIndex; + + // Check if this position belongs to current cell + if (cellCoord != Trinity::ComputeCellCoord(spawnPos.GetPositionX(), spawnPos.GetPositionY())) + continue; + + Creature* creature = Creature::CreateCreature(creatureId, i_map, spawnPos); + if (!creature) + continue; + + creature->SetHomePosition(spawnPos); + creature->AddToGrid(m); + ObjectGridLoader::SetObjectCell(creature, cellCoord); + creature->AddToWorld(); + ++i_creatures; + } } GarrisonMap::GarrisonMap(uint32 id, time_t expiry, uint32 instanceId, ObjectGuid const& owner) @@ -136,6 +222,22 @@ bool GarrisonMap::AddPlayerToMap(Player* player, bool initPlayer /*= true*/) { if (player->GetGUID() == _owner) _loadingPlayer = player; + else + { + // Allow party members to enter if they are in the same group as the owner + // and the party garrison feature is enabled + Player* owner = ObjectAccessor::FindConnectedPlayer(_owner); + if (!owner) + return false; + + Group* group = player->GetGroup(); + if (!group || !group->IsMember(owner->GetGUID())) + { + TC_LOG_DEBUG("garrison", "Player {} denied entry to garrison map {} - not in owner's group", + player->GetGUID().ToString().c_str(), GetId()); + return false; + } + } bool result = Map::AddPlayerToMap(player, initPlayer); diff --git a/src/server/game/Garrison/GarrisonMgr.cpp b/src/server/game/Garrison/GarrisonMgr.cpp index 3c110bf1b..396871cb6 100644 --- a/src/server/game/Garrison/GarrisonMgr.cpp +++ b/src/server/game/Garrison/GarrisonMgr.cpp @@ -19,6 +19,7 @@ #include "Containers.h" #include "DatabaseEnv.h" #include "DB2Stores.h" +#include "DB2Structure.h" #include "Garrison.h" #include "Log.h" #include "ObjectMgr.h" @@ -37,6 +38,10 @@ GarrisonMgr& GarrisonMgr::Instance() void GarrisonMgr::Initialize() { + // Build site level index for O(1) lookups by (siteId, level) + for (GarrSiteLevelEntry const* siteLevel : sGarrSiteLevelStore) + _garrSiteLevelBySiteAndLevel[std::make_pair(siteLevel->GarrSiteID, siteLevel->GarrLevel)] = siteLevel; + for (GarrSiteLevelPlotInstEntry const* siteLevelPlotInst : sGarrSiteLevelPlotInstStore) _garrisonPlotInstBySiteLevel[siteLevelPlotInst->GarrSiteLevelID].push_back(siteLevelPlotInst); @@ -73,6 +78,85 @@ void GarrisonMgr::Initialize() } } + // Build follower level XP index + for (GarrFollowerLevelXPEntry const* levelXP : sGarrFollowerLevelXPStore) + _followerLevelXP[std::make_pair(levelXP->GarrFollowerTypeID, levelXP->FollowerLevel)] = levelXP; + + // Build follower quality index + for (GarrFollowerQualityEntry const* quality : sGarrFollowerQualityStore) + _followerQuality[std::make_pair(quality->GarrFollowerTypeID, quality->Quality)] = quality; + + // Build mission index by garrison type + for (GarrMissionEntry const* mission : sGarrMissionStore) + _missionsByGarrType[mission->GarrTypeID].push_back(mission); + + // Build mission encounter index + for (GarrMissionXEncounterEntry const* missionEncounter : sGarrMissionXEncounterStore) + _missionEncounters[missionEncounter->GarrMissionID].push_back(missionEncounter); + + // Build mission required followers index + for (GarrMissionXFollowerEntry const* missionFollower : sGarrMissionXFollowerStore) + _missionRequiredFollowers[missionFollower->GarrMissionID].push_back(missionFollower); + + // Build encounter mechanics index (GarrEncounterXMechanic -> GarrMechanic per encounter) + for (GarrEncounterXMechanicEntry const* encounterMechanic : sGarrEncounterXMechanicStore) + { + if (GarrMechanicEntry const* mechanic = sGarrMechanicStore.LookupEntry(encounterMechanic->GarrMechanicID)) + _encounterMechanics[encounterMechanic->GarrEncounterID].push_back(mechanic); + } + + // Build shipment container index (building type -> container) + for (CharShipmentContainerEntry const* container : sCharShipmentContainerStore) + _shipmentContainersByBuildingType[container->GarrBuildingType] = container; + + // Build shipment index (container -> shipments) + for (CharShipmentEntry const* shipment : sCharShipmentStore) + _shipmentsByContainer[shipment->ContainerID].push_back(shipment); + + // Build follower type index (garrison type -> primary follower type) + // Each garrison type has one primary follower type (e.g., type 2 -> follower type 1, type 3 -> follower type 4) + for (GarrFollowerTypeEntry const* followerType : sGarrFollowerTypeStore) + { + // Use first (non-shipyard) follower type per garrison type as primary + // Shipyard followers (type 2) share garrTypeID 2 with regular followers (type 1) + auto itr = _followerTypeByGarrType.find(followerType->GarrTypeID); + if (itr == _followerTypeByGarrType.end() || followerType->ID < itr->second->ID) + _followerTypeByGarrType[followerType->GarrTypeID] = followerType; + } + + // Build talent tree index (garrison type -> talent trees) + for (GarrTalentTreeEntry const* tree : sGarrTalentTreeStore) + _talentTreesByGarrType[tree->GarrTypeID].push_back(tree); + + // Build talent index (talent tree -> talents) + for (GarrTalentEntry const* talent : sGarrTalentStore) + _talentsByTree[talent->GarrTalentTreeID].push_back(talent); + + // Build talent rank index (talent -> ranks, sorted by rank) + for (GarrTalentRankEntry const* rank : sGarrTalentRankStore) + _talentRanksByTalent[rank->GarrTalentID].push_back(rank); + + for (auto& [talentId, ranks] : _talentRanksByTalent) + std::sort(ranks.begin(), ranks.end(), [](GarrTalentRankEntry const* a, GarrTalentRankEntry const* b) { return a->Rank < b->Rank; }); + + // Build talent research index (talent tree -> research entry via crossref) + for (GarrTalTreeXGarrTalResearchEntry const* xref : sGarrTalTreeXGarrTalResearchStore) + { + if (GarrTalentResearchEntry const* research = sGarrTalentResearchStore.LookupEntry(xref->GarrTalentResearchID)) + _talentResearchByTree[xref->GarrTalentTreeID] = research; + } + + // Auto-combat indices + for (GarrAutoSpellEffectEntry const* effect : sGarrAutoSpellEffectStore) + _autoSpellEffects[effect->GarrAutoSpellID].push_back(effect); + + for (GarrEncounterSetXEncounterEntry const* xref : sGarrEncounterSetXEncounterStore) + _encounterSetEncounters[xref->GarrEncounterSetID].push_back(xref->GarrEncounterID); + + for (GarrAutoCombatantEntry const* combatant : sGarrAutoCombatantStore) + if (combatant->GarrEncounterID != 0) + _autoCombatantByEncounter[combatant->GarrEncounterID] = combatant; + InitializeDbIdSequences(); LoadPlotFinalizeGOInfo(); LoadFollowerClassSpecAbilities(); @@ -80,9 +164,9 @@ void GarrisonMgr::Initialize() GarrSiteLevelEntry const* GarrisonMgr::GetGarrSiteLevelEntry(uint32 garrSiteId, uint32 level) const { - for (GarrSiteLevelEntry const* siteLevel : sGarrSiteLevelStore) - if (siteLevel->GarrSiteID == garrSiteId && siteLevel->GarrLevel == level) - return siteLevel; + auto itr = _garrSiteLevelBySiteAndLevel.find(std::make_pair(garrSiteId, level)); + if (itr != _garrSiteLevelBySiteAndLevel.end()) + return itr->second; return nullptr; } @@ -332,10 +416,183 @@ std::list GarrisonMgr::GetClassSpecAbilities(GarrFollow return abilities; } +GarrFollowerTypeEntry const* GarrisonMgr::GetFollowerTypeForGarrType(int8 garrTypeID) const +{ + auto itr = _followerTypeByGarrType.find(garrTypeID); + if (itr != _followerTypeByGarrType.end()) + return itr->second; + + return nullptr; +} + +uint8 GarrisonMgr::GetPrimaryFollowerType(int8 garrTypeID) const +{ + GarrFollowerTypeEntry const* entry = GetFollowerTypeForGarrType(garrTypeID); + if (entry) + return entry->ID; + + // Fallback to WoD follower type + return FOLLOWER_TYPE_GARRISON; +} + +GarrFollowerLevelXPEntry const* GarrisonMgr::GetFollowerLevelXP(uint8 garrFollowerTypeID, int8 followerLevel) const +{ + auto itr = _followerLevelXP.find(std::make_pair(garrFollowerTypeID, followerLevel)); + if (itr != _followerLevelXP.end()) + return itr->second; + + return nullptr; +} + +GarrFollowerQualityEntry const* GarrisonMgr::GetFollowerQuality(uint16 garrFollowerTypeID, int8 quality) const +{ + auto itr = _followerQuality.find(std::make_pair(garrFollowerTypeID, quality)); + if (itr != _followerQuality.end()) + return itr->second; + + return nullptr; +} + +uint32 GarrisonMgr::GetFollowerZoneSupportSpell(uint32 garrFollowerID, uint32 factionIndex) const +{ + GarrFollSupportSpellEntry const* best = nullptr; + for (GarrFollSupportSpellEntry const* entry : sGarrFollSupportSpellStore) + { + if (entry->GarrFollowerID != garrFollowerID) + continue; + if (!best || entry->OrderIndex < best->OrderIndex) + best = entry; + } + + if (!best) + return 0; + + return factionIndex == GARRISON_FACTION_INDEX_ALLIANCE ? uint32(best->AllianceSpellID) : uint32(best->HordeSpellID); +} + +std::vector const* GarrisonMgr::GetMissionsByGarrType(int8 garrTypeID) const +{ + auto itr = _missionsByGarrType.find(garrTypeID); + if (itr != _missionsByGarrType.end()) + return &itr->second; + + return nullptr; +} + +std::vector const* GarrisonMgr::GetMissionEncounters(uint32 garrMissionID) const +{ + auto itr = _missionEncounters.find(garrMissionID); + if (itr != _missionEncounters.end()) + return &itr->second; + + return nullptr; +} + +std::vector const* GarrisonMgr::GetEncounterMechanics(uint32 garrEncounterID) const +{ + auto itr = _encounterMechanics.find(garrEncounterID); + if (itr != _encounterMechanics.end()) + return &itr->second; + + return nullptr; +} + +std::vector const* GarrisonMgr::GetMissionRequiredFollowers(uint32 garrMissionID) const +{ + auto itr = _missionRequiredFollowers.find(garrMissionID); + if (itr != _missionRequiredFollowers.end()) + return &itr->second; + + return nullptr; +} + +GarrMechanicTypeEntry const* GarrisonMgr::GetMechanicType(int32 garrMechanicTypeID) const +{ + return sGarrMechanicTypeStore.LookupEntry(garrMechanicTypeID); +} + +bool GarrisonMgr::DoesAbilityCounterMechanic(GarrAbilityEntry const* ability, GarrMechanicTypeEntry const* mechanicType) const +{ + if (!ability || !mechanicType) + return false; + + // An ability counters a mechanic if they share the same GarrAbilityCategoryID + return ability->GarrAbilityCategoryID == mechanicType->GarrAbilityCategoryID + && ability->GarrAbilityCategoryID != 0; +} + +CharShipmentContainerEntry const* GarrisonMgr::GetShipmentContainerForBuilding(uint8 garrBuildingType) const +{ + auto itr = _shipmentContainersByBuildingType.find(garrBuildingType); + if (itr != _shipmentContainersByBuildingType.end()) + return itr->second; + + return nullptr; +} + +std::vector const* GarrisonMgr::GetShipmentsForContainer(uint32 containerID) const +{ + auto itr = _shipmentsByContainer.find(containerID); + if (itr != _shipmentsByContainer.end()) + return &itr->second; + + return nullptr; +} + +uint64 GarrisonMgr::GenerateShipmentDbId() +{ + if (_shipmentDbIdGenerator >= std::numeric_limits::max()) + { + TC_LOG_ERROR("misc", "Garrison shipment db id overflow! Can't continue, shutting down server. "); + World::StopNow(ERROR_EXIT_CODE); + } + + return _shipmentDbIdGenerator++; +} + +std::vector const* GarrisonMgr::GetTalentTreesForGarrType(int8 garrTypeID) const +{ + auto itr = _talentTreesByGarrType.find(garrTypeID); + if (itr != _talentTreesByGarrType.end()) + return &itr->second; + + return nullptr; +} + +std::vector const* GarrisonMgr::GetTalentsForTree(uint32 garrTalentTreeID) const +{ + auto itr = _talentsByTree.find(garrTalentTreeID); + if (itr != _talentsByTree.end()) + return &itr->second; + + return nullptr; +} + +std::vector const* GarrisonMgr::GetTalentRanksForTalent(uint32 garrTalentID) const +{ + auto itr = _talentRanksByTalent.find(garrTalentID); + if (itr != _talentRanksByTalent.end()) + return &itr->second; + + return nullptr; +} + +GarrTalentResearchEntry const* GarrisonMgr::GetTalentResearchForTree(uint32 garrTalentTreeID) const +{ + auto itr = _talentResearchByTree.find(garrTalentTreeID); + if (itr != _talentResearchByTree.end()) + return itr->second; + + return nullptr; +} + void GarrisonMgr::InitializeDbIdSequences() { if (QueryResult result = CharacterDatabase.Query("SELECT MAX(dbId) FROM character_garrison_followers")) _followerDbIdGenerator = (*result)[0].GetUInt64() + 1; + + if (QueryResult result = CharacterDatabase.Query("SELECT MAX(dbId) FROM character_garrison_shipments")) + _shipmentDbIdGenerator = (*result)[0].GetUInt64() + 1; } void GarrisonMgr::LoadPlotFinalizeGOInfo() @@ -464,3 +721,35 @@ void GarrisonMgr::LoadFollowerClassSpecAbilities() TC_LOG_INFO("server.loading", ">> Loaded {} garrison follower class spec abilities in {}.", count, GetMSTimeDiffToNow(msTime)); } + +GarrAutoCombatantEntry const* GarrisonMgr::GetAutoCombatant(uint32 garrAutoCombatantID) const +{ + return sGarrAutoCombatantStore.LookupEntry(garrAutoCombatantID); +} + +GarrAutoCombatantEntry const* GarrisonMgr::GetAutoCombatantForEncounter(uint32 garrEncounterID) const +{ + auto itr = _autoCombatantByEncounter.find(garrEncounterID); + if (itr != _autoCombatantByEncounter.end()) + return itr->second; + + return nullptr; +} + +std::vector const* GarrisonMgr::GetAutoSpellEffects(uint32 garrAutoSpellID) const +{ + auto itr = _autoSpellEffects.find(garrAutoSpellID); + if (itr != _autoSpellEffects.end()) + return &itr->second; + + return nullptr; +} + +std::vector const* GarrisonMgr::GetEncounterSetEncounters(int32 garrEncounterSetID) const +{ + auto itr = _encounterSetEncounters.find(garrEncounterSetID); + if (itr != _encounterSetEncounters.end()) + return &itr->second; + + return nullptr; +} diff --git a/src/server/game/Garrison/GarrisonMgr.h b/src/server/game/Garrison/GarrisonMgr.h index 22cef01fa..9d53e52c9 100644 --- a/src/server/game/Garrison/GarrisonMgr.h +++ b/src/server/game/Garrison/GarrisonMgr.h @@ -27,11 +27,30 @@ #include #include +struct CharShipmentEntry; +struct CharShipmentContainerEntry; struct GameObjectsEntry; struct GarrAbilityEntry; +struct GarrAutoCombatantEntry; +struct GarrAutoSpellEntry; +struct GarrAutoSpellEffectEntry; +struct GarrEncounterEntry; struct GarrFollowerEntry; +struct GarrFollowerLevelXPEntry; +struct GarrFollowerQualityEntry; +struct GarrFollSupportSpellEntry; +struct GarrMechanicEntry; +struct GarrMissionXFollowerEntry; +struct GarrMechanicTypeEntry; +struct GarrMissionEntry; +struct GarrMissionXEncounterEntry; struct GarrSiteLevelEntry; struct GarrSiteLevelPlotInstEntry; +struct GarrFollowerTypeEntry; +struct GarrTalentEntry; +struct GarrTalentRankEntry; +struct GarrTalentResearchEntry; +struct GarrTalentTreeEntry; struct FinalizeGarrisonPlotGOInfo { @@ -74,11 +93,49 @@ public: std::list RollFollowerAbilities(uint32 garrFollowerId, GarrFollowerEntry const* follower, uint32 quality, uint32 faction, bool initial) const; std::list GetClassSpecAbilities(GarrFollowerEntry const* follower, uint32 faction) const; + // Follower type mapping + GarrFollowerTypeEntry const* GetFollowerTypeForGarrType(int8 garrTypeID) const; + uint8 GetPrimaryFollowerType(int8 garrTypeID) const; + + // Follower progression + GarrFollowerLevelXPEntry const* GetFollowerLevelXP(uint8 garrFollowerTypeID, int8 followerLevel) const; + GarrFollowerQualityEntry const* GetFollowerQuality(uint16 garrFollowerTypeID, int8 quality) const; + + // Returns the lowest-OrderIndex zone-support spell for a follower in the given faction + // (GARRISON_FACTION_INDEX_HORDE / GARRISON_FACTION_INDEX_ALLIANCE). Returns 0 when none defined. + uint32 GetFollowerZoneSupportSpell(uint32 garrFollowerID, uint32 factionIndex) const; + + // Shipment system accessors + CharShipmentContainerEntry const* GetShipmentContainerForBuilding(uint8 garrBuildingType) const; + std::vector const* GetShipmentsForContainer(uint32 containerID) const; + uint64 GenerateShipmentDbId(); + + // Talent system accessors + std::vector const* GetTalentTreesForGarrType(int8 garrTypeID) const; + std::vector const* GetTalentsForTree(uint32 garrTalentTreeID) const; + std::vector const* GetTalentRanksForTalent(uint32 garrTalentID) const; + GarrTalentResearchEntry const* GetTalentResearchForTree(uint32 garrTalentTreeID) const; + + // Mission system accessors + std::vector const* GetMissionsByGarrType(int8 garrTypeID) const; + std::vector const* GetMissionEncounters(uint32 garrMissionID) const; + std::vector const* GetEncounterMechanics(uint32 garrEncounterID) const; + std::vector const* GetMissionRequiredFollowers(uint32 garrMissionID) const; + GarrMechanicTypeEntry const* GetMechanicType(int32 garrMechanicTypeID) const; + bool DoesAbilityCounterMechanic(GarrAbilityEntry const* ability, GarrMechanicTypeEntry const* mechanicType) const; + + // Auto-combat accessors + GarrAutoCombatantEntry const* GetAutoCombatant(uint32 garrAutoCombatantID) const; + GarrAutoCombatantEntry const* GetAutoCombatantForEncounter(uint32 garrEncounterID) const; + std::vector const* GetAutoSpellEffects(uint32 garrAutoSpellID) const; + std::vector const* GetEncounterSetEncounters(int32 garrEncounterSetID) const; + private: void InitializeDbIdSequences(); void LoadPlotFinalizeGOInfo(); void LoadFollowerClassSpecAbilities(); + std::unordered_map, GarrSiteLevelEntry const*> _garrSiteLevelBySiteAndLevel; std::unordered_map> _garrisonPlotInstBySiteLevel; std::unordered_map> _garrisonPlots; std::unordered_map> _garrisonBuildingsByPlot; @@ -89,7 +146,36 @@ private: std::unordered_map> _garrisonFollowerClassSpecAbilities; std::set _garrisonFollowerRandomTraits; + // Follower type index (garrison type -> primary follower type entry) + std::unordered_map _followerTypeByGarrType; + + // Follower progression indices + std::unordered_map, GarrFollowerLevelXPEntry const*> _followerLevelXP; + std::unordered_map, GarrFollowerQualityEntry const*> _followerQuality; + + // Mission system indices + std::unordered_map> _missionsByGarrType; + std::unordered_map> _missionEncounters; + std::unordered_map> _encounterMechanics; + std::unordered_map> _missionRequiredFollowers; + + // Talent system indices + std::unordered_map> _talentTreesByGarrType; + std::unordered_map> _talentsByTree; + std::unordered_map> _talentRanksByTalent; + std::unordered_map _talentResearchByTree; + + // Auto-combat indices + std::unordered_map> _autoSpellEffects; + std::unordered_map> _encounterSetEncounters; + std::unordered_map _autoCombatantByEncounter; + + // Shipment system indices + std::unordered_map _shipmentContainersByBuildingType; + std::unordered_map> _shipmentsByContainer; + uint64 _followerDbIdGenerator = UI64LIT(1); + uint64 _shipmentDbIdGenerator = UI64LIT(1); }; #define sGarrisonMgr GarrisonMgr::Instance() diff --git a/src/server/game/Handlers/GarrisonHandler.cpp b/src/server/game/Handlers/GarrisonHandler.cpp index a0a044c52..8b8d08788 100644 --- a/src/server/game/Handlers/GarrisonHandler.cpp +++ b/src/server/game/Handlers/GarrisonHandler.cpp @@ -16,19 +16,48 @@ */ #include "WorldSession.h" +#include "Creature.h" +#include "DB2Stores.h" +#include "DB2Structure.h" +#include "GameTime.h" #include "Garrison.h" +#include "GarrisonMgr.h" #include "GarrisonPackets.h" +#include "Group.h" +#include "Log.h" +#include "ObjectAccessor.h" #include "Player.h" +#include "Random.h" +#include void WorldSession::HandleGetGarrisonInfo(WorldPackets::Garrison::GetGarrisonInfo& /*getGarrisonInfo*/) { + // Sniff-confirmed: troop quality refresh packets sent BEFORE main garrison info + for (auto const& [type, garrison] : _player->GetGarrisons()) + garrison->SendTroopQualityRefresh(); + WorldPackets::Garrison::GetGarrisonInfoResult garrisonInfo; garrisonInfo.FactionIndex = Garrison::GetFaction(_player->GetTeam()); - if (Garrison* garrison = _player->GetGarrison()) + for (auto const& [type, garrison] : _player->GetGarrisons()) garrison->BuildInfoPacket(garrisonInfo.Garrisons.emplace_back()); + garrisonInfo.FollowerSoftCaps = { + { FOLLOWER_TYPE_GARRISON, 20 }, + { FOLLOWER_TYPE_SHIPYARD, 6 }, + { FOLLOWER_TYPE_CLASS_ORDER, 6 }, + { FOLLOWER_TYPE_WAR_CAMPAIGN, 30 }, + { FOLLOWER_TYPE_COVENANT, 100 } + }; + SendPacket(garrisonInfo.Write()); + + // Follow up with expired mission cleanup and mission start condition updates per garrison + for (auto const& [type, garrison] : _player->GetGarrisons()) + { + garrison->SendDeleteExpiredMissionsResult(); + garrison->SendMissionStartConditionUpdate(); + } } void WorldSession::HandleGarrisonPurchaseBuilding(WorldPackets::Garrison::GarrisonPurchaseBuilding& garrisonPurchaseBuilding) @@ -51,12 +80,756 @@ void WorldSession::HandleGarrisonCancelConstruction(WorldPackets::Garrison::Garr void WorldSession::HandleGarrisonRequestBlueprintAndSpecializationData(WorldPackets::Garrison::GarrisonRequestBlueprintAndSpecializationData& /*garrisonRequestBlueprintAndSpecializationData*/) { - if (Garrison* garrison = _player->GetGarrison()) + for (auto const& [type, garrison] : _player->GetGarrisons()) garrison->SendBlueprintAndSpecializationData(); } void WorldSession::HandleGarrisonGetMapData(WorldPackets::Garrison::GarrisonGetMapData& /*garrisonGetMapData*/) { if (Garrison* garrison = _player->GetGarrison()) + { garrison->SendMapData(_player); + + // Send monument/trophy selections after map data (sniff-confirmed zone-in sequence) + WorldPackets::Garrison::GarrisonUpdateGarrisonMonumentSelections selections; + for (uint32 trophyId : garrison->GetTrophies()) + { + WorldPackets::Garrison::GarrisonTrophyData data; + data.TrophyID = trophyId; + selections.Trophies.push_back(data); + } + SendPacket(selections.Write()); + } +} + +// ============================================================ +// Mission handlers +// ============================================================ + +void WorldSession::HandleGarrisonStartMission(WorldPackets::Garrison::GarrisonStartMission& garrisonStartMission) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + GarrisonError result = garrison->StartMission(garrisonStartMission.MissionRecID, garrisonStartMission.FollowerDBIDs); + + WorldPackets::Garrison::GarrisonStartMissionResult startResult; + startResult.Result = result; + if (result == GARRISON_SUCCESS) + startResult.NumOfferedToday = static_cast(garrison->GetAndIncrementSessionMissionCount()); + if (Garrison::Mission const* mission = garrison->GetMissionByRecID(garrisonStartMission.MissionRecID)) + startResult.Mission = mission->PacketInfo; + + startResult.FollowerInfos.reserve(garrisonStartMission.FollowerDBIDs.size()); + for (uint64 dbId : garrisonStartMission.FollowerDBIDs) + { + WorldPackets::Garrison::GarrisonMissionFollowerEntry entry; + entry.DbID = dbId; + // BoardIndex/Health/HasFollowerEntry mirror the CMSG values; defaults match the + // shape of what the client sends in CMSG. The full GarrisonFollower trailer + // (Followers vector) stays empty in the standard "mission accepted" response. + startResult.FollowerInfos.push_back(entry); + } + SendPacket(startResult.Write()); +} + +void WorldSession::HandleGarrisonCompleteMission(WorldPackets::Garrison::GarrisonCompleteMission& garrisonCompleteMission) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + Garrison::Mission const* mission = garrison->GetMissionByRecID(garrisonCompleteMission.MissionRecID); + if (!mission) + return; + + GarrisonError result = garrison->CompleteMission(garrisonCompleteMission.MissionRecID); + + WorldPackets::Garrison::GarrisonCompleteMissionResult completeResult; + completeResult.Result = result; + completeResult.MissionRecID = garrisonCompleteMission.MissionRecID; + completeResult.GarrTypeID = static_cast(garrison->GetType()); + + // Re-fetch mission after completion (state may have changed) + mission = garrison->GetMissionByRecID(garrisonCompleteMission.MissionRecID); + if (mission) + { + completeResult.Mission = mission->PacketInfo; + // Determine success based on the success chance roll + completeResult.Succeeded = static_cast(urand(0, 99)) < mission->PacketInfo.SuccessChance; + } + + // FollowerInfos / Rounds left empty: no auto-combat replay generated for non-auto + // missions. The auto-combat simulator (GarrisonAutoCombat::ProcessTurn) populates + // these for Legion+ class hall and Shadowlands covenant missions. WoD-era classic + // missions don't drive the replay UI so they ship empty arrays here. + + SendPacket(completeResult.Write()); +} + +void WorldSession::HandleGarrisonMissionBonusRoll(WorldPackets::Garrison::GarrisonMissionBonusRoll& garrisonMissionBonusRoll) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + GarrisonError result = garrison->MissionBonusRoll(garrisonMissionBonusRoll.MissionRecID); + + WorldPackets::Garrison::GarrisonMissionBonusRollResult bonusResult; + bonusResult.MissionRecID = garrisonMissionBonusRoll.MissionRecID; + bonusResult.Result = result; + + if (Garrison::Mission const* mission = garrison->GetMissionByRecID(garrisonMissionBonusRoll.MissionRecID)) + bonusResult.Mission = mission->PacketInfo; + + SendPacket(bonusResult.Write()); +} + +void WorldSession::HandleGarrisonGetMissionReward(WorldPackets::Garrison::GarrisonGetMissionReward& garrisonGetMissionReward) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + GarrisonError result = garrison->ClaimMissionReward(garrisonGetMissionReward.MissionRecID); + + // ClaimMissionReward already sends GarrisonFollowerChangedXP for each follower, + // and removes the mission internally. Send a targeted deletion notification + // instead of a full GetGarrisonInfoResult to reduce bandwidth. + WorldPackets::Garrison::GarrisonDeleteMissionResult deleteMissionResult; + deleteMissionResult.Result = result; + deleteMissionResult.MissionRecID = garrisonGetMissionReward.MissionRecID; + deleteMissionResult.GarrTypeID = garrison->GetType(); + SendPacket(deleteMissionResult.Write()); +} + +void WorldSession::HandleOpenMissionNpc(WorldPackets::Garrison::OpenMissionNpc& /*openMissionNpc*/) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + // Send expired mission cleanup results for all garrison types + for (auto const& [type, garr] : _player->GetGarrisons()) + garr->SendDeleteExpiredMissionsResult(); + + // Remove expired offers (sends GarrisonDeleteMissionResult per expired mission) + // and generate new missions (sends GarrisonAddMissionResult per new mission). + // Individual targeted packets are sent instead of a full GetGarrisonInfoResult. + garrison->RemoveExpiredMissions(); + garrison->GenerateAvailableMissions(); + + // Send mission start condition update + garrison->SendMissionStartConditionUpdate(); +} + +// ============================================================ +// Follower handlers +// ============================================================ + +void WorldSession::HandleGarrisonAssignFollowerToBuilding(WorldPackets::Garrison::GarrisonAssignFollowerToBuilding& garrisonAssignFollowerToBuilding) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + garrison->AssignFollowerToBuilding(garrisonAssignFollowerToBuilding.FollowerDBID, garrisonAssignFollowerToBuilding.PlotInstanceID); +} + +void WorldSession::HandleGarrisonRemoveFollowerFromBuilding(WorldPackets::Garrison::GarrisonRemoveFollowerFromBuilding& garrisonRemoveFollowerFromBuilding) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + garrison->RemoveFollowerFromBuilding(garrisonRemoveFollowerFromBuilding.FollowerDBID); +} + +void WorldSession::HandleGarrisonRemoveFollower(WorldPackets::Garrison::GarrisonRemoveFollower& garrisonRemoveFollower) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + garrison->RemoveFollower(garrisonRemoveFollower.FollowerDBID); +} + +void WorldSession::HandleGarrisonRenameFollower(WorldPackets::Garrison::GarrisonRenameFollower& garrisonRenameFollower) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + garrison->RenameFollower(garrisonRenameFollower.FollowerDBID, garrisonRenameFollower.FollowerName); +} + +void WorldSession::HandleGarrisonSetFollowerFavorite(WorldPackets::Garrison::GarrisonSetFollowerFavorite& garrisonSetFollowerFavorite) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + garrison->SetFollowerFavorite(garrisonSetFollowerFavorite.FollowerDBID, garrisonSetFollowerFavorite.Favorite); +} + +void WorldSession::HandleGarrisonSetFollowerInactive(WorldPackets::Garrison::GarrisonSetFollowerInactive& garrisonSetFollowerInactive) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + garrison->SetFollowerInactive(garrisonSetFollowerInactive.FollowerDBID, garrisonSetFollowerInactive.Inactive); +} + +void WorldSession::HandleGarrisonRecruitFollower(WorldPackets::Garrison::GarrisonRecruitFollower& garrisonRecruitFollower) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + // FollowerIndex references the index in the available recruits list + auto const& recruits = garrison->GetAvailableRecruits(); + if (garrisonRecruitFollower.FollowerIndex >= recruits.size()) + { + WorldPackets::Garrison::GarrisonRecruitFollowerResult recruitResult; + recruitResult.Result = GARRISON_ERROR_INVALID_AVAILABLE_RECRUIT; + SendPacket(recruitResult.Write()); + return; + } + uint32 followerID = recruits[garrisonRecruitFollower.FollowerIndex].GarrFollowerID; + GarrisonError result = garrison->RecruitFollower(followerID); + + WorldPackets::Garrison::GarrisonRecruitFollowerResult recruitResult; + recruitResult.Result = result; + if (result == GARRISON_SUCCESS) + { + if (Garrison::Follower const* follower = garrison->GetFollowerByEntry(followerID)) + recruitResult.Follower = follower->PacketInfo; + } + SendPacket(recruitResult.Write()); +} + +void WorldSession::HandleGarrisonGenerateRecruits(WorldPackets::Garrison::GarrisonGenerateRecruits& /*garrisonGenerateRecruits*/) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + uint32 faction = static_cast(Garrison::GetFaction(_player->GetTeam())); + garrison->GenerateRecruits(faction); + + // SMSG_GARRISON_GENERATE_FOLLOWERS_RESULT (§8.42): exactly 3 inline GarrisonFollowers. + auto const& recruits = garrison->GetAvailableRecruits(); + WorldPackets::Garrison::GarrisonGenerateFollowersResult result; + for (size_t i = 0; i < result.Followers.size(); ++i) + { + if (i < recruits.size()) + result.Followers[i] = recruits[i]; + // Slots beyond the rolled count stay default-constructed (empty follower record). + } + SendPacket(result.Write()); +} + +void WorldSession::HandleGarrisonFullyHealAllFollowers(WorldPackets::Garrison::GarrisonFullyHealAllFollowers& /*garrisonFullyHealAllFollowers*/) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + garrison->HealAllFollowers(); + + // Send individual GarrisonUpdateFollower packets for each follower + // instead of a full GetGarrisonInfoResult to reduce bandwidth + garrison->SendAllFollowerUpdates(); +} + +void WorldSession::HandleGarrisonAddFollowerHealth(WorldPackets::Garrison::GarrisonAddFollowerHealth& garrisonAddFollowerHealth) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + Garrison::Follower* follower = garrison->GetFollower(garrisonAddFollowerHealth.FollowerDBID); + if (!follower) + return; + + follower->PacketInfo.Health = std::min(follower->PacketInfo.Health + garrisonAddFollowerHealth.HealthToAdd, static_cast(follower->PacketInfo.Durability)); + + WorldPackets::Garrison::GarrisonUpdateFollower updateFollower; + updateFollower.Result = GARRISON_SUCCESS; + updateFollower.Follower = follower->PacketInfo; + SendPacket(updateFollower.Write()); +} + +void WorldSession::HandleGarrisonGetClassSpecCategoryInfo(WorldPackets::Garrison::GarrisonGetClassSpecCategoryInfo& /*garrisonGetClassSpecCategoryInfo*/) +{ + WorldPackets::Garrison::GarrisonGetClassSpecCategoryInfoResult result; + + // Populate class spec categories from DB2 + for (GarrClassSpecEntry const* classSpec : sGarrClassSpecStore) + { + WorldPackets::Garrison::GarrisonGetClassSpecCategoryInfoResult::GarrisonFollowerCategoryInfo info; + info.GarrClassSpecID = classSpec->ID; + info.GarrFollowerTypeID = classSpec->FollowerClassLimit; + result.FollowerClassSpecCategoryInfos.push_back(info); + } + + SendPacket(result.Write()); +} + +void WorldSession::HandleGarrisonSetRecruitmentPreferences(WorldPackets::Garrison::GarrisonSetRecruitmentPreferences& garrisonSetRecruitmentPreferences) +{ + if (!_player->GetGarrison()) + return; + + _player->GetGarrison()->SetRecruitmentPreferences( + garrisonSetRecruitmentPreferences.AbilityID, + garrisonSetRecruitmentPreferences.TraitID); +} + +// ============================================================ +// Building/Utility handlers +// ============================================================ + +void WorldSession::HandleUpgradeGarrison(WorldPackets::Garrison::UpgradeGarrison& upgradeGarrison) +{ + if (!_player->GetNPCIfCanInteractWith(upgradeGarrison.NpcGUID, UNIT_NPC_FLAG_NONE, UNIT_NPC_FLAG_2_GARRISON_ARCHITECT)) + return; + + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + GarrSiteLevelEntry const* currentLevel = garrison->GetSiteLevel(); + if (!currentLevel) + { + WorldPackets::Garrison::GarrisonUpgradeResult result; + result.Result = GARRISON_ERROR_INVALID_GARRISON; + result.GarrSiteLevelID = 0; + SendPacket(result.Write()); + return; + } + + // Look for next level + GarrSiteLevelEntry const* nextLevel = sGarrisonMgr.GetGarrSiteLevelEntry(currentLevel->GarrSiteID, currentLevel->GarrLevel + 1); + if (!nextLevel) + { + WorldPackets::Garrison::GarrisonUpgradeResult result; + result.Result = GARRISON_ERROR_UPGRADE_LEVEL_EXCEEDS_GARRISON_LEVEL; + result.GarrSiteLevelID = currentLevel->ID; + SendPacket(result.Write()); + return; + } + + // Check upgrade cost (from GarrSiteLevelEntry) + if (nextLevel->UpgradeGoldCost > 0 && !_player->HasEnoughMoney(uint64(nextLevel->UpgradeGoldCost))) + { + WorldPackets::Garrison::GarrisonUpgradeResult result; + result.Result = GARRISON_ERROR_NOT_ENOUGH_GOLD; + result.GarrSiteLevelID = currentLevel->ID; + SendPacket(result.Write()); + return; + } + + if (nextLevel->UpgradeCost > 0 && !_player->HasCurrency(824 /*Garrison Resources*/, nextLevel->UpgradeCost)) + { + WorldPackets::Garrison::GarrisonUpgradeResult result; + result.Result = GARRISON_ERROR_NOT_ENOUGH_CURRENCY; + result.GarrSiteLevelID = currentLevel->ID; + SendPacket(result.Write()); + return; + } + + // Deduct costs + if (nextLevel->UpgradeGoldCost > 0) + _player->ModifyMoney(-int64(nextLevel->UpgradeGoldCost), false); + if (nextLevel->UpgradeCost > 0) + _player->RemoveCurrency(824 /*Garrison Resources*/, nextLevel->UpgradeCost, CurrencyDestroyReason::Garrison); + + garrison->Upgrade(); + + WorldPackets::Garrison::GarrisonUpgradeResult result; + result.Result = GARRISON_SUCCESS; + result.GarrSiteLevelID = garrison->GetSiteLevel() ? garrison->GetSiteLevel()->ID : 0; + SendPacket(result.Write()); +} + +void WorldSession::HandleGarrisonCheckUpgradeable(WorldPackets::Garrison::GarrisonCheckUpgradeable& garrisonCheckUpgradeable) +{ + // Client sends GarrSiteID, not GarrTypeID. Find the matching garrison. + Garrison* garrison = nullptr; + for (auto const& [type, garr] : _player->GetGarrisons()) + { + if (garr->GetSiteLevel() && garr->GetSiteLevel()->GarrSiteID == garrisonCheckUpgradeable.GarrSiteID) + { + garrison = garr.get(); + break; + } + } + GarrisonError upgradeResult = GARRISON_ERROR_UPGRADE_CONDITION_FAILED; + + if (garrison) + { + GarrSiteLevelEntry const* currentLevel = garrison->GetSiteLevel(); + if (currentLevel) + { + GarrSiteLevelEntry const* nextLevel = sGarrisonMgr.GetGarrSiteLevelEntry(currentLevel->GarrSiteID, currentLevel->GarrLevel + 1); + if (nextLevel) + { + bool canAfford = true; + if (nextLevel->UpgradeGoldCost > 0 && !_player->HasEnoughMoney(uint64(nextLevel->UpgradeGoldCost))) + canAfford = false; + if (nextLevel->UpgradeCost > 0 && !_player->HasCurrency(824 /*Garrison Resources*/, nextLevel->UpgradeCost)) + canAfford = false; + + if (canAfford) + upgradeResult = GARRISON_SUCCESS; + else + upgradeResult = GARRISON_ERROR_NOT_ENOUGH_CURRENCY; + } + else + upgradeResult = GARRISON_ERROR_UPGRADE_LEVEL_EXCEEDS_GARRISON_LEVEL; + } + } + + WorldPackets::Garrison::GarrisonIsUpgradeableResponse result; + result.Result = upgradeResult; + SendPacket(result.Write()); +} + +void WorldSession::HandleGarrisonSetBuildingActive(WorldPackets::Garrison::GarrisonSetBuildingActive& garrisonSetBuildingActive) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + garrison->ActivateBuilding(garrisonSetBuildingActive.PlotInstanceID); +} + +void WorldSession::HandleGarrisonSwapBuildings(WorldPackets::Garrison::GarrisonSwapBuildings& garrisonSwapBuildings) +{ + if (!_player->GetNPCIfCanInteractWith(garrisonSwapBuildings.NpcGUID, UNIT_NPC_FLAG_NONE, UNIT_NPC_FLAG_2_GARRISON_ARCHITECT)) + return; + + if (Garrison* garrison = _player->GetGarrison()) + garrison->SwapBuildings(garrisonSwapBuildings.PlotInstanceID1, garrisonSwapBuildings.PlotInstanceID2); +} + +// ============================================================ +// Talent handlers +// ============================================================ + +void WorldSession::HandleGarrisonLearnTalent(WorldPackets::Garrison::GarrisonLearnTalent& garrisonLearnTalent) +{ + GarrTalentEntry const* talentEntry = sGarrTalentStore.LookupEntry(garrisonLearnTalent.GarrTalentID); + if (!talentEntry) + { + WorldPackets::Garrison::GarrisonResearchTalentResult result; + result.Result = GARRISON_ERROR_INVALID_TALENT; + SendPacket(result.Write()); + return; + } + + GarrTalentTreeEntry const* treeEntry = sGarrTalentTreeStore.LookupEntry(talentEntry->GarrTalentTreeID); + if (!treeEntry) + { + WorldPackets::Garrison::GarrisonResearchTalentResult result; + result.Result = GARRISON_ERROR_INVALID_TALENT; + SendPacket(result.Write()); + return; + } + + Garrison* garrison = _player->GetGarrison(static_cast(treeEntry->GarrTypeID)); + if (!garrison) + { + WorldPackets::Garrison::GarrisonResearchTalentResult result; + result.Result = GARRISON_ERROR_NO_GARRISON; + result.GarrTypeID = treeEntry->GarrTypeID; + SendPacket(result.Write()); + return; + } + + garrison->LearnTalent(garrisonLearnTalent.GarrTalentID, garrisonLearnTalent.IsTemporary); +} + +void WorldSession::HandleGarrisonResearchTalent(WorldPackets::Garrison::GarrisonResearchTalent& garrisonResearchTalent) +{ + GarrTalentEntry const* talentEntry = sGarrTalentStore.LookupEntry(garrisonResearchTalent.GarrTalentID); + if (!talentEntry) + { + WorldPackets::Garrison::GarrisonResearchTalentResult result; + result.Result = GARRISON_ERROR_INVALID_TALENT; + SendPacket(result.Write()); + return; + } + + GarrTalentTreeEntry const* treeEntry = sGarrTalentTreeStore.LookupEntry(talentEntry->GarrTalentTreeID); + if (!treeEntry) + { + WorldPackets::Garrison::GarrisonResearchTalentResult result; + result.Result = GARRISON_ERROR_INVALID_TALENT; + SendPacket(result.Write()); + return; + } + + Garrison* garrison = _player->GetGarrison(static_cast(treeEntry->GarrTypeID)); + if (!garrison) + { + WorldPackets::Garrison::GarrisonResearchTalentResult result; + result.Result = GARRISON_ERROR_NO_GARRISON; + result.GarrTypeID = treeEntry->GarrTypeID; + SendPacket(result.Write()); + return; + } + + garrison->ResearchTalent(garrisonResearchTalent.GarrTalentID); +} + +void WorldSession::HandleGarrisonSocketTalent(WorldPackets::Garrison::GarrisonSocketTalent& garrisonSocketTalent) +{ + GarrTalentEntry const* talentEntry = sGarrTalentStore.LookupEntry(garrisonSocketTalent.GarrTalentID); + if (!talentEntry) + { + WorldPackets::Garrison::GarrisonResearchTalentResult result; + result.Result = GARRISON_ERROR_INVALID_TALENT; + SendPacket(result.Write()); + return; + } + + GarrTalentTreeEntry const* treeEntry = sGarrTalentTreeStore.LookupEntry(talentEntry->GarrTalentTreeID); + if (!treeEntry) + { + WorldPackets::Garrison::GarrisonResearchTalentResult result; + result.Result = GARRISON_ERROR_INVALID_TALENT; + SendPacket(result.Write()); + return; + } + + Garrison* garrison = _player->GetGarrison(static_cast(treeEntry->GarrTypeID)); + if (!garrison) + { + WorldPackets::Garrison::GarrisonResearchTalentResult result; + result.Result = GARRISON_ERROR_NO_GARRISON; + result.GarrTypeID = treeEntry->GarrTypeID; + SendPacket(result.Write()); + return; + } + + garrison->SocketTalent(garrisonSocketTalent.GarrTalentID, garrisonSocketTalent.SoulbindConduitID, garrisonSocketTalent.SoulbindConduitRank); +} + +// ============================================================ +// Other utility handlers +// ============================================================ + +void WorldSession::HandleGarrisonRequestShipmentInfo(WorldPackets::Garrison::GarrisonRequestShipmentInfo& garrisonRequestShipmentInfo) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + { + WorldPackets::Garrison::GetShipmentInfoResponse response; + SendPacket(response.Write()); + return; + } + + garrison->SendShipmentInfo(garrisonRequestShipmentInfo.NpcGUID); +} + +void WorldSession::HandleOpenShipmentNpc(WorldPackets::Garrison::OpenShipmentNpc& openShipmentNpc) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + uint32 plotInstanceId = garrison->FindPlotInstanceForNpc(openShipmentNpc.NpcGUID); + if (!plotInstanceId) + return; + + Garrison::Plot const* plot = garrison->GetPlot(plotInstanceId); + if (!plot || !plot->BuildingInfo.PacketInfo || !plot->BuildingInfo.PacketInfo->Active) + return; + + GarrBuildingEntry const* building = sGarrBuildingStore.LookupEntry(plot->BuildingInfo.PacketInfo->GarrBuildingID); + if (!building) + return; + + CharShipmentContainerEntry const* container = sGarrisonMgr.GetShipmentContainerForBuilding(building->BuildingType); + if (!container) + return; + + WorldPackets::Garrison::OpenShipmentNpcResult result; + result.NpcGUID = openShipmentNpc.NpcGUID; + result.CharShipmentContainerID = container->ID; + SendPacket(result.Write()); +} + +void WorldSession::HandleCreateShipment(WorldPackets::Garrison::CreateShipment& createShipment) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + garrison->CreateShipment(createShipment.NpcGUID, createShipment.Count); +} + +void WorldSession::HandleGetLandingPageShipments(WorldPackets::Garrison::GetLandingPageShipments& /*getLandingPageShipments*/) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + garrison->SendLandingPageShipments(); +} + +void WorldSession::HandleSetUsingPartyGarrison(WorldPackets::Garrison::SetUsingPartyGarrison& setUsingPartyGarrison) +{ + if (setUsingPartyGarrison.UsingPartyGarrison) + { + // Player wants to visit party leader's garrison + Group* group = _player->GetGroup(); + if (!group) + return; + + Player* leader = ObjectAccessor::FindPlayer(group->GetLeaderGUID()); + if (!leader || leader == _player) + return; + + Garrison* leaderGarrison = leader->GetGarrison(static_cast(setUsingPartyGarrison.GarrTypeID)); + if (!leaderGarrison) + return; + + GarrSiteLevelEntry const* siteLevel = leaderGarrison->GetSiteLevel(); + if (!siteLevel) + return; + + // Teleport the visiting player to the leader's garrison map instance + // The garrison map instance ID is the owner's GUID counter + _player->TeleportTo(WorldLocation(siteLevel->MapID, *_player), TELE_TO_SEAMLESS); + } + else + { + // Player wants to leave the party garrison ? teleport back to Draenor + if (Garrison* ownGarrison = _player->GetGarrison()) + ownGarrison->Leave(); + } +} + +void WorldSession::HandleQueryGarrisonPetName(WorldPackets::Garrison::QueryGarrisonPetName& queryGarrisonPetName) +{ + // IDA case 4980801 (§8.51 pet name): {ObjectGuid NpcGUID, SizedString PetName}. + // Look up the queried NPC and echo back its custom name (if any) ? for non-pet NPCs + // or NPCs without a stored custom name, send an empty string. + WorldPackets::Garrison::QueryGarrisonPetNameResponse response; + response.NpcGUID = queryGarrisonPetName.NpcGUID; + if (Creature const* creature = ObjectAccessor::GetCreature(*_player, queryGarrisonPetName.NpcGUID)) + { + // Garrison pets/bodyguards may carry a custom name on the creature template or summon. + // Until the BattlePet/garrison-pet system is wired up, echo the creature's localized name + // as a sane default. Empty string is also a valid response per IDA pseudocode. + if (CreatureTemplate const* tmpl = creature->GetCreatureTemplate()) + response.PetName = tmpl->Name; + } + SendPacket(response.Write()); +} + +void WorldSession::HandleRequestGarrisonTalentWorldQuestUnlocks(WorldPackets::Garrison::RequestGarrisonTalentWorldQuestUnlocks& /*requestGarrisonTalentWorldQuestUnlocks*/) +{ + // SMSG_GARRISON_TALENT_WORLD_QUEST_UNLOCKS_RESPONSE (0x4C004E) ? Legion+ talent-gated + // map POIs. IDA dispatcher uses opaque helper so exact field shape is unconfirmed; the + // best conservative match is a size-prefixed list of unlocked talent tree IDs (the + // server's view of which trees the player has unlocked talents in for world-quest UI). + WorldPackets::Garrison::GarrisonTalentWorldQuestUnlocksResponse response; + if (Garrison* garrison = _player->GetGarrison()) + { + response.GarrTypeID = static_cast(garrison->GetType()); + // Build the list of unique talent tree IDs from the player's known talents. + std::set trees; + for (auto const& [talentID, talent] : garrison->GetAllTalents()) + { + if (GarrTalentEntry const* entry = sGarrTalentStore.LookupEntry(talentID)) + trees.insert(entry->GarrTalentTreeID); + } + response.UnlockedTalentTreeIDs.assign(trees.begin(), trees.end()); + } + SendPacket(response.Write()); +} + +void WorldSession::HandleGetTrophyList(WorldPackets::Garrison::GetTrophyList& /*getTrophyList*/) +{ + WorldPackets::Garrison::GetTrophyListResponse response; + + Garrison* garrison = _player->GetGarrison(); + if (garrison) + { + response.Success = true; + for (uint32 trophyId : garrison->GetTrophies()) + { + WorldPackets::Garrison::GarrisonTrophyData data; + data.TrophyID = trophyId; + response.Trophies.push_back(data); + } + } + + SendPacket(response.Write()); +} + +void WorldSession::HandleReplaceTrophy(WorldPackets::Garrison::ReplaceTrophy& replaceTrophy) +{ + WorldPackets::Garrison::ReplaceTrophyResponse response; + + Garrison* garrison = _player->GetGarrison(); + if (garrison) + { + garrison->AddTrophy(replaceTrophy.TrophyID); + response.Success = true; + + WorldPackets::Garrison::GarrisonUpdateGarrisonMonumentSelections selections; + for (uint32 trophyId : garrison->GetTrophies()) + { + WorldPackets::Garrison::GarrisonTrophyData data; + data.TrophyID = trophyId; + selections.Trophies.push_back(data); + } + SendPacket(selections.Write()); + } + + SendPacket(response.Write()); +} + +void WorldSession::HandleLoadSelectedTrophy(WorldPackets::Garrison::LoadSelectedTrophy& loadSelectedTrophy) +{ + WorldPackets::Garrison::GetSelectedTrophyIDResponse response; + + Garrison* garrison = _player->GetGarrison(); + if (garrison && garrison->HasTrophy(loadSelectedTrophy.TrophyID)) + { + response.TrophyID = loadSelectedTrophy.TrophyID; + response.Success = true; + } + + SendPacket(response.Write()); +} + +void WorldSession::HandleChangeMonumentAppearance(WorldPackets::Garrison::ChangeMonumentAppearance& changeMonumentAppearance) +{ + Garrison* garrison = _player->GetGarrison(); + if (garrison) + garrison->AddTrophy(changeMonumentAppearance.TrophyID); +} + +void WorldSession::HandleRevertMonumentAppearance(WorldPackets::Garrison::RevertMonumentAppearance& /*revertMonumentAppearance*/) +{ + Garrison* garrison = _player->GetGarrison(); + if (!garrison) + return; + + // Clear all trophies from the garrison (revert to default monument appearance) + std::vector trophiesToRemove(garrison->GetTrophies().begin(), garrison->GetTrophies().end()); + for (uint32 trophyId : trophiesToRemove) + garrison->RemoveTrophy(trophyId); + + // Send updated (empty) monument selections to the client + WorldPackets::Garrison::GarrisonUpdateGarrisonMonumentSelections selections; + SendPacket(selections.Write()); } diff --git a/src/server/game/Server/Protocol/Opcodes.cpp b/src/server/game/Server/Protocol/Opcodes.cpp index fcb6ba824..90575ca57 100644 --- a/src/server/game/Server/Protocol/Opcodes.cpp +++ b/src/server/game/Server/Protocol/Opcodes.cpp @@ -458,33 +458,33 @@ void OpcodeTable::InitializeClientOpcodes() DEFINE_HANDLER(CMSG_GAME_EVENT_DEBUG_ENABLE, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL); DEFINE_HANDLER(CMSG_GAME_OBJ_REPORT_USE, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleGameobjectReportUse); DEFINE_HANDLER(CMSG_GAME_OBJ_USE, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleGameObjectUseOpcode); - DEFINE_HANDLER(CMSG_GARRISON_ADD_FOLLOWER_HEALTH, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_ASSIGN_FOLLOWER_TO_BUILDING, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_CANCEL_CONSTRUCTION, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonCancelConstruction); - DEFINE_HANDLER(CMSG_GARRISON_CHECK_UPGRADEABLE, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_COMPLETE_MISSION, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_FULLY_HEAL_ALL_FOLLOWERS, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_GENERATE_RECRUITS, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_GET_CLASS_SPEC_CATEGORY_INFO, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_GET_MAP_DATA, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonGetMapData); - DEFINE_HANDLER(CMSG_GARRISON_GET_MISSION_REWARD, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_LEARN_TALENT, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_MISSION_BONUS_ROLL, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_PURCHASE_BUILDING, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonPurchaseBuilding); - DEFINE_HANDLER(CMSG_GARRISON_RECRUIT_FOLLOWER, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_REMOVE_FOLLOWER, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_REMOVE_FOLLOWER_FROM_BUILDING, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_RENAME_FOLLOWER, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_REQUEST_BLUEPRINT_AND_SPECIALIZATION_DATA, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonRequestBlueprintAndSpecializationData); - DEFINE_HANDLER(CMSG_GARRISON_REQUEST_SHIPMENT_INFO, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_RESEARCH_TALENT, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_SET_BUILDING_ACTIVE, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_SET_FOLLOWER_FAVORITE, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_SET_FOLLOWER_INACTIVE, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_SET_RECRUITMENT_PREFERENCES, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_SOCKET_TALENT, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_START_MISSION, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); - DEFINE_HANDLER(CMSG_GARRISON_SWAP_BUILDINGS, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL); + DEFINE_HANDLER(CMSG_GARRISON_ADD_FOLLOWER_HEALTH, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonAddFollowerHealth); + DEFINE_HANDLER(CMSG_GARRISON_ASSIGN_FOLLOWER_TO_BUILDING, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonAssignFollowerToBuilding); + DEFINE_HANDLER(CMSG_GARRISON_CANCEL_CONSTRUCTION, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonCancelConstruction); + DEFINE_HANDLER(CMSG_GARRISON_CHECK_UPGRADEABLE, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonCheckUpgradeable); + DEFINE_HANDLER(CMSG_GARRISON_COMPLETE_MISSION, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonCompleteMission); + DEFINE_HANDLER(CMSG_GARRISON_FULLY_HEAL_ALL_FOLLOWERS, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonFullyHealAllFollowers); + DEFINE_HANDLER(CMSG_GARRISON_GENERATE_RECRUITS, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonGenerateRecruits); + DEFINE_HANDLER(CMSG_GARRISON_GET_CLASS_SPEC_CATEGORY_INFO, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonGetClassSpecCategoryInfo); + DEFINE_HANDLER(CMSG_GARRISON_GET_MAP_DATA, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonGetMapData); + DEFINE_HANDLER(CMSG_GARRISON_GET_MISSION_REWARD, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonGetMissionReward); + DEFINE_HANDLER(CMSG_GARRISON_LEARN_TALENT, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonLearnTalent); + DEFINE_HANDLER(CMSG_GARRISON_MISSION_BONUS_ROLL, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonMissionBonusRoll); + DEFINE_HANDLER(CMSG_GARRISON_PURCHASE_BUILDING, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonPurchaseBuilding); + DEFINE_HANDLER(CMSG_GARRISON_RECRUIT_FOLLOWER, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonRecruitFollower); + DEFINE_HANDLER(CMSG_GARRISON_REMOVE_FOLLOWER, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonRemoveFollower); + DEFINE_HANDLER(CMSG_GARRISON_REMOVE_FOLLOWER_FROM_BUILDING, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonRemoveFollowerFromBuilding); + DEFINE_HANDLER(CMSG_GARRISON_RENAME_FOLLOWER, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonRenameFollower); + DEFINE_HANDLER(CMSG_GARRISON_REQUEST_BLUEPRINT_AND_SPECIALIZATION_DATA, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonRequestBlueprintAndSpecializationData); + DEFINE_HANDLER(CMSG_GARRISON_REQUEST_SHIPMENT_INFO, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonRequestShipmentInfo); + DEFINE_HANDLER(CMSG_GARRISON_RESEARCH_TALENT, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonResearchTalent); + DEFINE_HANDLER(CMSG_GARRISON_SET_BUILDING_ACTIVE, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonSetBuildingActive); + DEFINE_HANDLER(CMSG_GARRISON_SET_FOLLOWER_FAVORITE, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonSetFollowerFavorite); + DEFINE_HANDLER(CMSG_GARRISON_SET_FOLLOWER_INACTIVE, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonSetFollowerInactive); + DEFINE_HANDLER(CMSG_GARRISON_SET_RECRUITMENT_PREFERENCES, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonSetRecruitmentPreferences); + DEFINE_HANDLER(CMSG_GARRISON_SOCKET_TALENT, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonSocketTalent); + DEFINE_HANDLER(CMSG_GARRISON_START_MISSION, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonStartMission); + DEFINE_HANDLER(CMSG_GARRISON_SWAP_BUILDINGS, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGarrisonSwapBuildings); DEFINE_HANDLER(CMSG_GENERATE_RANDOM_CHARACTER_NAME, STATUS_AUTHED, PROCESS_THREADUNSAFE, &WorldSession::HandleRandomizeCharNameOpcode); DEFINE_HANDLER(CMSG_GET_ACCOUNT_CHARACTER_LIST, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL); DEFINE_HANDLER(CMSG_GET_ACCOUNT_NOTIFICATIONS, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL); @@ -1603,76 +1603,76 @@ void OpcodeTable::InitializeServerOpcodes() DEFINE_SERVER_OPCODE_HANDLER(SMSG_GAME_SPEED_SET, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GAME_TIME_SET, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GAME_TIME_UPDATE, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_ACTIVATE_MISSION_BONUS_ABILITY, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_ADD_EVENT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_ACTIVATE_MISSION_BONUS_ABILITY, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_ADD_EVENT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_ADD_FOLLOWER_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_ADD_MISSION_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_ADD_SPEC_GROUPS, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_APPLY_TALENT_SOCKET_DATA_CHANGES, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_ASSIGN_FOLLOWER_TO_BUILDING_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_AUTO_TROOP_MIN_LEVEL_UPDATE_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_ADD_MISSION_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_ADD_SPEC_GROUPS, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_APPLY_TALENT_SOCKET_DATA_CHANGES, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_ASSIGN_FOLLOWER_TO_BUILDING_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_AUTO_TROOP_MIN_LEVEL_UPDATE_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_BUILDING_ACTIVATED, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_BUILDING_REMOVED, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_BUILDING_SET_ACTIVE_SPECIALIZATION_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_CHANGE_MISSION_START_TIME_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_CLEAR_COLLECTION, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_CLEAR_EVENT_LIST, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_CLEAR_SPEC_GROUPS, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_COLLECTION_REMOVE_ENTRY, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_COLLECTION_UPDATE_ENTRY, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_COMPLETE_BUILDING_CONSTRUCTION_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_COMPLETE_MISSION_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_BUILDING_SET_ACTIVE_SPECIALIZATION_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_CHANGE_MISSION_START_TIME_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_CLEAR_COLLECTION, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_CLEAR_EVENT_LIST, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_CLEAR_SPEC_GROUPS, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_COLLECTION_REMOVE_ENTRY, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_COLLECTION_UPDATE_ENTRY, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_COMPLETE_BUILDING_CONSTRUCTION_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_COMPLETE_MISSION_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_CREATE_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_DELETE_MISSION_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_DELETE_MISSION_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_DELETE_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_FOLLOWER_ACTIVATIONS_SET, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_FOLLOWER_CHANGED_FLAGS, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_FOLLOWER_CHANGED_ITEM_LEVEL, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_FOLLOWER_CHANGED_QUALITY, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_FOLLOWER_CHANGED_XP, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_FOLLOWER_FATIGUE_CLEARED, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_GENERATE_FOLLOWERS_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_GET_CLASS_SPEC_CATEGORY_INFO_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_GET_RECALL_PORTAL_LAST_USED_TIME_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_IS_UPGRADEABLE_RESPONSE, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_FOLLOWER_ACTIVATIONS_SET, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_FOLLOWER_CHANGED_FLAGS, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_FOLLOWER_CHANGED_ITEM_LEVEL, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_FOLLOWER_CHANGED_QUALITY, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_FOLLOWER_CHANGED_XP, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_FOLLOWER_FATIGUE_CLEARED, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_GENERATE_FOLLOWERS_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_GET_CLASS_SPEC_CATEGORY_INFO_RESULT, STATUS_NEVER, CONNECTION_TYPE_REALM); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_GET_RECALL_PORTAL_LAST_USED_TIME_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_IS_UPGRADEABLE_RESPONSE, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_LEARN_BLUEPRINT_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_LEARN_SPECIALIZATION_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_LIST_COMPLETED_MISSIONS_CHEAT_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_LIST_FOLLOWERS_CHEAT_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_LEARN_SPECIALIZATION_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_LIST_COMPLETED_MISSIONS_CHEAT_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_LIST_FOLLOWERS_CHEAT_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_MAP_DATA_RESPONSE, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_MISSION_BONUS_ROLL_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_MISSION_REQUEST_REWARD_INFO_RESPONSE, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_MISSION_START_CONDITION_UPDATE, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_OPEN_CRAFTER, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_OPEN_RECRUITMENT_NPC, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_MISSION_BONUS_ROLL_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_MISSION_REQUEST_REWARD_INFO_RESPONSE, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_MISSION_START_CONDITION_UPDATE, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_OPEN_CRAFTER, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_OPEN_RECRUITMENT_NPC, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_PLACE_BUILDING_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_PLOT_PLACED, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_PLOT_REMOVED, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_RECRUIT_FOLLOWER_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_RECRUIT_FOLLOWER_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_REMOTE_INFO, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_REMOVE_EVENT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_REMOVE_FOLLOWER_ABILITY_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_REMOVE_FOLLOWER_FROM_BUILDING_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_REMOVE_EVENT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_REMOVE_FOLLOWER_ABILITY_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_REMOVE_FOLLOWER_FROM_BUILDING_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_REMOVE_FOLLOWER_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_RENAME_FOLLOWER_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_RENAME_FOLLOWER_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_REQUEST_BLUEPRINT_AND_SPECIALIZATION_DATA_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_RESEARCH_TALENT_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_RESET_TALENT_TREE, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_RESET_TALENT_TREE_SOCKET_DATA, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_START_MISSION_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_SWAP_BUILDINGS_RESPONSE, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_SWITCH_TALENT_TREE_BRANCH, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_TALENT_COMPLETED, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_TALENT_REMOVED, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_TALENT_REMOVE_SOCKET_DATA, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_TALENT_UPDATE_SOCKET_DATA, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_TALENT_WORLD_QUEST_UNLOCKS_RESPONSE, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_RESEARCH_TALENT_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_RESET_TALENT_TREE, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_RESET_TALENT_TREE_SOCKET_DATA, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_START_MISSION_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_SWAP_BUILDINGS_RESPONSE, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_SWITCH_TALENT_TREE_BRANCH, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_TALENT_COMPLETED, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_TALENT_REMOVED, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_TALENT_REMOVE_SOCKET_DATA, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_TALENT_UPDATE_SOCKET_DATA, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_TALENT_WORLD_QUEST_UNLOCKS_RESPONSE, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_UNLEARN_BLUEPRINT_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_UPDATE_FOLLOWER, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_UPDATE_GARRISON_MONUMENT_SELECTIONS, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_UPDATE_MISSION_CHEAT_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_UPGRADE_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_USE_RECALL_PORTAL_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_UPDATE_FOLLOWER, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_UPDATE_GARRISON_MONUMENT_SELECTIONS, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_UPDATE_MISSION_CHEAT_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_UPGRADE_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_GARRISON_USE_RECALL_PORTAL_RESULT, STATUS_NEVER, CONNECTION_TYPE_INSTANCE); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GENERATE_RANDOM_CHARACTER_NAME_RESULT, STATUS_NEVER, CONNECTION_TYPE_REALM); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GENERATE_SSO_TOKEN_RESPONSE, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); DEFINE_SERVER_OPCODE_HANDLER(SMSG_GET_ACCOUNT_CHARACTER_LIST_RESULT, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); diff --git a/src/server/game/Server/WorldSession.h b/src/server/game/Server/WorldSession.h index 307578d4b..7333110ed 100644 --- a/src/server/game/Server/WorldSession.h +++ b/src/server/game/Server/WorldSession.h @@ -381,6 +381,42 @@ namespace WorldPackets class GarrisonCancelConstruction; class GarrisonRequestBlueprintAndSpecializationData; class GarrisonGetMapData; + class GarrisonStartMission; + class GarrisonCompleteMission; + class GarrisonMissionBonusRoll; + class GarrisonGetMissionReward; + class OpenMissionNpc; + class UpgradeGarrison; + class GarrisonCheckUpgradeable; + class GarrisonSetBuildingActive; + class GarrisonSwapBuildings; + class GarrisonAssignFollowerToBuilding; + class GarrisonRemoveFollowerFromBuilding; + class GarrisonRemoveFollower; + class GarrisonRenameFollower; + class GarrisonSetFollowerFavorite; + class GarrisonSetFollowerInactive; + class GarrisonRecruitFollower; + class GarrisonGenerateRecruits; + class GarrisonFullyHealAllFollowers; + class GarrisonAddFollowerHealth; + class GarrisonGetClassSpecCategoryInfo; + class GarrisonSetRecruitmentPreferences; + class GarrisonLearnTalent; + class GarrisonResearchTalent; + class GarrisonSocketTalent; + class GarrisonRequestShipmentInfo; + class OpenShipmentNpc; + class CreateShipment; + class GetLandingPageShipments; + class SetUsingPartyGarrison; + class QueryGarrisonPetName; + class RequestGarrisonTalentWorldQuestUnlocks; + class GetTrophyList; + class ReplaceTrophy; + class LoadSelectedTrophy; + class ChangeMonumentAppearance; + class RevertMonumentAppearance; } namespace Guild @@ -2232,6 +2268,42 @@ class TC_GAME_API WorldSession void HandleGarrisonCancelConstruction(WorldPackets::Garrison::GarrisonCancelConstruction& garrisonCancelConstruction); void HandleGarrisonRequestBlueprintAndSpecializationData(WorldPackets::Garrison::GarrisonRequestBlueprintAndSpecializationData& garrisonRequestBlueprintAndSpecializationData); void HandleGarrisonGetMapData(WorldPackets::Garrison::GarrisonGetMapData& garrisonGetMapData); + void HandleGarrisonStartMission(WorldPackets::Garrison::GarrisonStartMission& garrisonStartMission); + void HandleGarrisonCompleteMission(WorldPackets::Garrison::GarrisonCompleteMission& garrisonCompleteMission); + void HandleGarrisonMissionBonusRoll(WorldPackets::Garrison::GarrisonMissionBonusRoll& garrisonMissionBonusRoll); + void HandleGarrisonGetMissionReward(WorldPackets::Garrison::GarrisonGetMissionReward& garrisonGetMissionReward); + void HandleOpenMissionNpc(WorldPackets::Garrison::OpenMissionNpc& openMissionNpc); + void HandleUpgradeGarrison(WorldPackets::Garrison::UpgradeGarrison& upgradeGarrison); + void HandleGarrisonCheckUpgradeable(WorldPackets::Garrison::GarrisonCheckUpgradeable& garrisonCheckUpgradeable); + void HandleGarrisonSetBuildingActive(WorldPackets::Garrison::GarrisonSetBuildingActive& garrisonSetBuildingActive); + void HandleGarrisonSwapBuildings(WorldPackets::Garrison::GarrisonSwapBuildings& garrisonSwapBuildings); + void HandleGarrisonAssignFollowerToBuilding(WorldPackets::Garrison::GarrisonAssignFollowerToBuilding& garrisonAssignFollowerToBuilding); + void HandleGarrisonRemoveFollowerFromBuilding(WorldPackets::Garrison::GarrisonRemoveFollowerFromBuilding& garrisonRemoveFollowerFromBuilding); + void HandleGarrisonRemoveFollower(WorldPackets::Garrison::GarrisonRemoveFollower& garrisonRemoveFollower); + void HandleGarrisonRenameFollower(WorldPackets::Garrison::GarrisonRenameFollower& garrisonRenameFollower); + void HandleGarrisonSetFollowerFavorite(WorldPackets::Garrison::GarrisonSetFollowerFavorite& garrisonSetFollowerFavorite); + void HandleGarrisonSetFollowerInactive(WorldPackets::Garrison::GarrisonSetFollowerInactive& garrisonSetFollowerInactive); + void HandleGarrisonRecruitFollower(WorldPackets::Garrison::GarrisonRecruitFollower& garrisonRecruitFollower); + void HandleGarrisonGenerateRecruits(WorldPackets::Garrison::GarrisonGenerateRecruits& garrisonGenerateRecruits); + void HandleGarrisonFullyHealAllFollowers(WorldPackets::Garrison::GarrisonFullyHealAllFollowers& garrisonFullyHealAllFollowers); + void HandleGarrisonAddFollowerHealth(WorldPackets::Garrison::GarrisonAddFollowerHealth& garrisonAddFollowerHealth); + void HandleGarrisonGetClassSpecCategoryInfo(WorldPackets::Garrison::GarrisonGetClassSpecCategoryInfo& garrisonGetClassSpecCategoryInfo); + void HandleGarrisonSetRecruitmentPreferences(WorldPackets::Garrison::GarrisonSetRecruitmentPreferences& garrisonSetRecruitmentPreferences); + void HandleGarrisonLearnTalent(WorldPackets::Garrison::GarrisonLearnTalent& garrisonLearnTalent); + void HandleGarrisonResearchTalent(WorldPackets::Garrison::GarrisonResearchTalent& garrisonResearchTalent); + void HandleGarrisonSocketTalent(WorldPackets::Garrison::GarrisonSocketTalent& garrisonSocketTalent); + void HandleGarrisonRequestShipmentInfo(WorldPackets::Garrison::GarrisonRequestShipmentInfo& garrisonRequestShipmentInfo); + void HandleOpenShipmentNpc(WorldPackets::Garrison::OpenShipmentNpc& openShipmentNpc); + void HandleCreateShipment(WorldPackets::Garrison::CreateShipment& createShipment); + void HandleGetLandingPageShipments(WorldPackets::Garrison::GetLandingPageShipments& getLandingPageShipments); + void HandleSetUsingPartyGarrison(WorldPackets::Garrison::SetUsingPartyGarrison& setUsingPartyGarrison); + void HandleQueryGarrisonPetName(WorldPackets::Garrison::QueryGarrisonPetName& queryGarrisonPetName); + void HandleRequestGarrisonTalentWorldQuestUnlocks(WorldPackets::Garrison::RequestGarrisonTalentWorldQuestUnlocks& requestGarrisonTalentWorldQuestUnlocks); + void HandleGetTrophyList(WorldPackets::Garrison::GetTrophyList& getTrophyList); + void HandleReplaceTrophy(WorldPackets::Garrison::ReplaceTrophy& replaceTrophy); + void HandleLoadSelectedTrophy(WorldPackets::Garrison::LoadSelectedTrophy& loadSelectedTrophy); + void HandleChangeMonumentAppearance(WorldPackets::Garrison::ChangeMonumentAppearance& changeMonumentAppearance); + void HandleRevertMonumentAppearance(WorldPackets::Garrison::RevertMonumentAppearance& revertMonumentAppearance); // Battle Pets void HandleBattlePetRequestJournal(WorldPackets::BattlePet::BattlePetRequestJournal& battlePetRequestJournal);