aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShauren <shauren.trinity@gmail.com>2022-12-16 22:44:55 +0100
committerShauren <shauren.trinity@gmail.com>2022-12-16 22:44:55 +0100
commit0cc5ab8372f19dad7412038d52dcd39db5e0e171 (patch)
tree99e621862e90fc9f073897445d4a8bdd4fbfa557
parent9be60f240960f6538329b5e017f435c6237a89ea (diff)
Core/Players: Implemented new talent system
-rw-r--r--sql/base/characters_database.sql63
-rw-r--r--sql/updates/characters/master/2022_12_16_00_characters.sql37
-rw-r--r--sql/updates/hotfixes/master/2022_12_16_01_hotfixes.sql382
-rw-r--r--src/server/database/Database/Implementation/CharacterDatabase.cpp18
-rw-r--r--src/server/database/Database/Implementation/CharacterDatabase.h10
-rw-r--r--src/server/database/Database/Implementation/HotfixDatabase.cpp121
-rw-r--r--src/server/database/Database/Implementation/HotfixDatabase.h74
-rw-r--r--src/server/game/Conditions/ConditionMgr.cpp12
-rw-r--r--src/server/game/DataStores/DB2LoadInfo.h406
-rw-r--r--src/server/game/DataStores/DB2Stores.cpp68
-rw-r--r--src/server/game/DataStores/DB2Stores.h27
-rw-r--r--src/server/game/DataStores/DB2Structure.h224
-rw-r--r--src/server/game/DataStores/DBCEnums.h80
-rw-r--r--src/server/game/Entities/Player/Player.cpp791
-rw-r--r--src/server/game/Entities/Player/Player.h37
-rw-r--r--src/server/game/Handlers/CharacterHandler.cpp12
-rw-r--r--src/server/game/Handlers/TraitHandler.cpp253
-rw-r--r--src/server/game/Server/Packets/AllPackets.h1
-rw-r--r--src/server/game/Server/Packets/PacketUtilities.h10
-rw-r--r--src/server/game/Server/Packets/TraitPackets.cpp68
-rw-r--r--src/server/game/Server/Packets/TraitPackets.h106
-rw-r--r--src/server/game/Server/Packets/TraitPacketsCommon.cpp68
-rw-r--r--src/server/game/Server/Packets/TraitPacketsCommon.h14
-rw-r--r--src/server/game/Server/Protocol/Opcodes.cpp14
-rw-r--r--src/server/game/Server/WorldSession.h17
-rw-r--r--src/server/game/Spells/Spell.cpp8
-rw-r--r--src/server/game/Spells/Spell.h2
-rw-r--r--src/server/game/Spells/SpellEffects.cpp40
-rw-r--r--src/server/game/Spells/TraitMgr.cpp735
-rw-r--r--src/server/game/Spells/TraitMgr.h57
-rw-r--r--src/server/game/World/World.cpp4
31 files changed, 3688 insertions, 71 deletions
diff --git a/sql/base/characters_database.sql b/sql/base/characters_database.sql
index 5e9ebc0b9c5..53120ed9e3b 100644
--- a/sql/base/characters_database.sql
+++ b/sql/base/characters_database.sql
@@ -458,10 +458,11 @@ DROP TABLE IF EXISTS `character_action`;
CREATE TABLE `character_action` (
`guid` bigint unsigned NOT NULL DEFAULT '0',
`spec` tinyint unsigned NOT NULL DEFAULT '0',
+ `traitConfigId` int NOT NULL DEFAULT '0',
`button` tinyint unsigned NOT NULL DEFAULT '0',
`action` bigint unsigned NOT NULL DEFAULT '0',
`type` tinyint unsigned NOT NULL DEFAULT '0',
- PRIMARY KEY (`guid`,`spec`,`button`)
+ PRIMARY KEY (`guid`,`spec`,`traitConfigId`,`button`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
@@ -1700,6 +1701,63 @@ LOCK TABLES `character_talent` WRITE;
UNLOCK TABLES;
--
+-- Table structure for table `character_trait_config`
+--
+
+DROP TABLE IF EXISTS `character_trait_config`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `character_trait_config` (
+ `guid` bigint unsigned NOT NULL,
+ `traitConfigId` int NOT NULL,
+ `type` int NOT NULL,
+ `chrSpecializationId` int DEFAULT NULL,
+ `combatConfigFlags` int DEFAULT NULL,
+ `localIdentifier` int DEFAULT NULL,
+ `skillLineId` int DEFAULT NULL,
+ `traitSystemId` int DEFAULT NULL,
+ `name` varchar(260) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
+ PRIMARY KEY (`guid`,`traitConfigId`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `character_trait_config`
+--
+
+LOCK TABLES `character_trait_config` WRITE;
+/*!40000 ALTER TABLE `character_trait_config` DISABLE KEYS */;
+/*!40000 ALTER TABLE `character_trait_config` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
+-- Table structure for table `character_trait_entry`
+--
+
+DROP TABLE IF EXISTS `character_trait_entry`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `character_trait_entry` (
+ `guid` bigint unsigned NOT NULL,
+ `traitConfigId` int NOT NULL,
+ `traitNodeId` int NOT NULL,
+ `traitNodeEntryId` int NOT NULL,
+ `rank` int NOT NULL DEFAULT '0',
+ `grantedRanks` int NOT NULL DEFAULT '0',
+ PRIMARY KEY (`guid`,`traitConfigId`,`traitNodeId`,`traitNodeEntryId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `character_trait_entry`
+--
+
+LOCK TABLES `character_trait_entry` WRITE;
+/*!40000 ALTER TABLE `character_trait_entry` DISABLE KEYS */;
+/*!40000 ALTER TABLE `character_trait_entry` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
-- Table structure for table `character_transmog_outfits`
--
@@ -3606,7 +3664,8 @@ INSERT INTO `updates` VALUES
('2022_10_03_00_characters.sql','7B062787230D9158A622EB4AFE7FA6D18AB47BB3','ARCHIVED','2022-10-03 22:32:58',0),
('2022_10_03_01_characters.sql','7CF58BD9CC366301CC992017028568C8774C4BC2','ARCHIVED','2022-10-03 22:36:38',0),
('2022_10_03_02_characters.sql','33135AB3132943F15F4849A16EC5EFEA402F24F6','ARCHIVED','2022-10-03 22:38:27',0),
-('2022_11_20_00_characters.sql','4EB8BB24CAF16B0962DF3EF92C77BE05E234CFA6','ARCHIVED','2022-11-20 11:05:20',0);
+('2022_11_20_00_characters.sql','4EB8BB24CAF16B0962DF3EF92C77BE05E234CFA6','ARCHIVED','2022-11-20 11:05:20',0),
+('2022_12_16_00_characters.sql','ABD1E101FE6629E0520C91E98942E55067EDD492','RELEASED','2022-12-16 22:52:19',0);
/*!40000 ALTER TABLE `updates` ENABLE KEYS */;
UNLOCK TABLES;
diff --git a/sql/updates/characters/master/2022_12_16_00_characters.sql b/sql/updates/characters/master/2022_12_16_00_characters.sql
index 8a0dd2989cd..654062fc6ca 100644
--- a/sql/updates/characters/master/2022_12_16_00_characters.sql
+++ b/sql/updates/characters/master/2022_12_16_00_characters.sql
@@ -8,3 +8,40 @@ ALTER TABLE `character_inventory` ADD UNIQUE KEY `uk_location` (`guid`,`bag`,`sl
ALTER TABLE `character_inventory` DROP `newSlot`;
UPDATE `characters` SET `equipmentCache`=CONCAT(`equipmentCache`, '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ');
+
+--
+-- Table structure for table `character_trait_entry`
+--
+DROP TABLE IF EXISTS `character_trait_entry`;
+CREATE TABLE `character_trait_entry` (
+ `guid` bigint unsigned NOT NULL,
+ `traitConfigId` int NOT NULL,
+ `traitNodeId` int NOT NULL,
+ `traitNodeEntryId` int NOT NULL,
+ `rank` int NOT NULL DEFAULT '0',
+ `grantedRanks` int NOT NULL DEFAULT '0',
+ PRIMARY KEY (`guid`,`traitConfigId`,`traitNodeId`,`traitNodeEntryId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `character_trait_config`
+--
+DROP TABLE IF EXISTS `character_trait_config`;
+CREATE TABLE `character_trait_config` (
+ `guid` bigint unsigned NOT NULL,
+ `traitConfigId` int NOT NULL,
+ `type` int NOT NULL,
+ `chrSpecializationId` int DEFAULT NULL,
+ `combatConfigFlags` int DEFAULT NULL,
+ `localIdentifier` int DEFAULT NULL,
+ `skillLineId` int DEFAULT NULL,
+ `traitSystemId` int DEFAULT NULL,
+ `name` varchar(260) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
+ PRIMARY KEY (`guid`,`traitConfigId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+ALTER TABLE `character_action` ADD `traitConfigId` int NOT NULL DEFAULT 0 AFTER `spec`;
+ALTER TABLE `character_action` DROP PRIMARY KEY;
+ALTER TABLE `character_action` ADD PRIMARY KEY (`guid`,`spec`,`traitConfigId`,`button`);
+
+DELETE FROM `character_talent`;
diff --git a/sql/updates/hotfixes/master/2022_12_16_01_hotfixes.sql b/sql/updates/hotfixes/master/2022_12_16_01_hotfixes.sql
new file mode 100644
index 00000000000..a76ff3ea1a1
--- /dev/null
+++ b/sql/updates/hotfixes/master/2022_12_16_01_hotfixes.sql
@@ -0,0 +1,382 @@
+--
+-- Table structure for table `skill_line_x_trait_tree`
+--
+DROP TABLE IF EXISTS `skill_line_x_trait_tree`;
+CREATE TABLE `skill_line_x_trait_tree` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `SkillLineID` int(11) NOT NULL DEFAULT '0',
+ `TraitTreeID` int(11) NOT NULL DEFAULT '0',
+ `OrderIndex` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_cond`
+--
+DROP TABLE IF EXISTS `trait_cond`;
+CREATE TABLE `trait_cond` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `CondType` int(11) NOT NULL DEFAULT '0',
+ `TraitTreeID` int(11) NOT NULL DEFAULT '0',
+ `GrantedRanks` int(11) NOT NULL DEFAULT '0',
+ `QuestID` int(11) NOT NULL DEFAULT '0',
+ `AchievementID` int(11) NOT NULL DEFAULT '0',
+ `SpecSetID` int(11) NOT NULL DEFAULT '0',
+ `TraitNodeGroupID` int(11) NOT NULL DEFAULT '0',
+ `TraitNodeID` int(11) NOT NULL DEFAULT '0',
+ `TraitCurrencyID` int(11) NOT NULL DEFAULT '0',
+ `SpentAmountRequired` int(11) NOT NULL DEFAULT '0',
+ `Flags` int(11) NOT NULL DEFAULT '0',
+ `RequiredLevel` int(11) NOT NULL DEFAULT '0',
+ `FreeSharedStringID` int(11) NOT NULL DEFAULT '0',
+ `SpendMoreSharedStringID` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_cost`
+--
+DROP TABLE IF EXISTS `trait_cost`;
+CREATE TABLE `trait_cost` (
+ `InternalName` text,
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `Amount` int(11) NOT NULL DEFAULT '0',
+ `TraitCurrencyID` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_currency`
+--
+DROP TABLE IF EXISTS `trait_currency`;
+CREATE TABLE `trait_currency` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `Type` int(11) NOT NULL DEFAULT '0',
+ `CurrencyTypesID` int(11) NOT NULL DEFAULT '0',
+ `Flags` int(11) NOT NULL DEFAULT '0',
+ `Icon` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_currency_source`
+--
+DROP TABLE IF EXISTS `trait_currency_source`;
+CREATE TABLE `trait_currency_source` (
+ `Requirement` text,
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitCurrencyID` int(11) NOT NULL DEFAULT '0',
+ `Amount` int(11) NOT NULL DEFAULT '0',
+ `QuestID` int(11) NOT NULL DEFAULT '0',
+ `AchievementID` int(11) NOT NULL DEFAULT '0',
+ `PlayerLevel` int(11) NOT NULL DEFAULT '0',
+ `TraitNodeEntryID` int(11) NOT NULL DEFAULT '0',
+ `OrderIndex` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_currency_source_locale`
+--
+DROP TABLE IF EXISTS `trait_currency_source_locale`;
+CREATE TABLE `trait_currency_source_locale` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `locale` varchar(4) NOT NULL,
+ `Requirement_lang` text,
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`locale`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+PARTITION BY LIST COLUMNS(locale)
+(PARTITION deDE VALUES IN ('deDE') ENGINE = InnoDB,
+ PARTITION esES VALUES IN ('esES') ENGINE = InnoDB,
+ PARTITION esMX VALUES IN ('esMX') ENGINE = InnoDB,
+ PARTITION frFR VALUES IN ('frFR') ENGINE = InnoDB,
+ PARTITION itIT VALUES IN ('itIT') ENGINE = InnoDB,
+ PARTITION koKR VALUES IN ('koKR') ENGINE = InnoDB,
+ PARTITION ptBR VALUES IN ('ptBR') ENGINE = InnoDB,
+ PARTITION ruRU VALUES IN ('ruRU') ENGINE = InnoDB,
+ PARTITION zhCN VALUES IN ('zhCN') ENGINE = InnoDB,
+ PARTITION zhTW VALUES IN ('zhTW') ENGINE = InnoDB);
+
+--
+-- Table structure for table `trait_definition`
+--
+DROP TABLE IF EXISTS `trait_definition`;
+CREATE TABLE `trait_definition` (
+ `OverrideName` text,
+ `OverrideSubtext` text,
+ `OverrideDescription` text,
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `SpellID` int(11) NOT NULL DEFAULT '0',
+ `OverrideIcon` int(11) NOT NULL DEFAULT '0',
+ `OverridesSpellID` int(11) NOT NULL DEFAULT '0',
+ `VisibleSpellID` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_definition_locale`
+--
+DROP TABLE IF EXISTS `trait_definition_locale`;
+CREATE TABLE `trait_definition_locale` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `locale` varchar(4) NOT NULL,
+ `OverrideName_lang` text,
+ `OverrideSubtext_lang` text,
+ `OverrideDescription_lang` text,
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`locale`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+PARTITION BY LIST COLUMNS(locale)
+(PARTITION deDE VALUES IN ('deDE') ENGINE = InnoDB,
+ PARTITION esES VALUES IN ('esES') ENGINE = InnoDB,
+ PARTITION esMX VALUES IN ('esMX') ENGINE = InnoDB,
+ PARTITION frFR VALUES IN ('frFR') ENGINE = InnoDB,
+ PARTITION itIT VALUES IN ('itIT') ENGINE = InnoDB,
+ PARTITION koKR VALUES IN ('koKR') ENGINE = InnoDB,
+ PARTITION ptBR VALUES IN ('ptBR') ENGINE = InnoDB,
+ PARTITION ruRU VALUES IN ('ruRU') ENGINE = InnoDB,
+ PARTITION zhCN VALUES IN ('zhCN') ENGINE = InnoDB,
+ PARTITION zhTW VALUES IN ('zhTW') ENGINE = InnoDB);
+
+--
+-- Table structure for table `trait_definition_effect_points`
+--
+DROP TABLE IF EXISTS `trait_definition_effect_points`;
+CREATE TABLE `trait_definition_effect_points` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitDefinitionID` int(11) NOT NULL DEFAULT '0',
+ `EffectIndex` int(11) NOT NULL DEFAULT '0',
+ `OperationType` int(11) NOT NULL DEFAULT '0',
+ `CurveID` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_edge`
+--
+DROP TABLE IF EXISTS `trait_edge`;
+CREATE TABLE `trait_edge` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `VisualStyle` int(11) NOT NULL DEFAULT '0',
+ `LeftTraitNodeID` int(11) NOT NULL DEFAULT '0',
+ `RightTraitNodeID` int(11) NOT NULL DEFAULT '0',
+ `Type` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_node`
+--
+DROP TABLE IF EXISTS `trait_node`;
+CREATE TABLE `trait_node` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitTreeID` int(11) NOT NULL DEFAULT '0',
+ `PosX` int(11) NOT NULL DEFAULT '0',
+ `PosY` int(11) NOT NULL DEFAULT '0',
+ `Type` tinyint(4) NOT NULL DEFAULT '0',
+ `Flags` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_node_entry`
+--
+DROP TABLE IF EXISTS `trait_node_entry`;
+CREATE TABLE `trait_node_entry` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitDefinitionID` int(11) NOT NULL DEFAULT '0',
+ `MaxRanks` int(11) NOT NULL DEFAULT '0',
+ `NodeEntryType` tinyint(3) unsigned NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_node_entry_x_trait_cond`
+--
+DROP TABLE IF EXISTS `trait_node_entry_x_trait_cond`;
+CREATE TABLE `trait_node_entry_x_trait_cond` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitCondID` int(11) NOT NULL DEFAULT '0',
+ `TraitNodeEntryID` int(10) unsigned NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_node_entry_x_trait_cost`
+--
+DROP TABLE IF EXISTS `trait_node_entry_x_trait_cost`;
+CREATE TABLE `trait_node_entry_x_trait_cost` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitNodeEntryID` int(11) NOT NULL DEFAULT '0',
+ `TraitCostID` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_node_group`
+--
+DROP TABLE IF EXISTS `trait_node_group`;
+CREATE TABLE `trait_node_group` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitTreeID` int(11) NOT NULL DEFAULT '0',
+ `Flags` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_node_group_x_trait_cond`
+--
+DROP TABLE IF EXISTS `trait_node_group_x_trait_cond`;
+CREATE TABLE `trait_node_group_x_trait_cond` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitCondID` int(11) NOT NULL DEFAULT '0',
+ `TraitNodeGroupID` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_node_group_x_trait_cost`
+--
+DROP TABLE IF EXISTS `trait_node_group_x_trait_cost`;
+CREATE TABLE `trait_node_group_x_trait_cost` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitNodeGroupID` int(11) NOT NULL DEFAULT '0',
+ `TraitCostID` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_node_group_x_trait_node`
+--
+DROP TABLE IF EXISTS `trait_node_group_x_trait_node`;
+CREATE TABLE `trait_node_group_x_trait_node` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitNodeGroupID` int(11) NOT NULL DEFAULT '0',
+ `TraitNodeID` int(11) NOT NULL DEFAULT '0',
+ `Index` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_node_x_trait_cond`
+--
+DROP TABLE IF EXISTS `trait_node_x_trait_cond`;
+CREATE TABLE `trait_node_x_trait_cond` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitCondID` int(11) NOT NULL DEFAULT '0',
+ `TraitNodeID` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_node_x_trait_cost`
+--
+DROP TABLE IF EXISTS `trait_node_x_trait_cost`;
+CREATE TABLE `trait_node_x_trait_cost` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitNodeID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitCostID` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_node_x_trait_node_entry`
+--
+DROP TABLE IF EXISTS `trait_node_x_trait_node_entry`;
+CREATE TABLE `trait_node_x_trait_node_entry` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitNodeID` int(11) NOT NULL DEFAULT '0',
+ `TraitNodeEntryID` int(11) NOT NULL DEFAULT '0',
+ `Index` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_tree`
+--
+DROP TABLE IF EXISTS `trait_tree`;
+CREATE TABLE `trait_tree` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitSystemID` int(11) NOT NULL DEFAULT '0',
+ `Unused1000_1` int(11) NOT NULL DEFAULT '0',
+ `FirstTraitNodeID` int(11) NOT NULL DEFAULT '0',
+ `PlayerConditionID` int(11) NOT NULL DEFAULT '0',
+ `Flags` int(11) NOT NULL DEFAULT '0',
+ `Unused1000_2` float NOT NULL DEFAULT '0',
+ `Unused1000_3` float NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_tree_loadout`
+--
+DROP TABLE IF EXISTS `trait_tree_loadout`;
+CREATE TABLE `trait_tree_loadout` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitTreeID` int(11) NOT NULL DEFAULT '0',
+ `ChrSpecializationID` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+
+--
+-- Table structure for table `trait_tree_loadout_entry`
+--
+DROP TABLE IF EXISTS `trait_tree_loadout_entry`;
+CREATE TABLE `trait_tree_loadout_entry` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitTreeLoadoutID` int(11) NOT NULL DEFAULT '0',
+ `SelectedTraitNodeID` int(11) NOT NULL DEFAULT '0',
+ `SelectedTraitNodeEntryID` int(11) NOT NULL DEFAULT '0',
+ `NumPoints` int(11) NOT NULL DEFAULT '0',
+ `OrderIndex` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_tree_x_trait_cost`
+--
+DROP TABLE IF EXISTS `trait_tree_x_trait_cost`;
+CREATE TABLE `trait_tree_x_trait_cost` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitTreeID` int(10) unsigned NOT NULL DEFAULT '0',
+ `TraitCostID` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `trait_tree_x_trait_currency`
+--
+DROP TABLE IF EXISTS `trait_tree_x_trait_currency`;
+CREATE TABLE `trait_tree_x_trait_currency` (
+ `ID` int(10) unsigned NOT NULL DEFAULT '0',
+ `Index` int(11) NOT NULL DEFAULT '0',
+ `TraitTreeID` int(11) NOT NULL DEFAULT '0',
+ `TraitCurrencyID` int(11) NOT NULL DEFAULT '0',
+ `VerifiedBuild` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp
index 5d448f35a68..b3032c7a795 100644
--- a/src/server/database/Database/Implementation/CharacterDatabase.cpp
+++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp
@@ -119,7 +119,6 @@ void CharacterDatabaseConnection::DoPrepareStatements()
PrepareStatement(CHAR_SEL_CHARACTER_REPUTATION, "SELECT faction, standing, flags FROM character_reputation WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_CHARACTER_INVENTORY, "SELECT " SelectItemInstanceContent ", bag, slot FROM character_inventory ci JOIN item_instance ii ON ci.item = ii.guid LEFT JOIN item_instance_gems ig ON ii.guid = ig.itemGuid LEFT JOIN item_instance_transmog iit ON ii.guid = iit.itemGuid LEFT JOIN item_instance_modifiers im ON ii.guid = im.itemGuid WHERE ci.guid = ? ORDER BY (ii.flags & 0x80000) ASC, bag ASC, slot ASC", CONNECTION_ASYNC);
- PrepareStatement(CHAR_SEL_CHARACTER_ACTIONS, "SELECT a.button, a.action, a.type FROM character_action as a, characters as c WHERE a.guid = c.guid AND a.spec = c.activeTalentGroup AND a.guid = ? ORDER BY button", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_MAIL_COUNT, "SELECT COUNT(*) FROM mail WHERE receiver = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_CHARACTER_SOCIALLIST, "SELECT cs.friend, c.account, cs.flags, cs.note FROM character_social cs JOIN characters c ON c.guid = cs.friend WHERE cs.guid = ? AND c.deleteinfos_name IS NULL LIMIT 255", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_CHARACTER_HOMEBIND, "SELECT mapId, zoneId, posX, posY, posZ, orientation FROM character_homebind WHERE guid = ?", CONNECTION_ASYNC);
@@ -151,7 +150,7 @@ void CharacterDatabaseConnection::DoPrepareStatements()
PrepareStatement(CHAR_DEL_CHARACTER_FAVORITE_AUCTIONS_BY_CHAR, "DELETE FROM character_favorite_auctions WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_ACCOUNT_INSTANCELOCKTIMES, "SELECT instanceId, releaseTime FROM account_instance_times WHERE accountId = ?", CONNECTION_ASYNC);
- PrepareStatement(CHAR_SEL_CHARACTER_ACTIONS_SPEC, "SELECT button, action, type FROM character_action WHERE guid = ? AND spec = ? ORDER BY button", CONNECTION_ASYNC);
+ PrepareStatement(CHAR_SEL_CHARACTER_ACTIONS_SPEC, "SELECT button, action, type FROM character_action WHERE guid = ? AND spec = ? AND traitConfigId = ? ORDER BY button", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_MAILITEMS, "SELECT " SelectItemInstanceContent ", ii.owner_guid, m.id FROM mail_items mi INNER JOIN mail m ON mi.mail_id = m.id LEFT JOIN item_instance ii ON mi.item_guid = ii.guid LEFT JOIN item_instance_gems ig ON ii.guid = ig.itemGuid LEFT JOIN item_instance_transmog iit ON ii.guid = iit.itemGuid LEFT JOIN item_instance_modifiers im ON ii.guid = im.itemGuid WHERE m.receiver = ?", CONNECTION_BOTH);
PrepareStatement(CHAR_SEL_MAILITEMS_ARTIFACT, "SELECT a.itemGuid, a.xp, a.artifactAppearanceId, a.artifactTierId, ap.artifactPowerId, ap.purchasedRank FROM item_instance_artifact_powers ap LEFT JOIN item_instance_artifact a ON ap.itemGuid = a.itemGuid INNER JOIN mail_items mi ON a.itemGuid = mi.item_guid INNER JOIN mail m ON mi.mail_id = m.id WHERE m.receiver = ?", CONNECTION_BOTH);
PrepareStatement(CHAR_SEL_MAILITEMS_AZERITE, "SELECT iz.itemGuid, iz.xp, iz.level, iz.knowledgeLevel, "
@@ -616,9 +615,10 @@ void CharacterDatabaseConnection::DoPrepareStatements()
PrepareStatement(CHAR_DEL_CHAR_PVP_TALENT, "DELETE FROM character_pvp_talent WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_DEL_CHAR_SKILLS, "DELETE FROM character_skills WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_UPD_CHAR_MONEY, "UPDATE characters SET money = ? WHERE guid = ?", CONNECTION_ASYNC);
- PrepareStatement(CHAR_INS_CHAR_ACTION, "INSERT INTO character_action (guid, spec, button, action, type) VALUES (?, ?, ?, ?, ?)", CONNECTION_ASYNC);
- PrepareStatement(CHAR_UPD_CHAR_ACTION, "UPDATE character_action SET action = ?, type = ? WHERE guid = ? AND button = ? AND spec = ?", CONNECTION_ASYNC);
- PrepareStatement(CHAR_DEL_CHAR_ACTION_BY_BUTTON_SPEC, "DELETE FROM character_action WHERE guid = ? and button = ? and spec = ?", CONNECTION_ASYNC);
+ PrepareStatement(CHAR_INS_CHAR_ACTION, "INSERT INTO character_action (guid, spec, traitConfigId, button, action, type) VALUES (?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC);
+ PrepareStatement(CHAR_UPD_CHAR_ACTION, "UPDATE character_action SET action = ?, type = ? WHERE guid = ? AND button = ? AND spec = ? AND traitConfigId = ?", CONNECTION_ASYNC);
+ PrepareStatement(CHAR_DEL_CHAR_ACTION_BY_BUTTON_SPEC, "DELETE FROM character_action WHERE guid = ? and button = ? and spec = ? AND traitConfigId = ?", CONNECTION_ASYNC);
+ PrepareStatement(CHAR_DEL_CHAR_ACTION_BY_TRAIT_CONFIG, "DELETE FROM character_action WHERE guid = ? AND traitConfigId = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_DEL_CHAR_INVENTORY_BY_ITEM, "DELETE FROM character_inventory WHERE item = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_DEL_CHAR_INVENTORY_BY_BAG_SLOT, "DELETE FROM character_inventory WHERE bag = ? AND slot = ? AND guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_UPD_MAIL, "UPDATE mail SET has_items = ?, expire_time = ?, deliver_time = ?, money = ?, cod = ?, checked = ? WHERE id = ?", CONNECTION_ASYNC);
@@ -650,6 +650,14 @@ void CharacterDatabaseConnection::DoPrepareStatements()
PrepareStatement(CHAR_UPD_CHAR_LIST_SLOT, "UPDATE characters SET slot = ? WHERE guid = ? AND account = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_INS_CHAR_FISHINGSTEPS, "INSERT INTO character_fishingsteps (guid, fishingSteps) VALUES (?, ?)", CONNECTION_ASYNC);
PrepareStatement(CHAR_DEL_CHAR_FISHINGSTEPS, "DELETE FROM character_fishingsteps WHERE guid = ?", CONNECTION_ASYNC);
+ PrepareStatement(CHAR_SEL_CHAR_TRAIT_ENTRIES, "SELECT traitConfigId, traitNodeId, traitNodeEntryId, `rank`, grantedRanks FROM character_trait_entry WHERE guid = ?", CONNECTION_ASYNC);
+ PrepareStatement(CHAR_INS_CHAR_TRAIT_ENTRIES, "INSERT INTO character_trait_entry (guid, traitConfigId, traitNodeId, traitNodeEntryId, `rank`, grantedRanks) VALUES (?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC);
+ PrepareStatement(CHAR_DEL_CHAR_TRAIT_ENTRIES, "DELETE FROM character_trait_entry WHERE guid = ? AND traitConfigId = ?", CONNECTION_ASYNC);
+ PrepareStatement(CHAR_DEL_CHAR_TRAIT_ENTRIES_BY_CHAR, "DELETE FROM character_trait_entry WHERE guid = ?", CONNECTION_ASYNC);
+ PrepareStatement(CHAR_SEL_CHAR_TRAIT_CONFIGS, "SELECT traitConfigId, type, chrSpecializationId, combatConfigFlags, localIdentifier, skillLineId, traitSystemId, `name` FROM character_trait_config WHERE guid = ?", CONNECTION_ASYNC);
+ PrepareStatement(CHAR_INS_CHAR_TRAIT_CONFIGS, "INSERT INTO character_trait_config (guid, traitConfigId, type, chrSpecializationId, combatConfigFlags, localIdentifier, skillLineId, traitSystemId, `name`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC);
+ PrepareStatement(CHAR_DEL_CHAR_TRAIT_CONFIGS, "DELETE FROM character_trait_config WHERE guid = ? AND traitConfigId = ?", CONNECTION_ASYNC);
+ PrepareStatement(CHAR_DEL_CHAR_TRAIT_CONFIGS_BY_CHAR, "DELETE FROM character_trait_config WHERE guid = ?", CONNECTION_ASYNC);
// Void Storage
PrepareStatement(CHAR_SEL_CHAR_VOID_STORAGE, "SELECT itemId, itemEntry, slot, creatorGuid, randomBonusListId, fixedScalingLevel, artifactKnowledgeLevel, context, bonusListIDs FROM character_void_storage WHERE playerGuid = ?", CONNECTION_ASYNC);
diff --git a/src/server/database/Database/Implementation/CharacterDatabase.h b/src/server/database/Database/Implementation/CharacterDatabase.h
index c0579fe6a3d..5c19d4780ea 100644
--- a/src/server/database/Database/Implementation/CharacterDatabase.h
+++ b/src/server/database/Database/Implementation/CharacterDatabase.h
@@ -91,7 +91,6 @@ enum CharacterDatabaseStatements : uint32
CHAR_SEL_CHARACTER_REPUTATION,
CHAR_SEL_CHARACTER_INVENTORY,
- CHAR_SEL_CHARACTER_ACTIONS,
CHAR_SEL_CHARACTER_ACTIONS_SPEC,
CHAR_SEL_MAIL_COUNT,
CHAR_SEL_CHARACTER_SOCIALLIST,
@@ -503,6 +502,7 @@ enum CharacterDatabaseStatements : uint32
CHAR_INS_CHAR_ACTION,
CHAR_UPD_CHAR_ACTION,
CHAR_DEL_CHAR_ACTION_BY_BUTTON_SPEC,
+ CHAR_DEL_CHAR_ACTION_BY_TRAIT_CONFIG,
CHAR_DEL_CHAR_INVENTORY_BY_ITEM,
CHAR_DEL_CHAR_INVENTORY_BY_BAG_SLOT,
CHAR_UPD_MAIL,
@@ -532,6 +532,14 @@ enum CharacterDatabaseStatements : uint32
CHAR_UPD_CHAR_LIST_SLOT,
CHAR_INS_CHAR_FISHINGSTEPS,
CHAR_DEL_CHAR_FISHINGSTEPS,
+ CHAR_SEL_CHAR_TRAIT_ENTRIES,
+ CHAR_INS_CHAR_TRAIT_ENTRIES,
+ CHAR_DEL_CHAR_TRAIT_ENTRIES,
+ CHAR_DEL_CHAR_TRAIT_ENTRIES_BY_CHAR,
+ CHAR_SEL_CHAR_TRAIT_CONFIGS,
+ CHAR_INS_CHAR_TRAIT_CONFIGS,
+ CHAR_DEL_CHAR_TRAIT_CONFIGS,
+ CHAR_DEL_CHAR_TRAIT_CONFIGS_BY_CHAR,
CHAR_SEL_CHAR_VOID_STORAGE,
CHAR_REP_CHAR_VOID_STORAGE_ITEM,
diff --git a/src/server/database/Database/Implementation/HotfixDatabase.cpp b/src/server/database/Database/Implementation/HotfixDatabase.cpp
index 88b8ad18193..f6e3f0ec08f 100644
--- a/src/server/database/Database/Implementation/HotfixDatabase.cpp
+++ b/src/server/database/Database/Implementation/HotfixDatabase.cpp
@@ -1346,6 +1346,11 @@ void HotfixDatabaseConnection::DoPrepareStatements()
PREPARE_LOCALE_STMT(HOTFIX_SEL_SKILL_LINE_ABILITY, "SELECT ID, AbilityVerb_lang, AbilityAllVerb_lang FROM skill_line_ability_locale"
" WHERE (`VerifiedBuild` > 0) = ? AND locale = ?", CONNECTION_SYNCH);
+ // SkillLineXTraitTree.db2
+ PrepareStatement(HOTFIX_SEL_SKILL_LINE_X_TRAIT_TREE, "SELECT ID, SkillLineID, TraitTreeID, OrderIndex FROM skill_line_x_trait_tree"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_SKILL_LINE_X_TRAIT_TREE, "SELECT MAX(ID) + 1 FROM skill_line_x_trait_tree", CONNECTION_SYNCH);
+
// SkillRaceClassInfo.db2
PrepareStatement(HOTFIX_SEL_SKILL_RACE_CLASS_INFO, "SELECT ID, RaceMask, SkillID, ClassMask, Flags, Availability, MinLevel, SkillTierID"
" FROM skill_race_class_info WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
@@ -1640,6 +1645,122 @@ void HotfixDatabaseConnection::DoPrepareStatements()
PrepareStatement(HOTFIX_SEL_TRANSMOG_HOLIDAY, "SELECT ID, RequiredTransmogHoliday FROM transmog_holiday WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRANSMOG_HOLIDAY, "SELECT MAX(ID) + 1 FROM transmog_holiday", CONNECTION_SYNCH);
+ // TraitCond.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_COND, "SELECT ID, CondType, TraitTreeID, GrantedRanks, QuestID, AchievementID, SpecSetID, TraitNodeGroupID, "
+ "TraitNodeID, TraitCurrencyID, SpentAmountRequired, Flags, RequiredLevel, FreeSharedStringID, SpendMoreSharedStringID FROM trait_cond"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_COND, "SELECT MAX(ID) + 1 FROM trait_cond", CONNECTION_SYNCH);
+
+ // TraitCost.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_COST, "SELECT InternalName, ID, Amount, TraitCurrencyID FROM trait_cost WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_COST, "SELECT MAX(ID) + 1 FROM trait_cost", CONNECTION_SYNCH);
+
+ // TraitCurrency.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_CURRENCY, "SELECT ID, Type, CurrencyTypesID, Flags, Icon FROM trait_currency WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_CURRENCY, "SELECT MAX(ID) + 1 FROM trait_currency", CONNECTION_SYNCH);
+
+ // TraitCurrencySource.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_CURRENCY_SOURCE, "SELECT Requirement, ID, TraitCurrencyID, Amount, QuestID, AchievementID, PlayerLevel, "
+ "TraitNodeEntryID, OrderIndex FROM trait_currency_source WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_CURRENCY_SOURCE, "SELECT MAX(ID) + 1 FROM trait_currency_source", CONNECTION_SYNCH);
+ PREPARE_LOCALE_STMT(HOTFIX_SEL_TRAIT_CURRENCY_SOURCE, "SELECT ID, Requirement_lang FROM trait_currency_source_locale"
+ " WHERE (`VerifiedBuild` > 0) = ? AND locale = ?", CONNECTION_SYNCH);
+
+ // TraitDefinition.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_DEFINITION, "SELECT OverrideName, OverrideSubtext, OverrideDescription, ID, SpellID, OverrideIcon, "
+ "OverridesSpellID, VisibleSpellID FROM trait_definition WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_DEFINITION, "SELECT MAX(ID) + 1 FROM trait_definition", CONNECTION_SYNCH);
+ PREPARE_LOCALE_STMT(HOTFIX_SEL_TRAIT_DEFINITION, "SELECT ID, OverrideName_lang, OverrideSubtext_lang, OverrideDescription_lang"
+ " FROM trait_definition_locale WHERE (`VerifiedBuild` > 0) = ? AND locale = ?", CONNECTION_SYNCH);
+
+ // TraitDefinitionEffectPoints.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_DEFINITION_EFFECT_POINTS, "SELECT ID, TraitDefinitionID, EffectIndex, OperationType, CurveID"
+ " FROM trait_definition_effect_points WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_DEFINITION_EFFECT_POINTS, "SELECT MAX(ID) + 1 FROM trait_definition_effect_points", CONNECTION_SYNCH);
+
+ // TraitEdge.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_EDGE, "SELECT ID, VisualStyle, LeftTraitNodeID, RightTraitNodeID, Type FROM trait_edge"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_EDGE, "SELECT MAX(ID) + 1 FROM trait_edge", CONNECTION_SYNCH);
+
+ // TraitNode.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_NODE, "SELECT ID, TraitTreeID, PosX, PosY, Type, Flags FROM trait_node WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_NODE, "SELECT MAX(ID) + 1 FROM trait_node", CONNECTION_SYNCH);
+
+ // TraitNodeEntry.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_NODE_ENTRY, "SELECT ID, TraitDefinitionID, MaxRanks, NodeEntryType FROM trait_node_entry"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_NODE_ENTRY, "SELECT MAX(ID) + 1 FROM trait_node_entry", CONNECTION_SYNCH);
+
+ // TraitNodeEntryXTraitCond.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_NODE_ENTRY_X_TRAIT_COND, "SELECT ID, TraitCondID, TraitNodeEntryID FROM trait_node_entry_x_trait_cond"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_NODE_ENTRY_X_TRAIT_COND, "SELECT MAX(ID) + 1 FROM trait_node_entry_x_trait_cond", CONNECTION_SYNCH);
+
+ // TraitNodeEntryXTraitCost.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_NODE_ENTRY_X_TRAIT_COST, "SELECT ID, TraitNodeEntryID, TraitCostID FROM trait_node_entry_x_trait_cost"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_NODE_ENTRY_X_TRAIT_COST, "SELECT MAX(ID) + 1 FROM trait_node_entry_x_trait_cost", CONNECTION_SYNCH);
+
+ // TraitNodeGroup.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_NODE_GROUP, "SELECT ID, TraitTreeID, Flags FROM trait_node_group WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_NODE_GROUP, "SELECT MAX(ID) + 1 FROM trait_node_group", CONNECTION_SYNCH);
+
+ // TraitNodeGroupXTraitCond.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_COND, "SELECT ID, TraitCondID, TraitNodeGroupID FROM trait_node_group_x_trait_cond"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_COND, "SELECT MAX(ID) + 1 FROM trait_node_group_x_trait_cond", CONNECTION_SYNCH);
+
+ // TraitNodeGroupXTraitCost.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_COST, "SELECT ID, TraitNodeGroupID, TraitCostID FROM trait_node_group_x_trait_cost"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_COST, "SELECT MAX(ID) + 1 FROM trait_node_group_x_trait_cost", CONNECTION_SYNCH);
+
+ // TraitNodeGroupXTraitNode.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_NODE, "SELECT ID, TraitNodeGroupID, TraitNodeID, `Index` FROM trait_node_group_x_trait_node"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_NODE, "SELECT MAX(ID) + 1 FROM trait_node_group_x_trait_node", CONNECTION_SYNCH);
+
+ // TraitNodeXTraitCond.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_NODE_X_TRAIT_COND, "SELECT ID, TraitCondID, TraitNodeID FROM trait_node_x_trait_cond"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_NODE_X_TRAIT_COND, "SELECT MAX(ID) + 1 FROM trait_node_x_trait_cond", CONNECTION_SYNCH);
+
+ // TraitNodeXTraitCost.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_NODE_X_TRAIT_COST, "SELECT ID, TraitNodeID, TraitCostID FROM trait_node_x_trait_cost"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_NODE_X_TRAIT_COST, "SELECT MAX(ID) + 1 FROM trait_node_x_trait_cost", CONNECTION_SYNCH);
+
+ // TraitNodeXTraitNodeEntry.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_NODE_X_TRAIT_NODE_ENTRY, "SELECT ID, TraitNodeID, TraitNodeEntryID, `Index` FROM trait_node_x_trait_node_entry"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_NODE_X_TRAIT_NODE_ENTRY, "SELECT MAX(ID) + 1 FROM trait_node_x_trait_node_entry", CONNECTION_SYNCH);
+
+ // TraitTree.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_TREE, "SELECT ID, TraitSystemID, Unused1000_1, FirstTraitNodeID, PlayerConditionID, Flags, Unused1000_2, "
+ "Unused1000_3 FROM trait_tree WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_TREE, "SELECT MAX(ID) + 1 FROM trait_tree", CONNECTION_SYNCH);
+
+ // TraitTreeLoadout.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_TREE_LOADOUT, "SELECT ID, TraitTreeID, ChrSpecializationID FROM trait_tree_loadout"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_TREE_LOADOUT, "SELECT MAX(ID) + 1 FROM trait_tree_loadout", CONNECTION_SYNCH);
+
+ // TraitTreeLoadoutEntry.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_TREE_LOADOUT_ENTRY, "SELECT ID, TraitTreeLoadoutID, SelectedTraitNodeID, SelectedTraitNodeEntryID, NumPoints, "
+ "OrderIndex FROM trait_tree_loadout_entry WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_TREE_LOADOUT_ENTRY, "SELECT MAX(ID) + 1 FROM trait_tree_loadout_entry", CONNECTION_SYNCH);
+
+ // TraitTreeXTraitCost.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_TREE_X_TRAIT_COST, "SELECT ID, TraitTreeID, TraitCostID FROM trait_tree_x_trait_cost"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_TREE_X_TRAIT_COST, "SELECT MAX(ID) + 1 FROM trait_tree_x_trait_cost", CONNECTION_SYNCH);
+
+ // TraitTreeXTraitCurrency.db2
+ PrepareStatement(HOTFIX_SEL_TRAIT_TREE_X_TRAIT_CURRENCY, "SELECT ID, `Index`, TraitTreeID, TraitCurrencyID FROM trait_tree_x_trait_currency"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_TRAIT_TREE_X_TRAIT_CURRENCY, "SELECT MAX(ID) + 1 FROM trait_tree_x_trait_currency", CONNECTION_SYNCH);
+
// TransmogIllusion.db2
PrepareStatement(HOTFIX_SEL_TRANSMOG_ILLUSION, "SELECT ID, UnlockConditionID, TransmogCost, SpellItemEnchantmentID, Flags FROM transmog_illusion"
" WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
diff --git a/src/server/database/Database/Implementation/HotfixDatabase.h b/src/server/database/Database/Implementation/HotfixDatabase.h
index 7816a6e7de1..46da89df7b0 100644
--- a/src/server/database/Database/Implementation/HotfixDatabase.h
+++ b/src/server/database/Database/Implementation/HotfixDatabase.h
@@ -777,6 +777,9 @@ enum HotfixDatabaseStatements : uint32
HOTFIX_SEL_SKILL_LINE_ABILITY_MAX_ID,
HOTFIX_SEL_SKILL_LINE_ABILITY_LOCALE,
+ HOTFIX_SEL_SKILL_LINE_X_TRAIT_TREE,
+ HOTFIX_SEL_SKILL_LINE_X_TRAIT_TREE_MAX_ID,
+
HOTFIX_SEL_SKILL_RACE_CLASS_INFO,
HOTFIX_SEL_SKILL_RACE_CLASS_INFO_MAX_ID,
@@ -944,6 +947,77 @@ enum HotfixDatabaseStatements : uint32
HOTFIX_SEL_TRANSMOG_HOLIDAY,
HOTFIX_SEL_TRANSMOG_HOLIDAY_MAX_ID,
+ HOTFIX_SEL_TRAIT_COND,
+ HOTFIX_SEL_TRAIT_COND_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_COST,
+ HOTFIX_SEL_TRAIT_COST_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_CURRENCY,
+ HOTFIX_SEL_TRAIT_CURRENCY_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_CURRENCY_SOURCE,
+ HOTFIX_SEL_TRAIT_CURRENCY_SOURCE_MAX_ID,
+ HOTFIX_SEL_TRAIT_CURRENCY_SOURCE_LOCALE,
+
+ HOTFIX_SEL_TRAIT_DEFINITION,
+ HOTFIX_SEL_TRAIT_DEFINITION_MAX_ID,
+ HOTFIX_SEL_TRAIT_DEFINITION_LOCALE,
+
+ HOTFIX_SEL_TRAIT_DEFINITION_EFFECT_POINTS,
+ HOTFIX_SEL_TRAIT_DEFINITION_EFFECT_POINTS_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_EDGE,
+ HOTFIX_SEL_TRAIT_EDGE_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_NODE,
+ HOTFIX_SEL_TRAIT_NODE_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_NODE_ENTRY,
+ HOTFIX_SEL_TRAIT_NODE_ENTRY_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_NODE_ENTRY_X_TRAIT_COND,
+ HOTFIX_SEL_TRAIT_NODE_ENTRY_X_TRAIT_COND_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_NODE_ENTRY_X_TRAIT_COST,
+ HOTFIX_SEL_TRAIT_NODE_ENTRY_X_TRAIT_COST_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_NODE_GROUP,
+ HOTFIX_SEL_TRAIT_NODE_GROUP_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_COND,
+ HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_COND_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_COST,
+ HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_COST_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_NODE,
+ HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_NODE_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_NODE_X_TRAIT_COND,
+ HOTFIX_SEL_TRAIT_NODE_X_TRAIT_COND_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_NODE_X_TRAIT_COST,
+ HOTFIX_SEL_TRAIT_NODE_X_TRAIT_COST_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_NODE_X_TRAIT_NODE_ENTRY,
+ HOTFIX_SEL_TRAIT_NODE_X_TRAIT_NODE_ENTRY_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_TREE,
+ HOTFIX_SEL_TRAIT_TREE_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_TREE_LOADOUT,
+ HOTFIX_SEL_TRAIT_TREE_LOADOUT_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_TREE_LOADOUT_ENTRY,
+ HOTFIX_SEL_TRAIT_TREE_LOADOUT_ENTRY_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_TREE_X_TRAIT_COST,
+ HOTFIX_SEL_TRAIT_TREE_X_TRAIT_COST_MAX_ID,
+
+ HOTFIX_SEL_TRAIT_TREE_X_TRAIT_CURRENCY,
+ HOTFIX_SEL_TRAIT_TREE_X_TRAIT_CURRENCY_MAX_ID,
+
HOTFIX_SEL_TRANSMOG_ILLUSION,
HOTFIX_SEL_TRANSMOG_ILLUSION_MAX_ID,
diff --git a/src/server/game/Conditions/ConditionMgr.cpp b/src/server/game/Conditions/ConditionMgr.cpp
index e1c385dc881..66a6079b2a9 100644
--- a/src/server/game/Conditions/ConditionMgr.cpp
+++ b/src/server/game/Conditions/ConditionMgr.cpp
@@ -3992,13 +3992,17 @@ int32 GetUnitConditionVariable(Unit const* unit, Unit const* otherUnit, UnitCond
case UnitConditionVariable::IsEnemy:
return otherUnit && unit->GetReactionTo(otherUnit) <= REP_HOSTILE;
case UnitConditionVariable::IsSpecMelee:
- return unit->IsPlayer() && sChrSpecializationStore.AssertEntry(unit->ToPlayer()->GetPrimarySpecialization())->Flags & CHR_SPECIALIZATION_FLAG_MELEE;
+ return unit->IsPlayer() && unit->ToPlayer()->GetPrimarySpecialization()
+ && sChrSpecializationStore.AssertEntry(unit->ToPlayer()->GetPrimarySpecialization())->Flags & CHR_SPECIALIZATION_FLAG_MELEE;
case UnitConditionVariable::IsSpecTank:
- return unit->IsPlayer() && sChrSpecializationStore.AssertEntry(unit->ToPlayer()->GetPrimarySpecialization())->Role == 0;
+ return unit->IsPlayer() && unit->ToPlayer()->GetPrimarySpecialization()
+ && sChrSpecializationStore.AssertEntry(unit->ToPlayer()->GetPrimarySpecialization())->Role == 0;
case UnitConditionVariable::IsSpecRanged:
- return unit->IsPlayer() && sChrSpecializationStore.AssertEntry(unit->ToPlayer()->GetPrimarySpecialization())->Flags & CHR_SPECIALIZATION_FLAG_RANGED;
+ return unit->IsPlayer() && unit->ToPlayer()->GetPrimarySpecialization()
+ && sChrSpecializationStore.AssertEntry(unit->ToPlayer()->GetPrimarySpecialization())->Flags & CHR_SPECIALIZATION_FLAG_RANGED;
case UnitConditionVariable::IsSpecHealer:
- return unit->IsPlayer() && sChrSpecializationStore.AssertEntry(unit->ToPlayer()->GetPrimarySpecialization())->Role == 1;
+ return unit->IsPlayer() && unit->ToPlayer()->GetPrimarySpecialization()
+ && sChrSpecializationStore.AssertEntry(unit->ToPlayer()->GetPrimarySpecialization())->Role == 1;
case UnitConditionVariable::IsPlayerControlledNPC:
return unit->IsCreature() && unit->HasUnitFlag(UNIT_FLAG_PLAYER_CONTROLLED);
case UnitConditionVariable::IsDying:
diff --git a/src/server/game/DataStores/DB2LoadInfo.h b/src/server/game/DataStores/DB2LoadInfo.h
index 53fd6206137..6fa74823672 100644
--- a/src/server/game/DataStores/DB2LoadInfo.h
+++ b/src/server/game/DataStores/DB2LoadInfo.h
@@ -5134,6 +5134,22 @@ struct SkillLineAbilityLoadInfo
}
};
+struct SkillLineXTraitTreeLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "SkillLineID" },
+ { true, FT_INT, "TraitTreeID" },
+ { true, FT_INT, "OrderIndex" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), SkillLineXTraitTreeMeta::Instance(), HOTFIX_SEL_SKILL_LINE_X_TRAIT_TREE);
+ return &loadInfo;
+ }
+};
+
struct SkillRaceClassInfoLoadInfo
{
static DB2LoadInfo const* Instance()
@@ -6314,6 +6330,396 @@ struct TransmogHolidayLoadInfo
}
};
+struct TraitCondLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "CondType" },
+ { true, FT_INT, "TraitTreeID" },
+ { true, FT_INT, "GrantedRanks" },
+ { true, FT_INT, "QuestID" },
+ { true, FT_INT, "AchievementID" },
+ { true, FT_INT, "SpecSetID" },
+ { true, FT_INT, "TraitNodeGroupID" },
+ { true, FT_INT, "TraitNodeID" },
+ { true, FT_INT, "TraitCurrencyID" },
+ { true, FT_INT, "SpentAmountRequired" },
+ { true, FT_INT, "Flags" },
+ { true, FT_INT, "RequiredLevel" },
+ { true, FT_INT, "FreeSharedStringID" },
+ { true, FT_INT, "SpendMoreSharedStringID" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitCondMeta::Instance(), HOTFIX_SEL_TRAIT_COND);
+ return &loadInfo;
+ }
+};
+
+struct TraitCostLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_STRING_NOT_LOCALIZED, "InternalName" },
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "Amount" },
+ { true, FT_INT, "TraitCurrencyID" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitCostMeta::Instance(), HOTFIX_SEL_TRAIT_COST);
+ return &loadInfo;
+ }
+};
+
+struct TraitCurrencyLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "Type" },
+ { true, FT_INT, "CurrencyTypesID" },
+ { true, FT_INT, "Flags" },
+ { true, FT_INT, "Icon" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitCurrencyMeta::Instance(), HOTFIX_SEL_TRAIT_CURRENCY);
+ return &loadInfo;
+ }
+};
+
+struct TraitCurrencySourceLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_STRING, "Requirement" },
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitCurrencyID" },
+ { true, FT_INT, "Amount" },
+ { true, FT_INT, "QuestID" },
+ { true, FT_INT, "AchievementID" },
+ { true, FT_INT, "PlayerLevel" },
+ { true, FT_INT, "TraitNodeEntryID" },
+ { true, FT_INT, "OrderIndex" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitCurrencySourceMeta::Instance(), HOTFIX_SEL_TRAIT_CURRENCY_SOURCE);
+ return &loadInfo;
+ }
+};
+
+struct TraitDefinitionLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_STRING, "OverrideName" },
+ { false, FT_STRING, "OverrideSubtext" },
+ { false, FT_STRING, "OverrideDescription" },
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "SpellID" },
+ { true, FT_INT, "OverrideIcon" },
+ { true, FT_INT, "OverridesSpellID" },
+ { true, FT_INT, "VisibleSpellID" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitDefinitionMeta::Instance(), HOTFIX_SEL_TRAIT_DEFINITION);
+ return &loadInfo;
+ }
+};
+
+struct TraitDefinitionEffectPointsLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitDefinitionID" },
+ { true, FT_INT, "EffectIndex" },
+ { true, FT_INT, "OperationType" },
+ { true, FT_INT, "CurveID" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitDefinitionEffectPointsMeta::Instance(), HOTFIX_SEL_TRAIT_DEFINITION_EFFECT_POINTS);
+ return &loadInfo;
+ }
+};
+
+struct TraitEdgeLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "VisualStyle" },
+ { true, FT_INT, "LeftTraitNodeID" },
+ { true, FT_INT, "RightTraitNodeID" },
+ { true, FT_INT, "Type" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitEdgeMeta::Instance(), HOTFIX_SEL_TRAIT_EDGE);
+ return &loadInfo;
+ }
+};
+
+struct TraitNodeLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitTreeID" },
+ { true, FT_INT, "PosX" },
+ { true, FT_INT, "PosY" },
+ { true, FT_BYTE, "Type" },
+ { true, FT_INT, "Flags" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitNodeMeta::Instance(), HOTFIX_SEL_TRAIT_NODE);
+ return &loadInfo;
+ }
+};
+
+struct TraitNodeEntryLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitDefinitionID" },
+ { true, FT_INT, "MaxRanks" },
+ { false, FT_BYTE, "NodeEntryType" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitNodeEntryMeta::Instance(), HOTFIX_SEL_TRAIT_NODE_ENTRY);
+ return &loadInfo;
+ }
+};
+
+struct TraitNodeEntryXTraitCondLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitCondID" },
+ { false, FT_INT, "TraitNodeEntryID" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitNodeEntryXTraitCondMeta::Instance(), HOTFIX_SEL_TRAIT_NODE_ENTRY_X_TRAIT_COND);
+ return &loadInfo;
+ }
+};
+
+struct TraitNodeEntryXTraitCostLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitNodeEntryID" },
+ { true, FT_INT, "TraitCostID" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitNodeEntryXTraitCostMeta::Instance(), HOTFIX_SEL_TRAIT_NODE_ENTRY_X_TRAIT_COST);
+ return &loadInfo;
+ }
+};
+
+struct TraitNodeGroupLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitTreeID" },
+ { true, FT_INT, "Flags" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitNodeGroupMeta::Instance(), HOTFIX_SEL_TRAIT_NODE_GROUP);
+ return &loadInfo;
+ }
+};
+
+struct TraitNodeGroupXTraitCondLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitCondID" },
+ { true, FT_INT, "TraitNodeGroupID" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitNodeGroupXTraitCondMeta::Instance(), HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_COND);
+ return &loadInfo;
+ }
+};
+
+struct TraitNodeGroupXTraitCostLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitNodeGroupID" },
+ { true, FT_INT, "TraitCostID" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitNodeGroupXTraitCostMeta::Instance(), HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_COST);
+ return &loadInfo;
+ }
+};
+
+struct TraitNodeGroupXTraitNodeLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitNodeGroupID" },
+ { true, FT_INT, "TraitNodeID" },
+ { true, FT_INT, "Index" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitNodeGroupXTraitNodeMeta::Instance(), HOTFIX_SEL_TRAIT_NODE_GROUP_X_TRAIT_NODE);
+ return &loadInfo;
+ }
+};
+
+struct TraitNodeXTraitCondLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitCondID" },
+ { true, FT_INT, "TraitNodeID" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitNodeXTraitCondMeta::Instance(), HOTFIX_SEL_TRAIT_NODE_X_TRAIT_COND);
+ return &loadInfo;
+ }
+};
+
+struct TraitNodeXTraitCostLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { false, FT_INT, "TraitNodeID" },
+ { true, FT_INT, "TraitCostID" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitNodeXTraitCostMeta::Instance(), HOTFIX_SEL_TRAIT_NODE_X_TRAIT_COST);
+ return &loadInfo;
+ }
+};
+
+struct TraitNodeXTraitNodeEntryLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitNodeID" },
+ { true, FT_INT, "TraitNodeEntryID" },
+ { true, FT_INT, "Index" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitNodeXTraitNodeEntryMeta::Instance(), HOTFIX_SEL_TRAIT_NODE_X_TRAIT_NODE_ENTRY);
+ return &loadInfo;
+ }
+};
+
+struct TraitTreeLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitSystemID" },
+ { true, FT_INT, "Unused1000_1" },
+ { true, FT_INT, "FirstTraitNodeID" },
+ { true, FT_INT, "PlayerConditionID" },
+ { true, FT_INT, "Flags" },
+ { false, FT_FLOAT, "Unused1000_2" },
+ { false, FT_FLOAT, "Unused1000_3" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitTreeMeta::Instance(), HOTFIX_SEL_TRAIT_TREE);
+ return &loadInfo;
+ }
+};
+
+struct TraitTreeLoadoutLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitTreeID" },
+ { true, FT_INT, "ChrSpecializationID" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitTreeLoadoutMeta::Instance(), HOTFIX_SEL_TRAIT_TREE_LOADOUT);
+ return &loadInfo;
+ }
+};
+
+struct TraitTreeLoadoutEntryLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "TraitTreeLoadoutID" },
+ { true, FT_INT, "SelectedTraitNodeID" },
+ { true, FT_INT, "SelectedTraitNodeEntryID" },
+ { true, FT_INT, "NumPoints" },
+ { true, FT_INT, "OrderIndex" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitTreeLoadoutEntryMeta::Instance(), HOTFIX_SEL_TRAIT_TREE_LOADOUT_ENTRY);
+ return &loadInfo;
+ }
+};
+
+struct TraitTreeXTraitCostLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { false, FT_INT, "TraitTreeID" },
+ { true, FT_INT, "TraitCostID" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitTreeXTraitCostMeta::Instance(), HOTFIX_SEL_TRAIT_TREE_X_TRAIT_COST);
+ return &loadInfo;
+ }
+};
+
+struct TraitTreeXTraitCurrencyLoadInfo
+{
+ static DB2LoadInfo const* Instance()
+ {
+ static constexpr DB2FieldMeta fields[] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "Index" },
+ { true, FT_INT, "TraitTreeID" },
+ { true, FT_INT, "TraitCurrencyID" },
+ };
+ static DB2LoadInfo const loadInfo(&fields[0], std::size(fields), TraitTreeXTraitCurrencyMeta::Instance(), HOTFIX_SEL_TRAIT_TREE_X_TRAIT_CURRENCY);
+ return &loadInfo;
+ }
+};
+
struct TransmogIllusionLoadInfo
{
static DB2LoadInfo const* Instance()
diff --git a/src/server/game/DataStores/DB2Stores.cpp b/src/server/game/DataStores/DB2Stores.cpp
index 5ba794df95c..c184584e7d3 100644
--- a/src/server/game/DataStores/DB2Stores.cpp
+++ b/src/server/game/DataStores/DB2Stores.cpp
@@ -265,6 +265,7 @@ DB2Storage<SceneScriptPackageEntry> sSceneScriptPackageStore("SceneS
DB2Storage<SceneScriptTextEntry> sSceneScriptTextStore("SceneScriptText.db2", SceneScriptTextLoadInfo::Instance());
DB2Storage<SkillLineEntry> sSkillLineStore("SkillLine.db2", SkillLineLoadInfo::Instance());
DB2Storage<SkillLineAbilityEntry> sSkillLineAbilityStore("SkillLineAbility.db2", SkillLineAbilityLoadInfo::Instance());
+DB2Storage<SkillLineXTraitTreeEntry> sSkillLineXTraitTreeStore("SkillLineXTraitTree.db2", SkillLineXTraitTreeLoadInfo::Instance());
DB2Storage<SkillRaceClassInfoEntry> sSkillRaceClassInfoStore("SkillRaceClassInfo.db2", SkillRaceClassInfoLoadInfo::Instance());
DB2Storage<SoulbindConduitRankEntry> sSoulbindConduitRankStore("SoulbindConduitRank.db2", SoulbindConduitRankLoadInfo::Instance());
DB2Storage<SoundKitEntry> sSoundKitStore("SoundKit.db2", SoundKitLoadInfo::Instance());
@@ -316,6 +317,29 @@ DB2Storage<TaxiPathEntry> sTaxiPathStore("TaxiPath.db2", T
DB2Storage<TaxiPathNodeEntry> sTaxiPathNodeStore("TaxiPathNode.db2", TaxiPathNodeLoadInfo::Instance());
DB2Storage<TotemCategoryEntry> sTotemCategoryStore("TotemCategory.db2", TotemCategoryLoadInfo::Instance());
DB2Storage<ToyEntry> sToyStore("Toy.db2", ToyLoadInfo::Instance());
+DB2Storage<TraitCondEntry> sTraitCondStore("TraitCond.db2", TraitCondLoadInfo::Instance());
+DB2Storage<TraitCostEntry> sTraitCostStore("TraitCost.db2", TraitCostLoadInfo::Instance());
+DB2Storage<TraitCurrencyEntry> sTraitCurrencyStore("TraitCurrency.db2", TraitCurrencyLoadInfo::Instance());
+DB2Storage<TraitCurrencySourceEntry> sTraitCurrencySourceStore("TraitCurrencySource.db2", TraitCurrencySourceLoadInfo::Instance());
+DB2Storage<TraitDefinitionEntry> sTraitDefinitionStore("TraitDefinition.db2", TraitDefinitionLoadInfo::Instance());
+DB2Storage<TraitDefinitionEffectPointsEntry> sTraitDefinitionEffectPointsStore("TraitDefinitionEffectPoints.db2", TraitDefinitionEffectPointsLoadInfo::Instance());
+DB2Storage<TraitEdgeEntry> sTraitEdgeStore("TraitEdge.db2", TraitEdgeLoadInfo::Instance());
+DB2Storage<TraitNodeEntry> sTraitNodeStore("TraitNode.db2", TraitNodeLoadInfo::Instance());
+DB2Storage<TraitNodeEntryEntry> sTraitNodeEntryStore("TraitNodeEntry.db2", TraitNodeEntryLoadInfo::Instance());
+DB2Storage<TraitNodeEntryXTraitCondEntry> sTraitNodeEntryXTraitCondStore("TraitNodeEntryXTraitCond.db2", TraitNodeEntryXTraitCondLoadInfo::Instance());
+DB2Storage<TraitNodeEntryXTraitCostEntry> sTraitNodeEntryXTraitCostStore("TraitNodeEntryXTraitCost.db2", TraitNodeEntryXTraitCostLoadInfo::Instance());
+DB2Storage<TraitNodeGroupEntry> sTraitNodeGroupStore("TraitNodeGroup.db2", TraitNodeGroupLoadInfo::Instance());
+DB2Storage<TraitNodeGroupXTraitCondEntry> sTraitNodeGroupXTraitCondStore("TraitNodeGroupXTraitCond.db2", TraitNodeGroupXTraitCondLoadInfo::Instance());
+DB2Storage<TraitNodeGroupXTraitCostEntry> sTraitNodeGroupXTraitCostStore("TraitNodeGroupXTraitCost.db2", TraitNodeGroupXTraitCostLoadInfo::Instance());
+DB2Storage<TraitNodeGroupXTraitNodeEntry> sTraitNodeGroupXTraitNodeStore("TraitNodeGroupXTraitNode.db2", TraitNodeGroupXTraitNodeLoadInfo::Instance());
+DB2Storage<TraitNodeXTraitCondEntry> sTraitNodeXTraitCondStore("TraitNodeXTraitCond.db2", TraitNodeXTraitCondLoadInfo::Instance());
+DB2Storage<TraitNodeXTraitCostEntry> sTraitNodeXTraitCostStore("TraitNodeXTraitCost.db2", TraitNodeXTraitCostLoadInfo::Instance());
+DB2Storage<TraitNodeXTraitNodeEntryEntry> sTraitNodeXTraitNodeEntryStore("TraitNodeXTraitNodeEntry.db2", TraitNodeXTraitNodeEntryLoadInfo::Instance());
+DB2Storage<TraitTreeEntry> sTraitTreeStore("TraitTree.db2", TraitTreeLoadInfo::Instance());
+DB2Storage<TraitTreeLoadoutEntry> sTraitTreeLoadoutStore("TraitTreeLoadout.db2", TraitTreeLoadoutLoadInfo::Instance());
+DB2Storage<TraitTreeLoadoutEntryEntry> sTraitTreeLoadoutEntryStore("TraitTreeLoadoutEntry.db2", TraitTreeLoadoutEntryLoadInfo::Instance());
+DB2Storage<TraitTreeXTraitCostEntry> sTraitTreeXTraitCostStore("TraitTreeXTraitCost.db2", TraitTreeXTraitCostLoadInfo::Instance());
+DB2Storage<TraitTreeXTraitCurrencyEntry> sTraitTreeXTraitCurrencyStore("TraitTreeXTraitCurrency.db2", TraitTreeXTraitCurrencyLoadInfo::Instance());
DB2Storage<TransmogHolidayEntry> sTransmogHolidayStore("TransmogHoliday.db2", TransmogHolidayLoadInfo::Instance());
DB2Storage<TransmogIllusionEntry> sTransmogIllusionStore("TransmogIllusion.db2", TransmogIllusionLoadInfo::Instance());
DB2Storage<TransmogSetEntry> sTransmogSetStore("TransmogSet.db2", TransmogSetLoadInfo::Instance());
@@ -841,6 +865,7 @@ uint32 DB2Manager::LoadStores(std::string const& dataPath, LocaleConstant defaul
LOAD_DB2(sSceneScriptTextStore);
LOAD_DB2(sSkillLineStore);
LOAD_DB2(sSkillLineAbilityStore);
+ LOAD_DB2(sSkillLineXTraitTreeStore);
LOAD_DB2(sSkillRaceClassInfoStore);
LOAD_DB2(sSoulbindConduitRankStore);
LOAD_DB2(sSoundKitStore);
@@ -892,6 +917,29 @@ uint32 DB2Manager::LoadStores(std::string const& dataPath, LocaleConstant defaul
LOAD_DB2(sTaxiPathNodeStore);
LOAD_DB2(sTotemCategoryStore);
LOAD_DB2(sToyStore);
+ LOAD_DB2(sTraitCondStore);
+ LOAD_DB2(sTraitCostStore);
+ LOAD_DB2(sTraitCurrencyStore);
+ LOAD_DB2(sTraitCurrencySourceStore);
+ LOAD_DB2(sTraitDefinitionStore);
+ LOAD_DB2(sTraitDefinitionEffectPointsStore);
+ LOAD_DB2(sTraitEdgeStore);
+ LOAD_DB2(sTraitNodeStore);
+ LOAD_DB2(sTraitNodeEntryStore);
+ LOAD_DB2(sTraitNodeEntryXTraitCondStore);
+ LOAD_DB2(sTraitNodeEntryXTraitCostStore);
+ LOAD_DB2(sTraitNodeGroupStore);
+ LOAD_DB2(sTraitNodeGroupXTraitCondStore);
+ LOAD_DB2(sTraitNodeGroupXTraitCostStore);
+ LOAD_DB2(sTraitNodeGroupXTraitNodeStore);
+ LOAD_DB2(sTraitNodeXTraitCondStore);
+ LOAD_DB2(sTraitNodeXTraitCostStore);
+ LOAD_DB2(sTraitNodeXTraitNodeEntryStore);
+ LOAD_DB2(sTraitTreeStore);
+ LOAD_DB2(sTraitTreeLoadoutStore);
+ LOAD_DB2(sTraitTreeLoadoutEntryStore);
+ LOAD_DB2(sTraitTreeXTraitCostStore);
+ LOAD_DB2(sTraitTreeXTraitCurrencyStore);
LOAD_DB2(sTransmogHolidayStore);
LOAD_DB2(sTransmogIllusionStore);
LOAD_DB2(sTransmogSetStore);
@@ -2942,22 +2990,30 @@ std::vector<SkillLineAbilityEntry const*> const* DB2Manager::GetSkillLineAbiliti
return Trinity::Containers::MapGetValuePtr(_skillLineAbilitiesBySkillupSkill, skillId);
}
-SkillRaceClassInfoEntry const* DB2Manager::GetSkillRaceClassInfo(uint32 skill, uint8 race, uint8 class_)
+SkillRaceClassInfoEntry const* DB2Manager::GetSkillRaceClassInfo(uint32 skill, uint8 race, uint8 class_) const
{
- auto bounds = _skillRaceClassInfoBySkill.equal_range(skill);
- for (auto itr = bounds.first; itr != bounds.second; ++itr)
+ for (auto&& [_, skllRaceClassInfo] : Trinity::Containers::MapEqualRange(_skillRaceClassInfoBySkill, skill))
{
- if (!itr->second->RaceMask.IsEmpty() && !(itr->second->RaceMask.HasRace(race)))
+ if (!skllRaceClassInfo->RaceMask.IsEmpty() && !(skllRaceClassInfo->RaceMask.HasRace(race)))
continue;
- if (itr->second->ClassMask && !(itr->second->ClassMask & (1 << (class_ - 1))))
+ if (skllRaceClassInfo->ClassMask && !(skllRaceClassInfo->ClassMask & (1 << (class_ - 1))))
continue;
- return itr->second;
+ return skllRaceClassInfo;
}
return nullptr;
}
+std::vector<SkillRaceClassInfoEntry const*> DB2Manager::GetSkillRaceClassInfo(uint32 skill) const
+{
+ std::vector<SkillRaceClassInfoEntry const*> result;
+ for (auto const& [_, skillRaceClassInfo] : Trinity::Containers::MapEqualRange(_skillRaceClassInfoBySkill, skill))
+ result.push_back(skillRaceClassInfo);
+
+ return result;
+}
+
SoulbindConduitRankEntry const* DB2Manager::GetSoulbindConduitRank(int32 soulbindConduitId, int32 rank) const
{
return Trinity::Containers::MapGetValuePtr(_soulbindConduitRanks, { soulbindConduitId, rank });
diff --git a/src/server/game/DataStores/DB2Stores.h b/src/server/game/DataStores/DB2Stores.h
index 6b232820363..607cda14e82 100644
--- a/src/server/game/DataStores/DB2Stores.h
+++ b/src/server/game/DataStores/DB2Stores.h
@@ -195,6 +195,7 @@ TC_GAME_API extern DB2Storage<ScenarioStepEntry> sScenarioSte
TC_GAME_API extern DB2Storage<SkillLineEntry> sSkillLineStore;
TC_GAME_API extern DB2Storage<SceneScriptPackageEntry> sSceneScriptPackageStore;
TC_GAME_API extern DB2Storage<SkillLineAbilityEntry> sSkillLineAbilityStore;
+TC_GAME_API extern DB2Storage<SkillLineXTraitTreeEntry> sSkillLineXTraitTreeStore;
TC_GAME_API extern DB2Storage<SkillRaceClassInfoEntry> sSkillRaceClassInfoStore;
TC_GAME_API extern DB2Storage<SoundKitEntry> sSoundKitStore;
TC_GAME_API extern DB2Storage<SpellAuraOptionsEntry> sSpellAuraOptionsStore;
@@ -237,6 +238,29 @@ TC_GAME_API extern DB2Storage<SummonPropertiesEntry> sSummonPrope
TC_GAME_API extern DB2Storage<TalentEntry> sTalentStore;
TC_GAME_API extern DB2Storage<TaxiNodesEntry> sTaxiNodesStore;
TC_GAME_API extern DB2Storage<TaxiPathEntry> sTaxiPathStore;
+TC_GAME_API extern DB2Storage<TraitCondEntry> sTraitCondStore;
+TC_GAME_API extern DB2Storage<TraitCostEntry> sTraitCostStore;
+TC_GAME_API extern DB2Storage<TraitCurrencyEntry> sTraitCurrencyStore;
+TC_GAME_API extern DB2Storage<TraitCurrencySourceEntry> sTraitCurrencySourceStore;
+TC_GAME_API extern DB2Storage<TraitDefinitionEntry> sTraitDefinitionStore;
+TC_GAME_API extern DB2Storage<TraitDefinitionEffectPointsEntry> sTraitDefinitionEffectPointsStore;
+TC_GAME_API extern DB2Storage<TraitEdgeEntry> sTraitEdgeStore;
+TC_GAME_API extern DB2Storage<TraitNodeEntry> sTraitNodeStore;
+TC_GAME_API extern DB2Storage<TraitNodeEntryEntry> sTraitNodeEntryStore;
+TC_GAME_API extern DB2Storage<TraitNodeEntryXTraitCondEntry> sTraitNodeEntryXTraitCondStore;
+TC_GAME_API extern DB2Storage<TraitNodeEntryXTraitCostEntry> sTraitNodeEntryXTraitCostStore;
+TC_GAME_API extern DB2Storage<TraitNodeGroupEntry> sTraitNodeGroupStore;
+TC_GAME_API extern DB2Storage<TraitNodeGroupXTraitCondEntry> sTraitNodeGroupXTraitCondStore;
+TC_GAME_API extern DB2Storage<TraitNodeGroupXTraitCostEntry> sTraitNodeGroupXTraitCostStore;
+TC_GAME_API extern DB2Storage<TraitNodeGroupXTraitNodeEntry> sTraitNodeGroupXTraitNodeStore;
+TC_GAME_API extern DB2Storage<TraitNodeXTraitCondEntry> sTraitNodeXTraitCondStore;
+TC_GAME_API extern DB2Storage<TraitNodeXTraitCostEntry> sTraitNodeXTraitCostStore;
+TC_GAME_API extern DB2Storage<TraitNodeXTraitNodeEntryEntry> sTraitNodeXTraitNodeEntryStore;
+TC_GAME_API extern DB2Storage<TraitTreeEntry> sTraitTreeStore;
+TC_GAME_API extern DB2Storage<TraitTreeLoadoutEntry> sTraitTreeLoadoutStore;
+TC_GAME_API extern DB2Storage<TraitTreeLoadoutEntryEntry> sTraitTreeLoadoutEntryStore;
+TC_GAME_API extern DB2Storage<TraitTreeXTraitCostEntry> sTraitTreeXTraitCostStore;
+TC_GAME_API extern DB2Storage<TraitTreeXTraitCurrencyEntry> sTraitTreeXTraitCurrencyStore;
TC_GAME_API extern DB2Storage<TransmogHolidayEntry> sTransmogHolidayStore;
TC_GAME_API extern DB2Storage<TransmogIllusionEntry> sTransmogIllusionStore;
TC_GAME_API extern DB2Storage<TransmogSetEntry> sTransmogSetStore;
@@ -452,7 +476,8 @@ public:
ShapeshiftFormModelData const* GetShapeshiftFormModelData(uint8 race, uint8 gender, uint8 form) const;
std::vector<SkillLineEntry const*> const* GetSkillLinesForParentSkill(uint32 parentSkillId) const;
std::vector<SkillLineAbilityEntry const*> const* GetSkillLineAbilitiesBySkill(uint32 skillId) const;
- SkillRaceClassInfoEntry const* GetSkillRaceClassInfo(uint32 skill, uint8 race, uint8 class_);
+ SkillRaceClassInfoEntry const* GetSkillRaceClassInfo(uint32 skill, uint8 race, uint8 class_) const;
+ std::vector<SkillRaceClassInfoEntry const*> GetSkillRaceClassInfo(uint32 skill) const;
SoulbindConduitRankEntry const* GetSoulbindConduitRank(int32 soulbindConduitId, int32 rank) const;
std::vector<SpecializationSpellsEntry const*> const* GetSpecializationSpells(uint32 specId) const;
bool IsSpecSetMember(int32 specSetId, uint32 specId) const;
diff --git a/src/server/game/DataStores/DB2Structure.h b/src/server/game/DataStores/DB2Structure.h
index 187eac98d3c..47b91ff31f0 100644
--- a/src/server/game/DataStores/DB2Structure.h
+++ b/src/server/game/DataStores/DB2Structure.h
@@ -3207,6 +3207,14 @@ struct SkillLineAbilityEntry
EnumFlag<SkillLineAbilityFlags> GetFlags() const { return static_cast<SkillLineAbilityFlags>(Flags); }
};
+struct SkillLineXTraitTreeEntry
+{
+ uint32 ID;
+ int32 SkillLineID;
+ int32 TraitTreeID;
+ int32 OrderIndex;
+};
+
struct SkillRaceClassInfoEntry
{
uint32 ID;
@@ -3855,6 +3863,222 @@ struct TransmogHolidayEntry
int32 RequiredTransmogHoliday;
};
+struct TraitCondEntry
+{
+ uint32 ID;
+ int32 CondType;
+ int32 TraitTreeID;
+ int32 GrantedRanks;
+ int32 QuestID;
+ int32 AchievementID;
+ int32 SpecSetID;
+ int32 TraitNodeGroupID;
+ int32 TraitNodeID;
+ int32 TraitCurrencyID;
+ int32 SpentAmountRequired;
+ int32 Flags;
+ int32 RequiredLevel;
+ int32 FreeSharedStringID;
+ int32 SpendMoreSharedStringID;
+
+ TraitConditionType GetCondType() const { return static_cast<TraitConditionType>(CondType); }
+};
+
+struct TraitCostEntry
+{
+ char const* InternalName;
+ uint32 ID;
+ int32 Amount;
+ int32 TraitCurrencyID;
+};
+
+struct TraitCurrencyEntry
+{
+ uint32 ID;
+ int32 Type;
+ int32 CurrencyTypesID;
+ int32 Flags;
+ int32 Icon;
+
+ TraitCurrencyType GetType() const { return static_cast<TraitCurrencyType>(Type); }
+};
+
+struct TraitCurrencySourceEntry
+{
+ LocalizedString Requirement;
+ uint32 ID;
+ int32 TraitCurrencyID;
+ int32 Amount;
+ int32 QuestID;
+ int32 AchievementID;
+ int32 PlayerLevel;
+ int32 TraitNodeEntryID;
+ int32 OrderIndex;
+};
+
+struct TraitDefinitionEntry
+{
+ LocalizedString OverrideName;
+ LocalizedString OverrideSubtext;
+ LocalizedString OverrideDescription;
+ uint32 ID;
+ int32 SpellID;
+ int32 OverrideIcon;
+ int32 OverridesSpellID;
+ int32 VisibleSpellID;
+};
+
+struct TraitDefinitionEffectPointsEntry
+{
+ uint32 ID;
+ int32 TraitDefinitionID;
+ int32 EffectIndex;
+ int32 OperationType;
+ int32 CurveID;
+
+ TraitPointsOperationType GetOperationType() const { return static_cast<TraitPointsOperationType>(OperationType); }
+};
+
+struct TraitEdgeEntry
+{
+ uint32 ID;
+ int32 VisualStyle;
+ int32 LeftTraitNodeID;
+ int32 RightTraitNodeID;
+ int32 Type;
+};
+
+struct TraitNodeEntry
+{
+ uint32 ID;
+ int32 TraitTreeID;
+ int32 PosX;
+ int32 PosY;
+ int8 Type;
+ int32 Flags;
+
+ TraitNodeType GetType() const { return static_cast<TraitNodeType>(Type); }
+};
+
+struct TraitNodeEntryEntry
+{
+ uint32 ID;
+ int32 TraitDefinitionID;
+ int32 MaxRanks;
+ uint8 NodeEntryType;
+};
+
+struct TraitNodeEntryXTraitCondEntry
+{
+ uint32 ID;
+ int32 TraitCondID;
+ uint32 TraitNodeEntryID;
+};
+
+struct TraitNodeEntryXTraitCostEntry
+{
+ uint32 ID;
+ int32 TraitNodeEntryID;
+ int32 TraitCostID;
+};
+
+struct TraitNodeGroupEntry
+{
+ uint32 ID;
+ int32 TraitTreeID;
+ int32 Flags;
+};
+
+struct TraitNodeGroupXTraitCondEntry
+{
+ uint32 ID;
+ int32 TraitCondID;
+ int32 TraitNodeGroupID;
+};
+
+struct TraitNodeGroupXTraitCostEntry
+{
+ uint32 ID;
+ int32 TraitNodeGroupID;
+ int32 TraitCostID;
+};
+
+struct TraitNodeGroupXTraitNodeEntry
+{
+ uint32 ID;
+ int32 TraitNodeGroupID;
+ int32 TraitNodeID;
+ int32 Index;
+};
+
+struct TraitNodeXTraitCondEntry
+{
+ uint32 ID;
+ int32 TraitCondID;
+ int32 TraitNodeID;
+};
+
+struct TraitNodeXTraitCostEntry
+{
+ uint32 ID;
+ uint32 TraitNodeID;
+ int32 TraitCostID;
+};
+
+struct TraitNodeXTraitNodeEntryEntry
+{
+ uint32 ID;
+ int32 TraitNodeID;
+ int32 TraitNodeEntryID;
+ int32 Index;
+};
+
+struct TraitTreeEntry
+{
+ uint32 ID;
+ int32 TraitSystemID;
+ int32 Unused1000_1;
+ int32 FirstTraitNodeID;
+ int32 PlayerConditionID;
+ int32 Flags;
+ float Unused1000_2;
+ float Unused1000_3;
+
+ EnumFlag<TraitTreeFlag> GetFlags() const { return static_cast<TraitTreeFlag>(Flags); }
+};
+
+struct TraitTreeLoadoutEntry
+{
+ uint32 ID;
+ int32 TraitTreeID;
+ int32 ChrSpecializationID;
+};
+
+struct TraitTreeLoadoutEntryEntry
+{
+ uint32 ID;
+ int32 TraitTreeLoadoutID;
+ int32 SelectedTraitNodeID;
+ int32 SelectedTraitNodeEntryID;
+ int32 NumPoints;
+ int32 OrderIndex;
+};
+
+struct TraitTreeXTraitCostEntry
+{
+ uint32 ID;
+ uint32 TraitTreeID;
+ int32 TraitCostID;
+};
+
+struct TraitTreeXTraitCurrencyEntry
+{
+ uint32 ID;
+ int32 Index;
+ int32 TraitTreeID;
+ int32 TraitCurrencyID;
+};
+
struct TransmogIllusionEntry
{
uint32 ID;
diff --git a/src/server/game/DataStores/DBCEnums.h b/src/server/game/DataStores/DBCEnums.h
index 4590b5909c9..6de7baedc9c 100644
--- a/src/server/game/DataStores/DBCEnums.h
+++ b/src/server/game/DataStores/DBCEnums.h
@@ -1893,6 +1893,24 @@ enum class TraitCombatConfigFlags : int32
DEFINE_ENUM_FLAG(TraitCombatConfigFlags);
+enum class TraitCondFlags : int32
+{
+ None = 0x0,
+ IsGate = 0x1,
+ IsAlwaysMet = 0x2,
+ IsSufficient = 0x4,
+};
+
+DEFINE_ENUM_FLAG(TraitCondFlags);
+
+enum class TraitConditionType : int32
+{
+ Available = 0,
+ Visible = 1,
+ Granted = 2,
+ Increased = 3
+};
+
enum class TraitConfigType : int32
{
Invalid = 0,
@@ -1901,6 +1919,68 @@ enum class TraitConfigType : int32
Generic = 3
};
+enum class TraitCurrencyType : int32
+{
+ Gold = 0,
+ CurrencyTypesBased = 1,
+ TraitSourced = 2
+};
+
+enum class TraitEdgeType : int32
+{
+ VisualOnly = 0,
+ DeprecatedRankConnection = 1,
+ SufficientForAvailability = 2,
+ RequiredForAvailability = 3,
+ MutuallyExclusive = 4,
+ DeprecatedSelectionOption = 5
+};
+
+enum class TraitNodeEntryType : int32
+{
+ SpendHex = 0,
+ SpendSquare = 1,
+ SpendCircle = 2,
+ SpendSmallCircle = 3,
+ DeprecatedSelect = 4,
+ DragAndDrop = 5,
+ SpendDiamond = 6,
+ ProfPath = 7,
+ ProfPerk = 8,
+ ProfPathUnlock = 9
+};
+
+enum class TraitNodeGroupFlag : int32
+{
+ None = 0x0,
+ AvailableByDefault = 0x1
+};
+
+DEFINE_ENUM_FLAG(TraitNodeGroupFlag);
+
+enum class TraitNodeType : int32
+{
+ Single = 0,
+ Tiered = 1,
+ Selection = 2
+};
+
+enum class TraitPointsOperationType : int32
+{
+ None = -1,
+ Set = 0,
+ Multiply = 1
+};
+
+enum class TraitTreeFlag : int32
+{
+ None = 0x0,
+ CannotRefund = 0x1,
+ HideSingleRankNumbers = 0x2
+};
+
+DEFINE_ENUM_FLAG(TraitTreeFlag);
+
enum class UiMapFlag : int32
{
None = 0,
diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp
index ad5e1d2a632..178a053c5b3 100644
--- a/src/server/game/Entities/Player/Player.cpp
+++ b/src/server/game/Entities/Player/Player.cpp
@@ -118,6 +118,8 @@
#include "TerrainMgr.h"
#include "ToyPackets.h"
#include "TradeData.h"
+#include "TraitMgr.h"
+#include "TraitPacketsCommon.h"
#include "Transport.h"
#include "UpdateData.h"
#include "Util.h"
@@ -2802,7 +2804,7 @@ WorldLocation const* Player::GetStoredAuraTeleportLocation(uint32 spellId) const
return nullptr;
}
-bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent, bool disabled, bool loading /*= false*/, int32 fromSkill /*= 0*/)
+bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent, bool disabled, bool loading /*= false*/, int32 fromSkill /*= 0*/, Optional<int32> traitDefinitionId /*= {}*/)
{
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId, DIFFICULTY_NONE);
if (!spellInfo)
@@ -2882,6 +2884,15 @@ bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent
dependent_set = true;
}
+ if (itr->second.TraitDefinitionId != traitDefinitionId)
+ {
+ if (itr->second.TraitDefinitionId)
+ if (TraitDefinitionEntry const* traitDefinition = sTraitDefinitionStore.LookupEntry(*itr->second.TraitDefinitionId))
+ RemoveOverrideSpell(traitDefinition->OverridesSpellID, spellId);
+
+ itr->second.TraitDefinitionId = traitDefinitionId;
+ }
+
// update active state for known spell
if (itr->second.active != active && itr->second.state != PLAYERSPELL_REMOVED && !itr->second.disabled)
{
@@ -2963,6 +2974,8 @@ bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent
newspell.active = active;
newspell.dependent = dependent;
newspell.disabled = disabled;
+ if (traitDefinitionId)
+ newspell.TraitDefinitionId = *traitDefinitionId;
// replace spells in action bars and spellbook to bigger rank if only one spell rank must be accessible
if (newspell.active && !newspell.disabled && spellInfo->IsRanked())
@@ -3011,26 +3024,66 @@ bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent
return false;
}
+ bool castSpell = false;
+
// cast talents with SPELL_EFFECT_LEARN_SPELL (other dependent spells will learned later as not auto-learned)
// note: all spells with SPELL_EFFECT_LEARN_SPELL isn't passive
if (!loading && spellInfo->HasAttribute(SPELL_ATTR0_CU_IS_TALENT) && spellInfo->HasEffect(SPELL_EFFECT_LEARN_SPELL))
- {
// ignore stance requirement for talent learn spell (stance set for spell only for client spell description show)
- CastSpell(this, spellId, true);
- }
+ castSpell = true;
// also cast passive spells (including all talents without SPELL_EFFECT_LEARN_SPELL) with additional checks
else if (spellInfo->IsPassive())
- {
- if (HandlePassiveSpellLearn(spellInfo))
- CastSpell(this, spellId, true);
- }
+ castSpell = HandlePassiveSpellLearn(spellInfo);
else if (spellInfo->HasEffect(SPELL_EFFECT_SKILL_STEP))
+ castSpell = true;
+ else if (spellInfo->HasAttribute(SPELL_ATTR1_CAST_WHEN_LEARNED))
+ castSpell = true;
+
+ if (castSpell)
{
- CastSpell(this, spellId, true);
- return false;
+ CastSpellExtraArgs args;
+ args.SetTriggerFlags(TRIGGERED_FULL_MASK);
+
+ if (traitDefinitionId)
+ {
+ if (UF::TraitConfig const* traitConfig = GetTraitConfig(m_activePlayerData->ActiveCombatTraitConfigID))
+ {
+ int32 traitEntryIndex = traitConfig->Entries.FindIndexIf([traitDefinitionId](UF::TraitEntry const& traitEntry)
+ {
+ return sTraitNodeEntryStore.AssertEntry(traitEntry.TraitNodeEntryID)->TraitDefinitionID == traitDefinitionId;
+ });
+ int32 rank = 0;
+ if (traitEntryIndex >= 0)
+ rank = traitConfig->Entries[traitEntryIndex].Rank + traitConfig->Entries[traitEntryIndex].GrantedRanks;
+
+ if (rank > 0)
+ {
+ if (std::vector<TraitDefinitionEffectPointsEntry const*> const* traitDefinitionEffectPoints = TraitMgr::GetTraitDefinitionEffectPointModifiers(*traitDefinitionId))
+ {
+ for (TraitDefinitionEffectPointsEntry const* traitDefinitionEffectPoint : *traitDefinitionEffectPoints)
+ {
+ if (traitDefinitionEffectPoint->EffectIndex >= int32(spellInfo->GetEffects().size()))
+ continue;
+
+ float basePoints = sDB2Manager.GetCurveValueAt(traitDefinitionEffectPoint->CurveID, rank);
+ if (traitDefinitionEffectPoint->GetOperationType() == TraitPointsOperationType::Multiply)
+ basePoints *= spellInfo->GetEffect(SpellEffIndex(traitDefinitionEffectPoint->EffectIndex)).CalcBaseValue(this, nullptr, 0, -1);
+
+ args.AddSpellMod(SpellValueMod(SPELLVALUE_BASE_POINT0 + traitDefinitionEffectPoint->EffectIndex), basePoints);
+ }
+ }
+ }
+ }
+ }
+
+ CastSpell(this, spellId, args);
+ if (spellInfo->HasEffect(SPELL_EFFECT_SKILL_STEP))
+ return false;
}
- else if (spellInfo->HasAttribute(SPELL_ATTR1_CAST_WHEN_LEARNED))
- CastSpell(this, spellId, true);
+
+ if (traitDefinitionId)
+ if (TraitDefinitionEntry const* traitDefinition = sTraitDefinitionStore.LookupEntry(*traitDefinitionId))
+ AddOverrideSpell(traitDefinition->OverridesSpellID, spellId);
// update free primary prof.points (if any, can be none in case GM .learn prof. learning)
if (uint32 freeProfs = GetFreePrimaryProfessionPoints())
@@ -3168,14 +3221,14 @@ bool Player::HandlePassiveSpellLearn(SpellInfo const* spellInfo)
return need_cast && (!spellInfo->CasterAuraState || HasAuraState(AuraStateType(spellInfo->CasterAuraState)));
}
-void Player::LearnSpell(uint32 spell_id, bool dependent, int32 fromSkill /*= 0*/, bool suppressMessaging /*= false*/)
+void Player::LearnSpell(uint32 spell_id, bool dependent, int32 fromSkill /*= 0*/, bool suppressMessaging /*= false*/, Optional<int32> traitDefinitionId /*= {}*/)
{
PlayerSpellMap::iterator itr = m_spells.find(spell_id);
bool disabled = (itr != m_spells.end()) ? itr->second.disabled : false;
bool active = disabled ? itr->second.active : true;
- bool learning = AddSpell(spell_id, active, true, dependent, false, false, fromSkill);
+ bool learning = AddSpell(spell_id, active, true, dependent, false, false, fromSkill, traitDefinitionId);
// prevent duplicated entires in spell book, also not send if not in world (loading)
if (learning && IsInWorld())
@@ -3183,6 +3236,7 @@ void Player::LearnSpell(uint32 spell_id, bool dependent, int32 fromSkill /*= 0*/
WorldPackets::Spells::LearnedSpells learnedSpells;
WorldPackets::Spells::LearnedSpellInfo& learnedSpellInfo = learnedSpells.ClientLearnedSpellData.emplace_back();
learnedSpellInfo.SpellID = spell_id;
+ learnedSpellInfo.TraitDefinitionID = traitDefinitionId;
learnedSpells.SuppressMessaging = suppressMessaging;
SendDirectMessage(learnedSpells.Write());
}
@@ -4169,6 +4223,14 @@ void Player::DeleteFromDB(ObjectGuid playerguid, uint32 accountId, bool updateRe
Garrison::DeleteFromDB(guid, trans);
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TRAIT_ENTRIES_BY_CHAR);
+ stmt->setUInt64(0, guid);
+ trans->Append(stmt);
+
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TRAIT_CONFIGS_BY_CHAR);
+ stmt->setUInt64(0, guid);
+ trans->Append(stmt);
+
sCharacterCache->DeleteCharacterCacheEntry(playerguid, name);
break;
}
@@ -17589,6 +17651,9 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
LearnDefaultSkills();
LearnCustomSpells();
+ _LoadTraits(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_TRAIT_CONFIGS),
+ holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_TRAIT_ENTRIES)); // must be after loading spells
+
// must be before inventory (some items required reputation check)
m_reputationMgr->LoadFromDB(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_REPUTATION));
@@ -17606,7 +17671,7 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
// update items with duration and realtime
UpdateItemDuration(time_diff, true);
- _LoadActions(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_ACTIONS));
+ StartLoadingActionButtons();
// unread mails and next delivery time, actual mails not loaded
_LoadMail(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_MAILS),
@@ -19559,6 +19624,7 @@ void Player::SaveToDB(LoginDatabaseTransaction loginTransaction, CharacterDataba
_SaveMonthlyQuestStatus(trans);
_SaveGlyphs(trans);
_SaveTalents(trans);
+ _SaveTraits(trans);
_SaveSpells(trans);
GetSpellHistory()->SaveToDB<Player>(trans);
_SaveActions(trans);
@@ -19661,6 +19727,26 @@ void Player::_SaveCustomizations(CharacterDatabaseTransaction trans)
void Player::_SaveActions(CharacterDatabaseTransaction trans)
{
+ int32 traitConfigId = [&]() -> int32
+ {
+ UF::TraitConfig const* traitConfig = GetTraitConfig(m_activePlayerData->ActiveCombatTraitConfigID);
+ if (!traitConfig)
+ return 0;
+
+ int32 usedSavedTraitConfigIndex = m_activePlayerData->TraitConfigs.FindIndexIf([localIdent = *traitConfig->LocalIdentifier](UF::TraitConfig const& savedConfig)
+ {
+ return static_cast<TraitConfigType>(*savedConfig.Type) == TraitConfigType::Combat
+ && (static_cast<TraitCombatConfigFlags>(*savedConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) == TraitCombatConfigFlags::None
+ && (static_cast<TraitCombatConfigFlags>(*savedConfig.CombatConfigFlags) & TraitCombatConfigFlags::SharedActionBars) == TraitCombatConfigFlags::None
+ && savedConfig.LocalIdentifier == localIdent;
+ });
+
+ if (usedSavedTraitConfigIndex >= 0)
+ return m_activePlayerData->TraitConfigs[usedSavedTraitConfigIndex].ID;
+
+ return 0;
+ }();
+
CharacterDatabasePreparedStatement* stmt;
for (ActionButtonList::iterator itr = m_actionButtons.begin(); itr != m_actionButtons.end();)
@@ -19671,9 +19757,10 @@ void Player::_SaveActions(CharacterDatabaseTransaction trans)
stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_ACTION);
stmt->setUInt64(0, GetGUID().GetCounter());
stmt->setUInt8(1, GetActiveTalentGroup());
- stmt->setUInt8(2, itr->first);
- stmt->setUInt64(3, itr->second.GetAction());
- stmt->setUInt8(4, uint8(itr->second.GetType()));
+ stmt->setInt32(2, traitConfigId);
+ stmt->setUInt8(3, itr->first);
+ stmt->setUInt64(4, itr->second.GetAction());
+ stmt->setUInt8(5, uint8(itr->second.GetType()));
trans->Append(stmt);
itr->second.uState = ACTIONBUTTON_UNCHANGED;
@@ -19686,6 +19773,7 @@ void Player::_SaveActions(CharacterDatabaseTransaction trans)
stmt->setUInt64(2, GetGUID().GetCounter());
stmt->setUInt8(3, itr->first);
stmt->setUInt8(4, GetActiveTalentGroup());
+ stmt->setInt32(5, traitConfigId);
trans->Append(stmt);
itr->second.uState = ACTIONBUTTON_UNCHANGED;
@@ -19696,6 +19784,7 @@ void Player::_SaveActions(CharacterDatabaseTransaction trans)
stmt->setUInt64(0, GetGUID().GetCounter());
stmt->setUInt8(1, itr->first);
stmt->setUInt8(2, GetActiveTalentGroup());
+ stmt->setInt32(3, traitConfigId);
trans->Append(stmt);
m_actionButtons.erase(itr++);
@@ -26493,6 +26582,146 @@ void Player::_LoadPvpTalents(PreparedQueryResult result)
}
}
+void Player::_LoadTraits(PreparedQueryResult configsResult, PreparedQueryResult entriesResult)
+{
+ std::unordered_multimap<int32, WorldPackets::Traits::TraitEntry> traitEntriesByConfig;
+ if (entriesResult)
+ {
+ // 0 1, 2 3 4
+ // SELECT traitConfigId, traitNodeId, traitNodeEntryId, rank, grantedRanks FROM character_trait_entry WHERE guid = ?
+ do
+ {
+ Field* fields = entriesResult->Fetch();
+ WorldPackets::Traits::TraitEntry traitEntry;
+ traitEntry.TraitNodeID = fields[1].GetInt32();
+ traitEntry.TraitNodeEntryID = fields[2].GetInt32();
+ traitEntry.Rank = fields[3].GetInt32();
+ traitEntry.GrantedRanks = fields[4].GetInt32();
+
+ if (!TraitMgr::IsValidEntry(traitEntry))
+ continue;
+
+ traitEntriesByConfig.emplace(fields[0].GetInt32(), traitEntry);
+
+ } while (entriesResult->NextRow());
+ }
+
+ if (configsResult)
+ {
+ // 0 1 2 3 4 5 6 7
+ // SELECT traitConfigId, type, chrSpecializationId, combatConfigFlags, localIdentifier, skillLineId, traitSystemId, `name` FROM character_trait_config WHERE guid = ?
+ do
+ {
+ Field* fields = configsResult->Fetch();
+ WorldPackets::Traits::TraitConfig traitConfig;
+ traitConfig.ID = fields[0].GetInt32();
+ traitConfig.Type = static_cast<TraitConfigType>(fields[1].GetInt32());
+ switch (traitConfig.Type)
+ {
+ case TraitConfigType::Combat:
+ traitConfig.ChrSpecializationID = fields[2].GetInt32();
+ traitConfig.CombatConfigFlags = static_cast<TraitCombatConfigFlags>(fields[3].GetInt32());
+ traitConfig.LocalIdentifier = fields[4].GetInt32();
+ break;
+ case TraitConfigType::Profession:
+ traitConfig.SkillLineID = fields[5].GetInt32();
+ break;
+ case TraitConfigType::Generic:
+ traitConfig.TraitSystemID = fields[6].GetInt32();
+ break;
+ default:
+ break;
+ }
+
+ traitConfig.Name = fields[7].GetString();
+
+ for (auto&& [_, traitEntry] : Trinity::Containers::MapEqualRange(traitEntriesByConfig, traitConfig.ID))
+ traitConfig.Entries.emplace_back() = traitEntry;
+
+ if (TraitMgr::ValidateConfig(traitConfig, this) != TALENT_LEARN_OK)
+ {
+ traitConfig.Entries.clear();
+ for (UF::TraitEntry const& grantedEntry : TraitMgr::GetGrantedTraitEntriesForConfig(traitConfig, this))
+ traitConfig.Entries.emplace_back(grantedEntry);
+ }
+
+ AddTraitConfig(traitConfig);
+
+ } while (configsResult->NextRow());
+ }
+
+ auto hasConfigForSpec = [&](int32 specId)
+ {
+ return m_activePlayerData->TraitConfigs.FindIndexIf([=](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.Type == AsUnderlyingType(TraitConfigType::Combat)
+ && traitConfig.ChrSpecializationID == specId
+ && traitConfig.CombatConfigFlags & AsUnderlyingType(TraitCombatConfigFlags::ActiveForSpec);
+ }) >= 0;
+ };
+
+ auto findFreeLocalIdentifier = [&](int32 specId)
+ {
+ int32 index = 1;
+ while (m_activePlayerData->TraitConfigs.FindIndexIf([specId, index](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.Type == AsUnderlyingType(TraitConfigType::Combat)
+ && traitConfig.ChrSpecializationID == specId
+ && traitConfig.LocalIdentifier == index;
+ }) >= 0)
+ ++index;
+
+ return index;
+ };
+
+ for (uint32 i = 0; i < MAX_SPECIALIZATIONS - 1 /*initial spec doesnt get a config*/; ++i)
+ {
+ if (ChrSpecializationEntry const* spec = sDB2Manager.GetChrSpecializationByIndex(GetClass(), i))
+ {
+ if (hasConfigForSpec(spec->ID))
+ continue;
+
+ WorldPackets::Traits::TraitConfig traitConfig;
+ traitConfig.Type = TraitConfigType::Combat;
+ traitConfig.ChrSpecializationID = spec->ID;
+ traitConfig.CombatConfigFlags = TraitCombatConfigFlags::ActiveForSpec;
+ traitConfig.LocalIdentifier = findFreeLocalIdentifier(spec->ID);
+ traitConfig.Name = spec->Name[GetSession()->GetSessionDbcLocale()];
+
+ CreateTraitConfig(traitConfig);
+ }
+ }
+
+ int32 activeConfig = m_activePlayerData->TraitConfigs.FindIndexIf([&](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.Type == AsUnderlyingType(TraitConfigType::Combat)
+ && traitConfig.ChrSpecializationID == int32(GetPrimarySpecialization())
+ && traitConfig.CombatConfigFlags & AsUnderlyingType(TraitCombatConfigFlags::ActiveForSpec);
+ });
+
+ if (activeConfig >= 0)
+ SetActiveCombatTraitConfigID(m_activePlayerData->TraitConfigs[activeConfig].ID);
+
+ for (UF::TraitConfig const& traitConfig : m_activePlayerData->TraitConfigs)
+ {
+ switch (static_cast<TraitConfigType>(*traitConfig.Type))
+ {
+ case TraitConfigType::Combat:
+ if (traitConfig.ID != int32(*m_activePlayerData->ActiveCombatTraitConfigID))
+ continue;
+ break;
+ case TraitConfigType::Profession:
+ if (!HasSkill(traitConfig.SkillLineID))
+ continue;
+ break;
+ default:
+ break;
+ }
+
+ ApplyTraitConfig(traitConfig.ID, true);
+ }
+}
+
void Player::_SaveTalents(CharacterDatabaseTransaction trans)
{
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TALENT);
@@ -26537,6 +26766,97 @@ void Player::_SaveTalents(CharacterDatabaseTransaction trans)
}
}
+void Player::_SaveTraits(CharacterDatabaseTransaction trans)
+{
+ CharacterDatabasePreparedStatement* stmt = nullptr;
+ for (auto& [traitConfigId, state] : m_traitConfigStates)
+ {
+ switch (state)
+ {
+ case PLAYERSPELL_CHANGED:
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TRAIT_ENTRIES);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfigId);
+ trans->Append(stmt);
+
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TRAIT_CONFIGS);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfigId);
+ trans->Append(stmt);
+
+ if (UF::TraitConfig const* traitConfig = GetTraitConfig(traitConfigId))
+ {
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_TRAIT_CONFIGS);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfig->ID);
+ stmt->setInt32(2, traitConfig->Type);
+ switch (static_cast<TraitConfigType>(*traitConfig->Type))
+ {
+ case TraitConfigType::Combat:
+ stmt->setInt32(3, traitConfig->ChrSpecializationID);
+ stmt->setInt32(4, traitConfig->CombatConfigFlags);
+ stmt->setInt32(5, traitConfig->LocalIdentifier);
+ stmt->setNull(6);
+ stmt->setNull(7);
+ break;
+ case TraitConfigType::Profession:
+ stmt->setNull(3);
+ stmt->setNull(4);
+ stmt->setNull(5);
+ stmt->setInt32(6, traitConfig->SkillLineID);
+ stmt->setNull(7);
+ break;
+ case TraitConfigType::Generic:
+ stmt->setNull(3);
+ stmt->setNull(4);
+ stmt->setNull(5);
+ stmt->setNull(6);
+ stmt->setInt32(7, traitConfig->TraitSystemID);
+ break;
+ default:
+ break;
+ }
+
+ stmt->setString(8, traitConfig->Name);
+ trans->Append(stmt);
+
+ for (UF::TraitEntry const& traitEntry : traitConfig->Entries)
+ {
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_TRAIT_ENTRIES);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfig->ID);
+ stmt->setInt32(2, traitEntry.TraitNodeID);
+ stmt->setInt32(3, traitEntry.TraitNodeEntryID);
+ stmt->setInt32(4, traitEntry.Rank);
+ stmt->setInt32(5, traitEntry.GrantedRanks);
+ trans->Append(stmt);
+ }
+ }
+ break;
+ case PLAYERSPELL_REMOVED:
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TRAIT_ENTRIES);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfigId);
+ trans->Append(stmt);
+
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TRAIT_CONFIGS);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfigId);
+ trans->Append(stmt);
+
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_ACTION_BY_TRAIT_CONFIG);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfigId);
+ trans->Append(stmt);
+ break;
+ default:
+ break;
+ }
+ }
+
+ m_traitConfigStates.clear();
+}
+
void Player::ActivateTalentGroup(ChrSpecializationEntry const* spec)
{
if (GetActiveTalentGroup() == spec->OrderIndex)
@@ -26634,6 +26954,8 @@ void Player::ActivateTalentGroup(ChrSpecializationEntry const* spec)
RemoveOverrideSpell(talentInfo->OverridesSpellID, talentInfo->SpellID);
}
+ ApplyTraitConfig(m_activePlayerData->ActiveCombatTraitConfigID, false);
+
// Remove spec specific spells
RemoveSpecializationSpells();
@@ -26642,6 +26964,16 @@ void Player::ActivateTalentGroup(ChrSpecializationEntry const* spec)
SetActiveTalentGroup(spec->OrderIndex);
SetPrimarySpecialization(spec->ID);
+ int32 specTraitConfigIndex = m_activePlayerData->TraitConfigs.FindIndexIf([spec](UF::TraitConfig const& traitConfig)
+ {
+ return static_cast<TraitConfigType>(*traitConfig.Type) == TraitConfigType::Combat
+ && traitConfig.ChrSpecializationID == int32(spec->ID)
+ && (static_cast<TraitCombatConfigFlags>(*traitConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) != TraitCombatConfigFlags::None;
+ });
+ if (specTraitConfigIndex >= 0)
+ SetActiveCombatTraitConfigID(m_activePlayerData->TraitConfigs[specTraitConfigIndex].ID);
+ else
+ SetActiveCombatTraitConfigID(0);
for (uint32 talentId = 0; talentId < sTalentStore.GetNumRows(); ++talentId)
{
@@ -26684,24 +27016,11 @@ void Player::ActivateTalentGroup(ChrSpecializationEntry const* spec)
if (uint32 mastery = spec->MasterySpellID[i])
LearnSpell(mastery, true);
+ ApplyTraitConfig(m_activePlayerData->ActiveCombatTraitConfigID, true);
+
InitTalentForLevel();
- // load them asynchronously
- {
- CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_ACTIONS_SPEC);
- stmt->setUInt64(0, GetGUID().GetCounter());
- stmt->setUInt8(1, GetActiveTalentGroup());
-
- WorldSession* mySess = GetSession();
- mySess->GetQueryProcessor().AddCallback(CharacterDatabase.AsyncQuery(stmt)
- .WithPreparedCallback([mySess](PreparedQueryResult result)
- {
- // safe callback, we can't pass this pointer directly
- // in case player logs out before db response (player would be deleted in that case)
- if (Player* thisPlayer = mySess->GetPlayer())
- thisPlayer->LoadActions(result);
- }));
- }
+ StartLoadingActionButtons();
UpdateDisplayPower();
Powers pw = GetPowerType();
@@ -26759,6 +27078,48 @@ void Player::ActivateTalentGroup(ChrSpecializationEntry const* spec)
}
}
+void Player::StartLoadingActionButtons(std::function<void()>&& callback /*= nullptr*/)
+{
+ int32 traitConfigId = [&]() -> int32
+ {
+ UF::TraitConfig const* traitConfig = GetTraitConfig(m_activePlayerData->ActiveCombatTraitConfigID);
+ if (!traitConfig)
+ return 0;
+
+ int32 usedSavedTraitConfigIndex = m_activePlayerData->TraitConfigs.FindIndexIf([localIdent = *traitConfig->LocalIdentifier](UF::TraitConfig const& savedConfig)
+ {
+ return static_cast<TraitConfigType>(*savedConfig.Type) == TraitConfigType::Combat
+ && (static_cast<TraitCombatConfigFlags>(*savedConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) == TraitCombatConfigFlags::None
+ && (static_cast<TraitCombatConfigFlags>(*savedConfig.CombatConfigFlags) & TraitCombatConfigFlags::SharedActionBars) == TraitCombatConfigFlags::None
+ && savedConfig.LocalIdentifier == localIdent;
+ });
+
+ if (usedSavedTraitConfigIndex >= 0)
+ return m_activePlayerData->TraitConfigs[usedSavedTraitConfigIndex].ID;
+
+ return 0;
+ }();
+
+ // load them asynchronously
+ CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_ACTIONS_SPEC);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setUInt8(1, GetActiveTalentGroup());
+ stmt->setInt32(2, traitConfigId);
+
+ WorldSession* mySess = GetSession();
+ mySess->GetQueryProcessor().AddCallback(CharacterDatabase.AsyncQuery(stmt)
+ .WithPreparedCallback([mySess, myGuid = GetGUID(), callback = std::move(callback)](PreparedQueryResult result)
+ {
+ // safe callback, we can't pass this pointer directly
+ // in case player logs out before db response (player would be deleted in that case)
+ if (Player* thisPlayer = mySess->GetPlayer(); thisPlayer && thisPlayer->GetGUID() == myGuid)
+ thisPlayer->LoadActions(result);
+
+ if (callback)
+ callback();
+ }));
+}
+
void Player::LoadActions(PreparedQueryResult result)
{
_LoadActions(result);
@@ -26766,6 +27127,368 @@ void Player::LoadActions(PreparedQueryResult result)
SendActionButtons(1);
}
+void Player::CreateTraitConfig(WorldPackets::Traits::TraitConfig& traitConfig)
+{
+ uint32 configId = TraitMgr::GenerateNewTraitConfigId();
+ auto hasConfigId = [&](int32 id)
+ {
+ return m_activePlayerData->TraitConfigs.FindIndexIf([id](UF::TraitConfig const& config) { return config.ID == id; }) >= 0;
+ };
+
+ while (hasConfigId(configId))
+ configId = TraitMgr::GenerateNewTraitConfigId();
+
+ traitConfig.ID = configId;
+
+ int32 traitConfigIndex = m_activePlayerData->TraitConfigs.size();
+ AddTraitConfig(traitConfig);
+
+ for (UF::TraitEntry const& grantedEntry : TraitMgr::GetGrantedTraitEntriesForConfig(traitConfig, this))
+ {
+ auto entryItr = std::find_if(traitConfig.Entries.begin(), traitConfig.Entries.end(),
+ [&](WorldPackets::Traits::TraitEntry const& entry) { return entry.TraitNodeID == grantedEntry.TraitNodeID && entry.TraitNodeEntryID == grantedEntry.TraitNodeEntryID; });
+
+ if (entryItr == traitConfig.Entries.end())
+ AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, traitConfigIndex)
+ .ModifyValue(&UF::TraitConfig::Entries)) = grantedEntry;
+ }
+
+ m_traitConfigStates[configId] = PLAYERSPELL_CHANGED;
+}
+
+void Player::AddTraitConfig(WorldPackets::Traits::TraitConfig const& traitConfig)
+{
+ auto setter = AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::TraitConfigs));
+ setter.ModifyValue(&UF::TraitConfig::ID).SetValue(traitConfig.ID);
+ setter.ModifyValue(&UF::TraitConfig::Name).SetValue(traitConfig.Name);
+ setter.ModifyValue(&UF::TraitConfig::Type).SetValue(AsUnderlyingType(traitConfig.Type));
+ setter.ModifyValue(&UF::TraitConfig::SkillLineID).SetValue(traitConfig.SkillLineID);;
+ setter.ModifyValue(&UF::TraitConfig::ChrSpecializationID).SetValue(traitConfig.ChrSpecializationID);
+ setter.ModifyValue(&UF::TraitConfig::CombatConfigFlags).SetValue(AsUnderlyingType(traitConfig.CombatConfigFlags));
+ setter.ModifyValue(&UF::TraitConfig::LocalIdentifier).SetValue(traitConfig.LocalIdentifier);
+ setter.ModifyValue(&UF::TraitConfig::TraitSystemID).SetValue(traitConfig.TraitSystemID);
+
+ for (WorldPackets::Traits::TraitEntry const& traitEntry : traitConfig.Entries)
+ {
+ UF::TraitEntry& newEntry = AddDynamicUpdateFieldValue(setter.ModifyValue(&UF::TraitConfig::Entries));
+ newEntry.TraitNodeID = traitEntry.TraitNodeID;
+ newEntry.TraitNodeEntryID = traitEntry.TraitNodeEntryID;
+ newEntry.Rank = traitEntry.Rank;
+ newEntry.GrantedRanks = traitEntry.GrantedRanks;
+ }
+}
+
+UF::TraitConfig const* Player::GetTraitConfig(int32 configId) const
+{
+ int32 index = m_activePlayerData->TraitConfigs.FindIndexIf([configId](UF::TraitConfig const& config) { return config.ID == configId; });
+ if (index < 0)
+ return nullptr;
+
+ return &m_activePlayerData->TraitConfigs[index];
+}
+
+void Player::UpdateTraitConfig(WorldPackets::Traits::TraitConfig&& newConfig, int32 savedConfigId, bool withCastTime)
+{
+ int32 index = m_activePlayerData->TraitConfigs.FindIndexIf([&](UF::TraitConfig const& config) { return config.ID == newConfig.ID; });
+ if (index < 0)
+ return;
+
+ if (withCastTime)
+ {
+ CastSpell(this, TraitMgr::COMMIT_COMBAT_TRAIT_CONFIG_CHANGES_SPELL_ID, CastSpellExtraArgs(SPELLVALUE_BASE_POINT0, savedConfigId).SetCustomArg(std::move(newConfig)));
+ return;
+ }
+
+ bool isActiveConfig = true;
+ bool loadActionButtons = false;
+ switch (TraitConfigType(*m_activePlayerData->TraitConfigs[index].Type))
+ {
+ case TraitConfigType::Combat:
+ isActiveConfig = newConfig.ID == int32(*m_activePlayerData->ActiveCombatTraitConfigID);
+ loadActionButtons = m_activePlayerData->TraitConfigs[index].LocalIdentifier != newConfig.LocalIdentifier;
+ break;
+ case TraitConfigType::Profession:
+ isActiveConfig = HasSkill(m_activePlayerData->TraitConfigs[index].SkillLineID);
+ break;
+ default:
+ break;
+ }
+
+ std::function<void()> finalizeTraitConfigUpdate = [=, newConfig = std::move(newConfig)]()
+ {
+ SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, index)
+ .ModifyValue(&UF::TraitConfig::LocalIdentifier), newConfig.LocalIdentifier);
+
+ ApplyTraitEntryChanges(newConfig.ID, newConfig, isActiveConfig, true);
+
+ if (savedConfigId)
+ ApplyTraitEntryChanges(savedConfigId, newConfig, false, false);
+
+ if (EnumFlag(newConfig.CombatConfigFlags).HasFlag(TraitCombatConfigFlags::StarterBuild))
+ SetTraitConfigUseStarterBuild(newConfig.ID, true);
+ };
+
+ if (loadActionButtons)
+ {
+ CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
+ _SaveActions(trans);
+ CharacterDatabase.CommitTransaction(trans);
+
+ StartLoadingActionButtons(std::move(finalizeTraitConfigUpdate));
+ }
+ else
+ finalizeTraitConfigUpdate();
+}
+
+void Player::ApplyTraitEntryChanges(int32 editedConfigId, WorldPackets::Traits::TraitConfig const& newConfig, bool applyTraits, bool consumeCurrencies)
+{
+ int32 editedIndex = m_activePlayerData->TraitConfigs.FindIndexIf([editedConfigId](UF::TraitConfig const& config) { return config.ID == editedConfigId; });
+ if (editedIndex < 0)
+ return;
+
+ auto makeTraitEntryFinder = [](int32 traitNodeId, int32 traitNodeEntryId)
+ {
+ return [=](auto const& ufEntry) { return ufEntry.TraitNodeID == traitNodeId && ufEntry.TraitNodeEntryID == traitNodeEntryId; };
+ };
+
+ UF::TraitConfig const& editedConfig = m_activePlayerData->TraitConfigs[editedIndex];
+
+ // remove traits not found in new config
+ std::set<int32, std::greater<>> entryIndicesToRemove;
+ for (int32 i = 0; i < int32(editedConfig.Entries.size()); ++i)
+ {
+ UF::TraitEntry const& oldEntry = editedConfig.Entries[i];
+ auto entryItr = std::find_if(newConfig.Entries.begin(), newConfig.Entries.end(), makeTraitEntryFinder(oldEntry.TraitNodeID, oldEntry.TraitNodeEntryID));
+ if (entryItr != newConfig.Entries.end())
+ continue;
+
+ if (applyTraits)
+ ApplyTraitEntry(oldEntry.TraitNodeEntryID, 0, 0, false);
+
+ entryIndicesToRemove.insert(i);
+ }
+
+ for (int32 indexToRemove : entryIndicesToRemove)
+ {
+ RemoveDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, editedIndex)
+ .ModifyValue(&UF::TraitConfig::Entries), indexToRemove);
+ }
+
+ std::vector<WorldPackets::Traits::TraitEntry> costEntries;
+
+ // apply new traits
+ for (std::size_t i = 0; i < newConfig.Entries.size(); ++i)
+ {
+ WorldPackets::Traits::TraitEntry const& newEntry = newConfig.Entries[i];
+ int32 oldEntryIndex = editedConfig.Entries.FindIndexIf(makeTraitEntryFinder(newEntry.TraitNodeID, newEntry.TraitNodeEntryID));
+ if (oldEntryIndex < 0)
+ {
+ if (consumeCurrencies)
+ costEntries.push_back(newEntry);
+
+ UF::TraitEntry& newUfEntry = AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, editedIndex)
+ .ModifyValue(&UF::TraitConfig::Entries));
+ newUfEntry.TraitNodeID = newEntry.TraitNodeID;
+ newUfEntry.TraitNodeEntryID = newEntry.TraitNodeEntryID;
+ newUfEntry.Rank = newEntry.Rank;
+ newUfEntry.GrantedRanks = newEntry.GrantedRanks;
+
+ if (applyTraits)
+ ApplyTraitEntry(newUfEntry.TraitNodeEntryID, newUfEntry.Rank, 0, true);
+ }
+ else if (newEntry.Rank != editedConfig.Entries[oldEntryIndex].Rank || newEntry.GrantedRanks != editedConfig.Entries[oldEntryIndex].GrantedRanks)
+ {
+ if (consumeCurrencies && newEntry.Rank > editedConfig.Entries[oldEntryIndex].Rank)
+ {
+ WorldPackets::Traits::TraitEntry& costEntry = costEntries.emplace_back(newEntry);
+ costEntry.Rank -= editedConfig.Entries[oldEntryIndex].Rank;
+ }
+
+ SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, editedIndex)
+ .ModifyValue(&UF::TraitConfig::Entries, oldEntryIndex)
+ .ModifyValue(&UF::TraitEntry::Rank), newEntry.Rank);
+
+ SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, editedIndex)
+ .ModifyValue(&UF::TraitConfig::Entries, oldEntryIndex)
+ .ModifyValue(&UF::TraitEntry::GrantedRanks), newEntry.GrantedRanks);
+
+ if (applyTraits)
+ ApplyTraitEntry(newEntry.TraitNodeEntryID, newEntry.Rank, newEntry.GrantedRanks, true);
+ }
+ }
+
+ if (consumeCurrencies)
+ {
+ std::map<int32, int32> currencies;
+ for (WorldPackets::Traits::TraitEntry const& costEntry : costEntries)
+ TraitMgr::FillSpentCurrenciesMap(costEntry, currencies);
+
+ for (auto [traitCurrencyId, amount] : currencies)
+ {
+ TraitCurrencyEntry const* traitCurrency = sTraitCurrencyStore.LookupEntry(traitCurrencyId);
+ if (!traitCurrency)
+ continue;
+
+ switch (traitCurrency->GetType())
+ {
+ case TraitCurrencyType::Gold:
+ ModifyMoney(-amount);
+ break;
+ case TraitCurrencyType::CurrencyTypesBased:
+ ModifyCurrency(traitCurrency->CurrencyTypesID, -amount);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ m_traitConfigStates[editedConfigId] = PLAYERSPELL_CHANGED;
+}
+
+void Player::RenameTraitConfig(int32 editedConfigId, std::string&& newName)
+{
+ int32 editedIndex = m_activePlayerData->TraitConfigs.FindIndexIf([editedConfigId](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.ID == editedConfigId
+ && static_cast<TraitConfigType>(*traitConfig.Type) == TraitConfigType::Combat
+ && (static_cast<TraitCombatConfigFlags>(*traitConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) == TraitCombatConfigFlags::None;
+ });
+ if (editedIndex < 0)
+ return;
+
+ SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, editedIndex)
+ .ModifyValue(&UF::TraitConfig::Name), std::move(newName));
+
+ m_traitConfigStates[editedConfigId] = PLAYERSPELL_CHANGED;
+}
+
+void Player::DeleteTraitConfig(int32 deletedConfigId)
+{
+ int32 deletedIndex = m_activePlayerData->TraitConfigs.FindIndexIf([deletedConfigId](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.ID == deletedConfigId
+ && static_cast<TraitConfigType>(*traitConfig.Type) == TraitConfigType::Combat
+ && (static_cast<TraitCombatConfigFlags>(*traitConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) == TraitCombatConfigFlags::None;
+ });
+ if (deletedIndex < 0)
+ return;
+
+ RemoveDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs), deletedIndex);
+
+ m_traitConfigStates[deletedConfigId] = PLAYERSPELL_REMOVED;
+}
+
+void Player::ApplyTraitConfig(int32 configId, bool apply)
+{
+ UF::TraitConfig const* traitConfig = GetTraitConfig(configId);
+ if (!traitConfig)
+ return;
+
+ for (UF::TraitEntry const& traitEntry : traitConfig->Entries)
+ ApplyTraitEntry(traitEntry.TraitNodeEntryID, traitEntry.Rank, traitEntry.GrantedRanks, apply);
+}
+
+void Player::ApplyTraitEntry(int32 traitNodeEntryId, int32 /*rank*/, int32 /*grantedRanks*/, bool apply)
+{
+ TraitNodeEntryEntry const* traitNodeEntry = sTraitNodeEntryStore.LookupEntry(traitNodeEntryId);
+ if (!traitNodeEntry)
+ return;
+
+ TraitDefinitionEntry const* traitDefinition = sTraitDefinitionStore.LookupEntry(traitNodeEntry->TraitDefinitionID);
+ if (!traitDefinition)
+ return;
+
+ if (traitDefinition->SpellID)
+ {
+ if (apply)
+ LearnSpell(traitDefinition->SpellID, true, 0, false, traitNodeEntry->TraitDefinitionID);
+ else
+ RemoveSpell(traitDefinition->SpellID);
+ }
+}
+
+void Player::SetTraitConfigUseStarterBuild(int32 traitConfigId, bool useStarterBuild)
+{
+ int32 configIndex = m_activePlayerData->TraitConfigs.FindIndexIf([traitConfigId](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.ID == traitConfigId
+ && static_cast<TraitConfigType>(*traitConfig.Type) == TraitConfigType::Combat
+ && (static_cast<TraitCombatConfigFlags>(*traitConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) != TraitCombatConfigFlags::None;
+ });
+ if (configIndex < 0)
+ return;
+
+ bool currentlyUsesStarterBuild = EnumFlag(static_cast<TraitCombatConfigFlags>(*m_activePlayerData->TraitConfigs[configIndex].CombatConfigFlags)).HasFlag(TraitCombatConfigFlags::StarterBuild);
+ if (currentlyUsesStarterBuild == useStarterBuild)
+ return;
+
+ if (useStarterBuild)
+ SetUpdateFieldFlagValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, configIndex)
+ .ModifyValue(&UF::TraitConfig::CombatConfigFlags), AsUnderlyingType(TraitCombatConfigFlags::StarterBuild));
+ else
+ RemoveUpdateFieldFlagValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, configIndex)
+ .ModifyValue(&UF::TraitConfig::CombatConfigFlags), AsUnderlyingType(TraitCombatConfigFlags::StarterBuild));
+
+ m_traitConfigStates[traitConfigId] = PLAYERSPELL_CHANGED;
+}
+
+void Player::SetTraitConfigUseSharedActionBars(int32 traitConfigId, bool usesSharedActionBars, bool isLastSelectedSavedConfig)
+{
+ int32 configIndex = m_activePlayerData->TraitConfigs.FindIndexIf([traitConfigId](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.ID == traitConfigId
+ && static_cast<TraitConfigType>(*traitConfig.Type) == TraitConfigType::Combat
+ && (static_cast<TraitCombatConfigFlags>(*traitConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) == TraitCombatConfigFlags::None;
+ });
+ if (configIndex < 0)
+ return;
+
+ bool currentlyUsesSharedActionBars = EnumFlag(static_cast<TraitCombatConfigFlags>(*m_activePlayerData->TraitConfigs[configIndex].CombatConfigFlags)).HasFlag(TraitCombatConfigFlags::SharedActionBars);
+ if (currentlyUsesSharedActionBars == usesSharedActionBars)
+ return;
+
+ if (usesSharedActionBars)
+ {
+ SetUpdateFieldFlagValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, configIndex)
+ .ModifyValue(&UF::TraitConfig::CombatConfigFlags), AsUnderlyingType(TraitCombatConfigFlags::SharedActionBars));
+
+ {
+ CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_ACTION_BY_TRAIT_CONFIG);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfigId);
+ CharacterDatabase.Execute(stmt);
+ }
+
+ if (isLastSelectedSavedConfig)
+ StartLoadingActionButtons(); // load action buttons that were saved in shared mode
+ }
+ else
+ {
+ RemoveUpdateFieldFlagValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, configIndex)
+ .ModifyValue(&UF::TraitConfig::CombatConfigFlags), AsUnderlyingType(TraitCombatConfigFlags::SharedActionBars));
+
+ // trigger a save with traitConfigId
+ for (auto&& [_, button] : m_actionButtons)
+ if (button.uState != ACTIONBUTTON_DELETED)
+ button.uState = ACTIONBUTTON_NEW;
+ }
+
+ m_traitConfigStates[traitConfigId] = PLAYERSPELL_CHANGED;
+}
+
void Player::SetReputation(uint32 factionentry, int32 value)
{
GetReputationMgr().SetReputation(sFactionStore.LookupEntry(factionentry), value);
diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h
index ff4b3b1afa9..1c108fc4cdb 100644
--- a/src/server/game/Entities/Player/Player.h
+++ b/src/server/game/Entities/Player/Player.h
@@ -120,6 +120,12 @@ namespace WorldPackets
{
enum class UpdateCollisionHeightReason : uint8;
}
+
+ namespace Traits
+ {
+ struct TraitConfig;
+ struct TraitEntry;
+ }
}
TC_GAME_API uint32 GetBagSize(Bag const* bag);
@@ -178,10 +184,11 @@ enum PlayerSpellState : uint8
struct PlayerSpell
{
- PlayerSpellState state : 8;
+ PlayerSpellState state;
bool active : 1; // show in spellbook
bool dependent : 1; // learned as result another spell learn, skill grow, quest reward, etc
bool disabled : 1; // first rank has been learned in result talent learn but currently talent unlearned, save max learned ranks
+ Optional<int32> TraitDefinitionId;
};
struct StoredAuraTeleportLocation
@@ -859,7 +866,6 @@ enum PlayerLoginQueryIndex
PLAYER_LOGIN_QUERY_LOAD_AZERITE_MILESTONE_POWERS,
PLAYER_LOGIN_QUERY_LOAD_AZERITE_UNLOCKED_ESSENCES,
PLAYER_LOGIN_QUERY_LOAD_AZERITE_EMPOWERED,
- PLAYER_LOGIN_QUERY_LOAD_ACTIONS,
PLAYER_LOGIN_QUERY_LOAD_MAILS,
PLAYER_LOGIN_QUERY_LOAD_MAIL_ITEMS,
PLAYER_LOGIN_QUERY_LOAD_MAIL_ITEMS_ARTIFACT,
@@ -901,6 +907,8 @@ 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_TRAIT_ENTRIES,
+ PLAYER_LOGIN_QUERY_LOAD_TRAIT_CONFIGS,
MAX_PLAYER_LOGIN_QUERY
};
@@ -1059,7 +1067,7 @@ struct GroupUpdateCounter
int32 UpdateSequenceNumber;
};
-enum TalentLearnResult
+enum TalentLearnResult : int32
{
TALENT_LEARN_OK = 0,
TALENT_FAILED_UNKNOWN = 1,
@@ -1793,8 +1801,8 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
void SendProficiency(ItemClass itemClass, uint32 itemSubclassMask) const;
void SendKnownSpells();
void SendUnlearnSpells();
- bool AddSpell(uint32 spellId, bool active, bool learning, bool dependent, bool disabled, bool loading = false, int32 fromSkill = 0);
- void LearnSpell(uint32 spell_id, bool dependent, int32 fromSkill = 0, bool suppressMessaging = false);
+ bool AddSpell(uint32 spellId, bool active, bool learning, bool dependent, bool disabled, bool loading = false, int32 fromSkill = 0, Optional<int32> traitDefinitionId = {});
+ void LearnSpell(uint32 spell_id, bool dependent, int32 fromSkill = 0, bool suppressMessaging = false, Optional<int32> traitDefinitionId = {});
void RemoveSpell(uint32 spell_id, bool disabled = false, bool learn_low_rank = true, bool suppressMessaging = false);
void ResetSpells(bool myClassOnly = false);
void LearnCustomSpells();
@@ -1869,8 +1877,23 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
std::vector<uint32> const& GetGlyphs(uint8 spec) const { return _specializationInfo.Glyphs[spec]; }
std::vector<uint32>& GetGlyphs(uint8 spec) { return _specializationInfo.Glyphs[spec]; }
ActionButtonList const& GetActionButtons() const { return m_actionButtons; }
+ void StartLoadingActionButtons(std::function<void()>&& callback = nullptr);
void LoadActions(PreparedQueryResult result);
+ // Traits
+ void CreateTraitConfig(WorldPackets::Traits::TraitConfig& traitConfig);
+ void AddTraitConfig(WorldPackets::Traits::TraitConfig const& traitConfig);
+ UF::TraitConfig const* GetTraitConfig(int32 configId) const;
+ void UpdateTraitConfig(WorldPackets::Traits::TraitConfig&& newConfig, int32 savedConfigId, bool withCastTime);
+ void ApplyTraitEntryChanges(int32 editedConfigId, WorldPackets::Traits::TraitConfig const& newConfig, bool applyTraits, bool consumeCurrencies);
+ void RenameTraitConfig(int32 editedConfigId, std::string&& newName);
+ void DeleteTraitConfig(int32 deletedConfigId);
+ void ApplyTraitConfig(int32 configId, bool apply);
+ void ApplyTraitEntry(int32 traitNodeEntryId, int32 rank, int32 grantedRanks, bool apply);
+ void SetActiveCombatTraitConfigID(int32 traitConfigId) { SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ActiveCombatTraitConfigID), traitConfigId); }
+ void SetTraitConfigUseStarterBuild(int32 traitConfigId, bool useStarterBuild);
+ void SetTraitConfigUseSharedActionBars(int32 traitConfigId, bool usesSharedActionBars, bool isLastSelectedSavedConfig);
+
uint32 GetFreePrimaryProfessionPoints() const { return m_activePlayerData->CharacterPoints; }
void SetFreePrimaryProfessions(uint16 profs) { SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::CharacterPoints), profs); }
void InitPrimaryProfessions();
@@ -2908,6 +2931,7 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
void _LoadGlyphs(PreparedQueryResult result);
void _LoadTalents(PreparedQueryResult result);
void _LoadPvpTalents(PreparedQueryResult result);
+ void _LoadTraits(PreparedQueryResult configsResult, PreparedQueryResult entriesResult);
void _LoadInstanceTimeRestrictions(PreparedQueryResult result);
void _LoadPetStable(uint32 summonedPetNumber, PreparedQueryResult result);
void _LoadCurrency(PreparedQueryResult result);
@@ -2935,6 +2959,7 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
void _SaveBGData(CharacterDatabaseTransaction trans);
void _SaveGlyphs(CharacterDatabaseTransaction trans) const;
void _SaveTalents(CharacterDatabaseTransaction trans);
+ void _SaveTraits(CharacterDatabaseTransaction trans);
void _SaveStats(CharacterDatabaseTransaction trans) const;
void _SaveInstanceTimeRestrictions(CharacterDatabaseTransaction trans);
void _SaveCurrency(CharacterDatabaseTransaction trans);
@@ -3014,6 +3039,8 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
SpecializationInfo _specializationInfo;
+ std::unordered_map<int32, PlayerSpellState> m_traitConfigStates;
+
ActionButtonList m_actionButtons;
float m_auraBaseFlatMod[BASEMOD_END];
diff --git a/src/server/game/Handlers/CharacterHandler.cpp b/src/server/game/Handlers/CharacterHandler.cpp
index 98adc84d6fe..9a7d20cc1a6 100644
--- a/src/server/game/Handlers/CharacterHandler.cpp
+++ b/src/server/game/Handlers/CharacterHandler.cpp
@@ -176,10 +176,6 @@ bool LoginQueryHolder::Initialize()
stmt->setUInt64(0, lowGuid);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_VOID_STORAGE, stmt);
- stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_ACTIONS);
- stmt->setUInt64(0, lowGuid);
- res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_ACTIONS, stmt);
-
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_MAIL);
stmt->setUInt64(0, lowGuid);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_MAILS, stmt);
@@ -331,6 +327,14 @@ bool LoginQueryHolder::Initialize()
stmt->setUInt64(0, lowGuid);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_GARRISON_FOLLOWER_ABILITIES, stmt);
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_TRAIT_ENTRIES);
+ stmt->setUInt64(0, lowGuid);
+ res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_TRAIT_ENTRIES, stmt);
+
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_TRAIT_CONFIGS);
+ stmt->setUInt64(0, lowGuid);
+ res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_TRAIT_CONFIGS, stmt);
+
return res;
}
diff --git a/src/server/game/Handlers/TraitHandler.cpp b/src/server/game/Handlers/TraitHandler.cpp
new file mode 100644
index 00000000000..ae61b159d62
--- /dev/null
+++ b/src/server/game/Handlers/TraitHandler.cpp
@@ -0,0 +1,253 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include "WorldSession.h"
+#include "Battleground.h"
+#include "DB2Stores.h"
+#include "Player.h"
+#include "SpellHistory.h"
+#include "TraitMgr.h"
+#include "TraitPackets.h"
+
+void WorldSession::HandleTraitsCommitConfig(WorldPackets::Traits::TraitsCommitConfig const& traitsCommitConfig)
+{
+ int32 configId = traitsCommitConfig.Config.ID;
+ UF::TraitConfig const* existingConfig = _player->GetTraitConfig(configId);
+ if (!existingConfig)
+ {
+ SendPacket(WorldPackets::Traits::TraitConfigCommitFailed(configId, 0, TALENT_FAILED_UNKNOWN).Write());
+ return;
+ }
+
+ if (_player->IsInCombat())
+ {
+ SendPacket(WorldPackets::Traits::TraitConfigCommitFailed(configId, 0, TALENT_FAILED_AFFECTING_COMBAT).Write());
+ return;
+ }
+
+ if (_player->GetBattleground() && _player->GetBattleground()->GetStatus() == STATUS_IN_PROGRESS)
+ {
+ SendPacket(WorldPackets::Traits::TraitConfigCommitFailed(configId, 0, TALENT_FAILED_IN_PVP_MATCH).Write());
+ return;
+ }
+
+ auto findEntry = [](WorldPackets::Traits::TraitConfig& config, int32 traitNodeId, int32 traitNodeEntryId) -> WorldPackets::Traits::TraitEntry*
+ {
+ auto entryItr = std::find_if(config.Entries.begin(), config.Entries.end(), [=](WorldPackets::Traits::TraitEntry const& traitEntry)
+ {
+ return traitEntry.TraitNodeID == traitNodeId && traitEntry.TraitNodeEntryID == traitNodeEntryId;
+ });
+ return entryItr != config.Entries.end() ? &*entryItr : nullptr;
+ };
+
+ bool hasRemovedEntries = false;
+ WorldPackets::Traits::TraitConfig newConfigState(*existingConfig);
+ for (WorldPackets::Traits::TraitEntry const& newEntry : traitsCommitConfig.Config.Entries)
+ {
+ WorldPackets::Traits::TraitEntry* traitEntry = findEntry(newConfigState, newEntry.TraitNodeID, newEntry.TraitNodeEntryID);
+ if (traitEntry && traitEntry->Rank > newEntry.Rank)
+ {
+ TraitNodeEntry const* traitNode = sTraitNodeStore.LookupEntry(newEntry.TraitNodeID);
+ if (!traitNode)
+ {
+ SendPacket(WorldPackets::Traits::TraitConfigCommitFailed(configId, 0, TALENT_FAILED_UNKNOWN).Write());
+ return;
+ }
+
+ TraitTreeEntry const* traitTree = sTraitTreeStore.LookupEntry(traitNode->TraitTreeID);
+ if (!traitTree)
+ {
+ SendPacket(WorldPackets::Traits::TraitConfigCommitFailed(configId, 0, TALENT_FAILED_UNKNOWN).Write());
+ return;
+ }
+
+ if (traitTree->GetFlags().HasFlag(TraitTreeFlag::CannotRefund))
+ {
+ SendPacket(WorldPackets::Traits::TraitConfigCommitFailed(configId, 0, TALENT_FAILED_CANT_REMOVE_TALENT).Write());
+ return;
+ }
+
+ TraitNodeEntryEntry const* traitNodeEntry = sTraitNodeEntryStore.LookupEntry(newEntry.TraitNodeEntryID);
+ if (!traitNodeEntry)
+ {
+ SendPacket(WorldPackets::Traits::TraitConfigCommitFailed(configId, 0, TALENT_FAILED_UNKNOWN).Write());
+ return;
+ }
+
+ TraitDefinitionEntry const* traitDefinition = sTraitDefinitionStore.LookupEntry(traitNodeEntry->TraitDefinitionID);
+ if (!traitDefinition)
+ {
+ SendPacket(WorldPackets::Traits::TraitConfigCommitFailed(configId, 0, TALENT_FAILED_UNKNOWN).Write());
+ return;
+ }
+
+ if (traitDefinition->SpellID && _player->GetSpellHistory()->HasCooldown(traitDefinition->SpellID))
+ {
+ SendPacket(WorldPackets::Traits::TraitConfigCommitFailed(configId, traitDefinition->SpellID, TALENT_FAILED_CANT_REMOVE_TALENT).Write());
+ return;
+ }
+
+ if (traitDefinition->VisibleSpellID && _player->GetSpellHistory()->HasCooldown(traitDefinition->VisibleSpellID))
+ {
+ SendPacket(WorldPackets::Traits::TraitConfigCommitFailed(configId, traitDefinition->VisibleSpellID, TALENT_FAILED_CANT_REMOVE_TALENT).Write());
+ return;
+ }
+
+ hasRemovedEntries = true;
+ }
+
+ if (traitEntry)
+ {
+ if (newEntry.Rank)
+ traitEntry->Rank = newEntry.Rank;
+ else
+ newConfigState.Entries.erase(std::remove_if(newConfigState.Entries.begin(), newConfigState.Entries.end(), [&newEntry](WorldPackets::Traits::TraitEntry const& traitEntry)
+ {
+ return traitEntry.TraitNodeID == newEntry.TraitNodeID && traitEntry.TraitNodeEntryID == newEntry.TraitNodeEntryID;
+ }), newConfigState.Entries.end());
+ }
+ else
+ newConfigState.Entries.emplace_back() = newEntry;
+ }
+
+ TalentLearnResult validationResult = TraitMgr::ValidateConfig(newConfigState, _player, true);
+ if (validationResult != TALENT_LEARN_OK)
+ {
+ SendPacket(WorldPackets::Traits::TraitConfigCommitFailed(configId, 0, validationResult).Write());
+ return;
+ }
+
+ bool needsCastTime = newConfigState.Type == TraitConfigType::Combat && hasRemovedEntries;
+
+ if (traitsCommitConfig.SavedLocalIdentifier)
+ newConfigState.LocalIdentifier = traitsCommitConfig.SavedLocalIdentifier;
+ else if (UF::TraitConfig const* savedConfig = _player->GetTraitConfig(traitsCommitConfig.SavedLocalIdentifier))
+ newConfigState.LocalIdentifier = savedConfig->LocalIdentifier;
+
+ _player->UpdateTraitConfig(std::move(newConfigState), traitsCommitConfig.SavedConfigID, needsCastTime);
+}
+
+void WorldSession::HandleClassTalentsRequestNewConfig(WorldPackets::Traits::ClassTalentsRequestNewConfig& classTalentsRequestNewConfig)
+{
+ if (classTalentsRequestNewConfig.Config.Type != TraitConfigType::Combat)
+ return;
+
+ if ((classTalentsRequestNewConfig.Config.CombatConfigFlags & TraitCombatConfigFlags::ActiveForSpec) != TraitCombatConfigFlags::None)
+ return;
+
+ int64 configCount = std::count_if(_player->m_activePlayerData->TraitConfigs.begin(), _player->m_activePlayerData->TraitConfigs.end(), [](UF::TraitConfig const& traitConfig)
+ {
+ return static_cast<TraitConfigType>(*traitConfig.Type) == TraitConfigType::Combat
+ && (static_cast<TraitCombatConfigFlags>(*traitConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) == TraitCombatConfigFlags::None;
+ });
+ if (configCount >= TraitMgr::MAX_COMBAT_TRAIT_CONFIGS)
+ return;
+
+ auto findFreeLocalIdentifier = [&]()
+ {
+ int32 index = 1;
+ while (_player->m_activePlayerData->TraitConfigs.FindIndexIf([&](UF::TraitConfig const& traitConfig)
+ {
+ return static_cast<TraitConfigType>(*traitConfig.Type) == TraitConfigType::Combat
+ && traitConfig.ChrSpecializationID == int32(_player->GetPrimarySpecialization())
+ && traitConfig.LocalIdentifier == index;
+ }) >= 0)
+ ++index;
+
+ return index;
+ };
+
+ classTalentsRequestNewConfig.Config.ChrSpecializationID = _player->GetPrimarySpecialization();
+ classTalentsRequestNewConfig.Config.LocalIdentifier = findFreeLocalIdentifier();
+
+ for (UF::TraitEntry const& grantedEntry : TraitMgr::GetGrantedTraitEntriesForConfig(classTalentsRequestNewConfig.Config, _player))
+ {
+ auto entryItr = std::find_if(classTalentsRequestNewConfig.Config.Entries.begin(), classTalentsRequestNewConfig.Config.Entries.end(),
+ [&](WorldPackets::Traits::TraitEntry const& entry) { return entry.TraitNodeID == grantedEntry.TraitNodeID && entry.TraitNodeEntryID == grantedEntry.TraitNodeEntryID; });
+
+ WorldPackets::Traits::TraitEntry& newEntry = entryItr != classTalentsRequestNewConfig.Config.Entries.end() ? *entryItr : classTalentsRequestNewConfig.Config.Entries.emplace_back();
+ newEntry.TraitNodeID = grantedEntry.TraitNodeID;
+ newEntry.TraitNodeEntryID = grantedEntry.TraitNodeEntryID;
+ newEntry.Rank = grantedEntry.Rank;
+ newEntry.GrantedRanks = grantedEntry.GrantedRanks;
+ if (TraitNodeEntryEntry const* traitNodeEntry = sTraitNodeEntryStore.LookupEntry(grantedEntry.TraitNodeEntryID))
+ if (newEntry.Rank + newEntry.GrantedRanks > traitNodeEntry->MaxRanks)
+ newEntry.Rank = std::max(0, traitNodeEntry->MaxRanks - newEntry.GrantedRanks);
+ }
+
+ TalentLearnResult validationResult = TraitMgr::ValidateConfig(classTalentsRequestNewConfig.Config, _player);
+ if (validationResult != TALENT_LEARN_OK)
+ return;
+
+ _player->CreateTraitConfig(classTalentsRequestNewConfig.Config);
+}
+
+void WorldSession::HandleClassTalentsRenameConfig(WorldPackets::Traits::ClassTalentsRenameConfig& classTalentsRenameConfig)
+{
+ _player->RenameTraitConfig(classTalentsRenameConfig.ConfigID, classTalentsRenameConfig.Name.Move());
+}
+
+void WorldSession::HandleClassTalentsDeleteConfig(WorldPackets::Traits::ClassTalentsDeleteConfig const& classTalentsDeleteConfig)
+{
+ _player->DeleteTraitConfig(classTalentsDeleteConfig.ConfigID);
+}
+
+void WorldSession::HandleClassTalentsSetStarterBuildActive(WorldPackets::Traits::ClassTalentsSetStarterBuildActive const& classTalentsSetStarterBuildActive)
+{
+ UF::TraitConfig const* traitConfig = _player->GetTraitConfig(classTalentsSetStarterBuildActive.ConfigID);
+ if (!traitConfig)
+ return;
+
+ if (static_cast<TraitConfigType>(*traitConfig->Type) != TraitConfigType::Combat)
+ return;
+
+ if (!EnumFlag(static_cast<TraitCombatConfigFlags>(*traitConfig->CombatConfigFlags)).HasFlag(TraitCombatConfigFlags::ActiveForSpec))
+ return;
+
+ if (classTalentsSetStarterBuildActive.Active)
+ {
+ WorldPackets::Traits::TraitConfig newConfigState(*traitConfig);
+
+ auto findFreeLocalIdentifier = [&]()
+ {
+ int32 index = 1;
+ while (_player->m_activePlayerData->TraitConfigs.FindIndexIf([&](UF::TraitConfig const& traitConfig)
+ {
+ return static_cast<TraitConfigType>(*traitConfig.Type) == TraitConfigType::Combat
+ && traitConfig.ChrSpecializationID == int32(_player->GetPrimarySpecialization())
+ && traitConfig.LocalIdentifier == index;
+ }) >= 0)
+ ++index;
+
+ return index;
+ };
+
+ TraitMgr::InitializeStarterBuildTraitConfig(newConfigState, _player);
+ newConfigState.CombatConfigFlags |= TraitCombatConfigFlags::StarterBuild;
+ newConfigState.LocalIdentifier = findFreeLocalIdentifier();
+
+ _player->UpdateTraitConfig(std::move(newConfigState), 0, true);
+ }
+ else
+ _player->SetTraitConfigUseStarterBuild(classTalentsSetStarterBuildActive.ConfigID, false);
+}
+
+void WorldSession::HandleClassTalentsSetUsesSharedActionBars(WorldPackets::Traits::ClassTalentsSetUsesSharedActionBars const& classTalentsSetUsesSharedActionBars)
+{
+ _player->SetTraitConfigUseSharedActionBars(classTalentsSetUsesSharedActionBars.ConfigID, classTalentsSetUsesSharedActionBars.UsesShared,
+ classTalentsSetUsesSharedActionBars.IsLastSelectedSavedConfig);
+}
diff --git a/src/server/game/Server/Packets/AllPackets.h b/src/server/game/Server/Packets/AllPackets.h
index eab189b56e9..a9658aec4eb 100644
--- a/src/server/game/Server/Packets/AllPackets.h
+++ b/src/server/game/Server/Packets/AllPackets.h
@@ -77,6 +77,7 @@
#include "TotemPackets.h"
#include "ToyPackets.h"
#include "TradePackets.h"
+#include "TraitPackets.h"
#include "TransmogrificationPackets.h"
#include "VehiclePackets.h"
#include "VoidStoragePackets.h"
diff --git a/src/server/game/Server/Packets/PacketUtilities.h b/src/server/game/Server/Packets/PacketUtilities.h
index f445169784c..4af2d2c4b17 100644
--- a/src/server/game/Server/Packets/PacketUtilities.h
+++ b/src/server/game/Server/Packets/PacketUtilities.h
@@ -198,6 +198,16 @@ namespace WorldPackets
return _storage.back();
}
+ iterator erase(const_iterator first, const_iterator last)
+ {
+ return _storage.erase(first, last);
+ }
+
+ void clear()
+ {
+ _storage.clear();
+ }
+
private:
storage_type _storage;
};
diff --git a/src/server/game/Server/Packets/TraitPackets.cpp b/src/server/game/Server/Packets/TraitPackets.cpp
new file mode 100644
index 00000000000..2c1770f6aaa
--- /dev/null
+++ b/src/server/game/Server/Packets/TraitPackets.cpp
@@ -0,0 +1,68 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include "TraitPackets.h"
+
+namespace WorldPackets::Traits
+{
+void TraitsCommitConfig::Read()
+{
+ _worldPacket >> Config;
+ _worldPacket >> SavedConfigID;
+ _worldPacket >> SavedLocalIdentifier;
+}
+
+WorldPacket const* TraitConfigCommitFailed::Write()
+{
+ _worldPacket << int32(ConfigID);
+ _worldPacket << int32(SpellID);
+ _worldPacket.WriteBits(Reason, 4);
+ _worldPacket.FlushBits();
+
+ return &_worldPacket;
+}
+
+void ClassTalentsRequestNewConfig::Read()
+{
+ _worldPacket >> Config;
+}
+
+void ClassTalentsRenameConfig::Read()
+{
+ _worldPacket >> ConfigID;
+ uint32 nameLength = _worldPacket.ReadBits(9);
+ Name = _worldPacket.ReadString(nameLength, false);
+}
+
+void ClassTalentsDeleteConfig::Read()
+{
+ _worldPacket >> ConfigID;
+}
+
+void ClassTalentsSetStarterBuildActive::Read()
+{
+ _worldPacket >> ConfigID;
+ Active = _worldPacket.ReadBit();
+}
+
+void ClassTalentsSetUsesSharedActionBars::Read()
+{
+ _worldPacket >> ConfigID;
+ UsesShared = _worldPacket.ReadBit();
+ IsLastSelectedSavedConfig = _worldPacket.ReadBit();
+}
+}
diff --git a/src/server/game/Server/Packets/TraitPackets.h b/src/server/game/Server/Packets/TraitPackets.h
new file mode 100644
index 00000000000..88c2ae43637
--- /dev/null
+++ b/src/server/game/Server/Packets/TraitPackets.h
@@ -0,0 +1,106 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TRINITYCORE_TRAIT_PACKETS_H
+#define TRINITYCORE_TRAIT_PACKETS_H
+
+#include "Packet.h"
+#include "TraitPacketsCommon.h"
+
+namespace WorldPackets::Traits
+{
+class TraitsCommitConfig final : public ClientPacket
+{
+public:
+ explicit TraitsCommitConfig(WorldPacket&& packet) : ClientPacket(CMSG_TRAITS_COMMIT_CONFIG, std::move(packet)) { }
+
+ void Read() override;
+
+ TraitConfig Config;
+ int32 SavedConfigID = 0;
+ int32 SavedLocalIdentifier = 0;
+};
+
+class TraitConfigCommitFailed final : public ServerPacket
+{
+public:
+ TraitConfigCommitFailed(int32 configId = 0, int32 spellId = 0, int32 reason = 0) : ServerPacket(SMSG_TRAIT_CONFIG_COMMIT_FAILED, 4 + 4 + 1),
+ ConfigID(configId), SpellID(spellId), Reason(reason) { }
+
+ WorldPacket const* Write() override;
+
+ int32 ConfigID;
+ int32 SpellID;
+ int32 Reason;
+};
+
+class ClassTalentsRequestNewConfig final : public ClientPacket
+{
+public:
+ explicit ClassTalentsRequestNewConfig(WorldPacket&& packet) : ClientPacket(CMSG_CLASS_TALENTS_REQUEST_NEW_CONFIG, std::move(packet)) { }
+
+ void Read() override;
+
+ TraitConfig Config;
+};
+
+class ClassTalentsRenameConfig final : public ClientPacket
+{
+public:
+ explicit ClassTalentsRenameConfig(WorldPacket&& packet) : ClientPacket(CMSG_CLASS_TALENTS_RENAME_CONFIG, std::move(packet)) { }
+
+ void Read() override;
+
+ int32 ConfigID = 0;
+ String<259> Name;
+};
+
+class ClassTalentsDeleteConfig final : public ClientPacket
+{
+public:
+ explicit ClassTalentsDeleteConfig(WorldPacket&& packet) : ClientPacket(CMSG_CLASS_TALENTS_DELETE_CONFIG, std::move(packet)) { }
+
+ void Read() override;
+
+ int32 ConfigID = 0;
+};
+
+class ClassTalentsSetStarterBuildActive final : public ClientPacket
+{
+public:
+ explicit ClassTalentsSetStarterBuildActive(WorldPacket&& packet) : ClientPacket(CMSG_CLASS_TALENTS_SET_STARTER_BUILD_ACTIVE, std::move(packet)) { }
+
+ void Read() override;
+
+ int32 ConfigID = 0;
+ bool Active = false;
+};
+
+class ClassTalentsSetUsesSharedActionBars final : public ClientPacket
+{
+public:
+ explicit ClassTalentsSetUsesSharedActionBars(WorldPacket&& packet) : ClientPacket(CMSG_CLASS_TALENTS_SET_USES_SHARED_ACTION_BARS, std::move(packet)) { }
+
+ void Read() override;
+
+ int32 ConfigID = 0;
+ bool UsesShared = false;
+ bool IsLastSelectedSavedConfig = false;
+};
+}
+
+#endif // TRINITYCORE_TRAIT_PACKETS_H
diff --git a/src/server/game/Server/Packets/TraitPacketsCommon.cpp b/src/server/game/Server/Packets/TraitPacketsCommon.cpp
index 482f3b5e91d..d6bfe6667cf 100644
--- a/src/server/game/Server/Packets/TraitPacketsCommon.cpp
+++ b/src/server/game/Server/Packets/TraitPacketsCommon.cpp
@@ -17,9 +17,46 @@
#include "TraitPacketsCommon.h"
#include "DBCEnums.h"
+#include "UpdateFields.h"
namespace WorldPackets::Traits
{
+TraitEntry::TraitEntry() = default;
+
+TraitEntry::TraitEntry(UF::TraitEntry const& ufEntry)
+{
+ TraitNodeID = ufEntry.TraitNodeID;
+ TraitNodeEntryID = ufEntry.TraitNodeEntryID;
+ Rank = ufEntry.Rank;
+ GrantedRanks = ufEntry.GrantedRanks;
+}
+
+TraitConfig::TraitConfig() = default;
+
+TraitConfig::TraitConfig(UF::TraitConfig const& ufConfig)
+{
+ ID = ufConfig.ID;
+ Type = static_cast<TraitConfigType>(*ufConfig.Type);
+ ChrSpecializationID = ufConfig.ChrSpecializationID;
+ CombatConfigFlags = static_cast<TraitCombatConfigFlags>(*ufConfig.CombatConfigFlags);
+ LocalIdentifier = ufConfig.LocalIdentifier;
+ SkillLineID = ufConfig.SkillLineID;
+ TraitSystemID = ufConfig.TraitSystemID;
+ for (UF::TraitEntry const& ufEntry : ufConfig.Entries)
+ Entries.emplace_back(ufEntry);
+ Name = ufConfig.Name;
+}
+
+ByteBuffer& operator>>(ByteBuffer& data, TraitEntry& traitEntry)
+{
+ data >> traitEntry.TraitNodeID;
+ data >> traitEntry.TraitNodeEntryID;
+ data >> traitEntry.Rank;
+ data >> traitEntry.GrantedRanks;
+
+ return data;
+}
+
ByteBuffer& operator<<(ByteBuffer& data, TraitEntry const& traitEntry)
{
data << int32(traitEntry.TraitNodeID);
@@ -30,6 +67,37 @@ ByteBuffer& operator<<(ByteBuffer& data, TraitEntry const& traitEntry)
return data;
}
+ByteBuffer& operator>>(ByteBuffer& data, TraitConfig& traitConfig)
+{
+ data >> traitConfig.ID;
+ traitConfig.Type = data.read<TraitConfigType, int32>();
+ traitConfig.Entries.resize(data.read<uint32>());
+ switch (traitConfig.Type)
+ {
+ case TraitConfigType::Combat:
+ data >> traitConfig.ChrSpecializationID;
+ traitConfig.CombatConfigFlags = data.read<TraitCombatConfigFlags, int32>();
+ data >> traitConfig.LocalIdentifier;
+ break;
+ case TraitConfigType::Profession:
+ data >> traitConfig.SkillLineID;
+ break;
+ case TraitConfigType::Generic:
+ data >> traitConfig.TraitSystemID;
+ break;
+ default:
+ break;
+ }
+
+ for (TraitEntry& traitEntry : traitConfig.Entries)
+ data >> traitEntry;
+
+ uint32 nameLength = data.ReadBits(9);
+ traitConfig.Name = data.ReadString(nameLength, false);
+
+ return data;
+}
+
ByteBuffer& operator<<(ByteBuffer& data, TraitConfig const& traitConfig)
{
data << int32(traitConfig.ID);
diff --git a/src/server/game/Server/Packets/TraitPacketsCommon.h b/src/server/game/Server/Packets/TraitPacketsCommon.h
index d61e16ed49b..96186412362 100644
--- a/src/server/game/Server/Packets/TraitPacketsCommon.h
+++ b/src/server/game/Server/Packets/TraitPacketsCommon.h
@@ -23,10 +23,19 @@
enum class TraitCombatConfigFlags : int32;
enum class TraitConfigType : int32;
+namespace UF
+{
+struct TraitConfig;
+struct TraitEntry;
+}
+
namespace WorldPackets::Traits
{
struct TraitEntry
{
+ TraitEntry();
+ explicit TraitEntry(UF::TraitEntry const& ufEntry);
+
int32 TraitNodeID = 0;
int32 TraitNodeEntryID = 0;
int32 Rank = 0;
@@ -35,6 +44,9 @@ struct TraitEntry
struct TraitConfig
{
+ TraitConfig();
+ explicit TraitConfig(UF::TraitConfig const& ufConfig);
+
int32 ID = 0;
TraitConfigType Type = {};
int32 ChrSpecializationID = 0;
@@ -46,7 +58,9 @@ struct TraitConfig
String<259> Name;
};
+ByteBuffer& operator>>(ByteBuffer& data, TraitEntry& traitEntry);
ByteBuffer& operator<<(ByteBuffer& data, TraitEntry const& traitEntry);
+ByteBuffer& operator>>(ByteBuffer& data, TraitConfig& traitConfig);
ByteBuffer& operator<<(ByteBuffer& data, TraitConfig const& traitConfig);
}
diff --git a/src/server/game/Server/Protocol/Opcodes.cpp b/src/server/game/Server/Protocol/Opcodes.cpp
index 8e197c8b98b..5df4f540e4e 100644
--- a/src/server/game/Server/Protocol/Opcodes.cpp
+++ b/src/server/game/Server/Protocol/Opcodes.cpp
@@ -329,13 +329,13 @@ void OpcodeTable::Initialize()
DEFINE_HANDLER(CMSG_CHOICE_RESPONSE, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandlePlayerChoiceResponse);
DEFINE_HANDLER(CMSG_CHROMIE_TIME_SELECT_EXPANSION, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_CLAIM_WEEKLY_REWARD, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL);
- DEFINE_HANDLER(CMSG_CLASS_TALENTS_DELETE_CONFIG, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
+ DEFINE_HANDLER(CMSG_CLASS_TALENTS_DELETE_CONFIG, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleClassTalentsDeleteConfig);
DEFINE_HANDLER(CMSG_CLASS_TALENTS_NOTIFY_EMPTY_CONFIG, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_CLASS_TALENTS_NOTIFY_VALIDATION_FAILED, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
- DEFINE_HANDLER(CMSG_CLASS_TALENTS_RENAME_CONFIG, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
- DEFINE_HANDLER(CMSG_CLASS_TALENTS_REQUEST_NEW_CONFIG, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
- DEFINE_HANDLER(CMSG_CLASS_TALENTS_SET_STARTER_BUILD_ACTIVE, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
- DEFINE_HANDLER(CMSG_CLASS_TALENTS_SET_USES_SHARED_ACTION_BARS, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
+ DEFINE_HANDLER(CMSG_CLASS_TALENTS_RENAME_CONFIG, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleClassTalentsRenameConfig);
+ DEFINE_HANDLER(CMSG_CLASS_TALENTS_REQUEST_NEW_CONFIG, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleClassTalentsRequestNewConfig);
+ DEFINE_HANDLER(CMSG_CLASS_TALENTS_SET_STARTER_BUILD_ACTIVE, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleClassTalentsSetStarterBuildActive);
+ DEFINE_HANDLER(CMSG_CLASS_TALENTS_SET_USES_SHARED_ACTION_BARS, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleClassTalentsSetUsesSharedActionBars);
DEFINE_HANDLER(CMSG_CLEAR_NEW_APPEARANCE, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_CLEAR_RAID_MARKER, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleClearRaidMarker);
DEFINE_HANDLER(CMSG_CLEAR_TRADE_ITEM, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleClearTradeItemOpcode);
@@ -931,7 +931,7 @@ void OpcodeTable::Initialize()
DEFINE_HANDLER(CMSG_TRADE_SKILL_SET_FAVORITE, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_TRAINER_BUY_SPELL, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleTrainerBuySpellOpcode);
DEFINE_HANDLER(CMSG_TRAINER_LIST, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleTrainerListOpcode);
- DEFINE_HANDLER(CMSG_TRAITS_COMMIT_CONFIG, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
+ DEFINE_HANDLER(CMSG_TRAITS_COMMIT_CONFIG, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleTraitsCommitConfig);
DEFINE_HANDLER(CMSG_TRAITS_TALENT_TEST_UNLEARN_SPELLS, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_TRANSMOGRIFY_ITEMS, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleTransmogrifyItems);
DEFINE_HANDLER(CMSG_TURN_IN_PETITION, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleTurnInPetition);
@@ -2070,7 +2070,7 @@ void OpcodeTable::Initialize()
DEFINE_SERVER_OPCODE_HANDLER(SMSG_TRADE_UPDATED, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_TRAINER_BUY_FAILED, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_TRAINER_LIST, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
- DEFINE_SERVER_OPCODE_HANDLER(SMSG_TRAIT_CONFIG_COMMIT_FAILED, STATUS_UNHANDLED, CONNECTION_TYPE_REALM);
+ DEFINE_SERVER_OPCODE_HANDLER(SMSG_TRAIT_CONFIG_COMMIT_FAILED, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_TRANSFER_ABORTED, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_TRANSFER_PENDING, STATUS_NEVER, CONNECTION_TYPE_REALM);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_TREASURE_PICKER_RESPONSE, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE);
diff --git a/src/server/game/Server/WorldSession.h b/src/server/game/Server/WorldSession.h
index 6ff16503847..b26204a0989 100644
--- a/src/server/game/Server/WorldSession.h
+++ b/src/server/game/Server/WorldSession.h
@@ -773,6 +773,16 @@ namespace WorldPackets
class TradeStatus;
}
+ namespace Traits
+ {
+ class TraitsCommitConfig;
+ class ClassTalentsRequestNewConfig;
+ class ClassTalentsRenameConfig;
+ class ClassTalentsDeleteConfig;
+ class ClassTalentsSetStarterBuildActive;
+ class ClassTalentsSetUsesSharedActionBars;
+ }
+
namespace Transmogrification
{
class TransmogrifyItems;
@@ -1497,6 +1507,13 @@ class TC_GAME_API WorldSession
void HandleConfirmRespecWipeOpcode(WorldPackets::Talent::ConfirmRespecWipe& confirmRespecWipe);
void HandleUnlearnSkillOpcode(WorldPackets::Spells::UnlearnSkill& packet);
+ void HandleTraitsCommitConfig(WorldPackets::Traits::TraitsCommitConfig const& traitsCommitConfig);
+ void HandleClassTalentsRequestNewConfig(WorldPackets::Traits::ClassTalentsRequestNewConfig& classTalentsRequestNewConfig);
+ void HandleClassTalentsRenameConfig(WorldPackets::Traits::ClassTalentsRenameConfig& classTalentsRenameConfig);
+ void HandleClassTalentsDeleteConfig(WorldPackets::Traits::ClassTalentsDeleteConfig const& classTalentsDeleteConfig);
+ void HandleClassTalentsSetStarterBuildActive(WorldPackets::Traits::ClassTalentsSetStarterBuildActive const& classTalentsSetStarterBuildActive);
+ void HandleClassTalentsSetUsesSharedActionBars(WorldPackets::Traits::ClassTalentsSetUsesSharedActionBars const& classTalentsSetUsesSharedActionBars);
+
void HandleQuestgiverStatusQueryOpcode(WorldPackets::Quest::QuestGiverStatusQuery& packet);
void HandleQuestgiverStatusMultipleQuery(WorldPackets::Quest::QuestGiverStatusMultipleQuery& packet);
void HandleQuestgiverHelloOpcode(WorldPackets::Quest::QuestGiverHello& packet);
diff --git a/src/server/game/Spells/Spell.cpp b/src/server/game/Spells/Spell.cpp
index 2cd10e0b4e8..58007c844ac 100644
--- a/src/server/game/Spells/Spell.cpp
+++ b/src/server/game/Spells/Spell.cpp
@@ -55,6 +55,7 @@
#include "SpellScript.h"
#include "TemporarySummon.h"
#include "TradeData.h"
+#include "TraitPackets.h"
#include "Util.h"
#include "VMapFactory.h"
#include "Vehicle.h"
@@ -4222,7 +4223,14 @@ void Spell::finish(bool ok)
Unit::ProcSkillsAndAuras(unitCaster, nullptr, PROC_FLAG_CAST_ENDED, PROC_FLAG_NONE, PROC_SPELL_TYPE_MASK_ALL, PROC_SPELL_PHASE_NONE, PROC_HIT_NONE, this, nullptr, nullptr);
if (!ok)
+ {
+ // on failure (or manual cancel) send TraitConfigCommitFailed to revert talent UI saved config selection
+ if (m_caster->IsPlayer() && m_spellInfo->HasEffect(SPELL_EFFECT_CHANGE_ACTIVE_COMBAT_TRAIT_CONFIG))
+ if (WorldPackets::Traits::TraitConfig const* traitConfig = std::any_cast<WorldPackets::Traits::TraitConfig>(&m_customArg))
+ m_caster->ToPlayer()->SendDirectMessage(WorldPackets::Traits::TraitConfigCommitFailed(traitConfig->ID).Write());
+
return;
+ }
if (unitCaster->GetTypeId() == TYPEID_UNIT && unitCaster->IsSummon())
{
diff --git a/src/server/game/Spells/Spell.h b/src/server/game/Spells/Spell.h
index 41b8b99160c..1ee8a54a5c3 100644
--- a/src/server/game/Spells/Spell.h
+++ b/src/server/game/Spells/Spell.h
@@ -408,6 +408,8 @@ class TC_GAME_API Spell
void EffectModifyCooldowns();
void EffectModifyCooldownsByCategory();
void EffectModifySpellCharges();
+ void EffectCreateTraitTreeConfig();
+ void EffectChangeActiveCombatTraitConfig();
typedef std::unordered_set<Aura*> UsedSpellMods;
diff --git a/src/server/game/Spells/SpellEffects.cpp b/src/server/game/Spells/SpellEffects.cpp
index 2bde0329e6f..bc768ab550b 100644
--- a/src/server/game/Spells/SpellEffects.cpp
+++ b/src/server/game/Spells/SpellEffects.cpp
@@ -75,6 +75,8 @@
#include "TalentPackets.h"
#include "TemporarySummon.h"
#include "Totem.h"
+#include "TraitMgr.h"
+#include "TraitPacketsCommon.h"
#include "Unit.h"
#include "Util.h"
#include "World.h"
@@ -386,8 +388,8 @@ NonDefaultConstructible<SpellEffectHandlerFn> SpellEffectHandlers[TOTAL_SPELL_EF
&Spell::EffectUnused, //300 SPELL_EFFECT_300
&Spell::EffectNULL, //301 SPELL_EFFECT_CRAFT_ENCHANT
&Spell::EffectNULL, //302 SPELL_EFFECT_GATHERING
- &Spell::EffectNULL, //303 SPELL_EFFECT_CREATE_TRAIT_TREE_CONFIG
- &Spell::EffectNULL, //304 SPELL_EFFECT_CHANGE_ACTIVE_COMBAT_TRAIT_CONFIG
+ &Spell::EffectCreateTraitTreeConfig, //303 SPELL_EFFECT_CREATE_TRAIT_TREE_CONFIG
+ &Spell::EffectChangeActiveCombatTraitConfig, //304 SPELL_EFFECT_CHANGE_ACTIVE_COMBAT_TRAIT_CONFIG
};
void Spell::EffectNULL()
@@ -5873,3 +5875,37 @@ void Spell::EffectModifySpellCharges()
for (int32 i = 0; i < damage; ++i)
unitTarget->GetSpellHistory()->RestoreCharge(effectInfo->MiscValue);
}
+
+void Spell::EffectCreateTraitTreeConfig()
+{
+ if (effectHandleMode != SPELL_EFFECT_HANDLE_HIT_TARGET)
+ return;
+
+ Player* target = Object::ToPlayer(unitTarget);
+ if (!target)
+ return;
+
+ WorldPackets::Traits::TraitConfig newConfig;
+ newConfig.Type = TraitMgr::GetConfigTypeForTree(effectInfo->MiscValue);
+ if (newConfig.Type != TraitConfigType::Generic)
+ return;
+
+ newConfig.TraitSystemID = sTraitTreeStore.AssertEntry(effectInfo->MiscValue)->TraitSystemID;
+ target->CreateTraitConfig(newConfig);
+}
+
+void Spell::EffectChangeActiveCombatTraitConfig()
+{
+ if (effectHandleMode != SPELL_EFFECT_HANDLE_HIT_TARGET)
+ return;
+
+ Player* target = Object::ToPlayer(unitTarget);
+ if (!target)
+ return;
+
+ WorldPackets::Traits::TraitConfig* traitConfig = std::any_cast<WorldPackets::Traits::TraitConfig>(&m_customArg);
+ if (!traitConfig)
+ return;
+
+ target->UpdateTraitConfig(std::move(*traitConfig), damage, false);
+}
diff --git a/src/server/game/Spells/TraitMgr.cpp b/src/server/game/Spells/TraitMgr.cpp
new file mode 100644
index 00000000000..33d5f215f2c
--- /dev/null
+++ b/src/server/game/Spells/TraitMgr.cpp
@@ -0,0 +1,735 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include "TraitMgr.h"
+#include "Containers.h"
+#include "DB2Stores.h"
+#include "IteratorPair.h"
+#include "Player.h"
+#include "TraitPacketsCommon.h"
+
+namespace TraitMgr
+{
+namespace
+{
+struct NodeEntry;
+struct Node;
+struct NodeGroup;
+struct Tree;
+
+struct NodeEntry
+{
+ TraitNodeEntryEntry const* Data = nullptr;
+ std::vector<TraitCondEntry const*> Conditions;
+ std::vector<TraitCostEntry const*> Costs;
+};
+
+struct Node
+{
+ TraitNodeEntry const* Data = nullptr;
+ std::vector<NodeEntry> Entries;
+ std::vector<NodeGroup const*> Groups;
+ std::vector<std::pair<Node const*, TraitEdgeType>> ParentNodes; // TraitEdge::LeftTraitNodeID
+ std::vector<TraitCondEntry const*> Conditions;
+ std::vector<TraitCostEntry const*> Costs;
+};
+
+struct NodeGroup
+{
+ TraitNodeGroupEntry const* Data = nullptr;
+ std::vector<TraitCondEntry const*> Conditions;
+ std::vector<TraitCostEntry const*> Costs;
+ std::vector<Node const*> Nodes;
+};
+
+struct Tree
+{
+ TraitTreeEntry const* Data = nullptr;
+ std::vector<Node const*> Nodes;
+ std::vector<TraitCostEntry const*> Costs;
+ std::vector<TraitCurrencyEntry const*> Currencies;
+ TraitConfigType ConfigType = TraitConfigType::Invalid;
+};
+
+std::unordered_map<int32, NodeGroup> _traitGroups;
+std::unordered_map<int32, Node> _traitNodes;
+std::unordered_map<int32, Tree> _traitTrees;
+std::array<int32, MAX_CLASSES> _skillLinesByClass;
+std::unordered_map<int32, std::vector<Tree const*>> _traitTreesBySkillLine;
+std::unordered_map<int32, std::vector<Tree const*>> _traitTreesByTraitSystem;
+int32 _configIdGenerator = 0;
+std::unordered_map<int32, std::vector<TraitCurrencySourceEntry const*>> _traitCurrencySourcesByCurrency;
+std::unordered_map<int32, std::vector<TraitDefinitionEffectPointsEntry const*>> _traitDefinitionEffectPointModifiers;
+std::unordered_map<int32, std::vector<TraitTreeLoadoutEntryEntry const*>> _traitTreeLoadoutsByChrSpecialization;
+}
+
+void Load()
+{
+ _configIdGenerator = time(nullptr);
+
+ std::unordered_map<int32, std::vector<TraitCondEntry const*>> nodeEntryConditions;
+ for (TraitNodeEntryXTraitCondEntry const* traitNodeEntryXTraitCondEntry : sTraitNodeEntryXTraitCondStore)
+ if (TraitCondEntry const* traitCondEntry = sTraitCondStore.LookupEntry(traitNodeEntryXTraitCondEntry->TraitCondID))
+ nodeEntryConditions[traitNodeEntryXTraitCondEntry->TraitNodeEntryID].push_back(traitCondEntry);
+
+ std::unordered_map<int32, std::vector<TraitCostEntry const*>> nodeEntryCosts;
+ for (TraitNodeEntryXTraitCostEntry const* traitNodeEntryXTraitCostEntry : sTraitNodeEntryXTraitCostStore)
+ if (TraitCostEntry const* traitCostEntry = sTraitCostStore.LookupEntry(traitNodeEntryXTraitCostEntry->TraitCostID))
+ nodeEntryCosts[traitNodeEntryXTraitCostEntry->TraitNodeEntryID].push_back(traitCostEntry);
+
+ std::unordered_map<int32, std::vector<TraitCondEntry const*>> nodeGroupConditions;
+ for (TraitNodeGroupXTraitCondEntry const* traitNodeGroupXTraitCondEntry : sTraitNodeGroupXTraitCondStore)
+ if (TraitCondEntry const* traitCondEntry = sTraitCondStore.LookupEntry(traitNodeGroupXTraitCondEntry->TraitCondID))
+ nodeGroupConditions[traitNodeGroupXTraitCondEntry->TraitNodeGroupID].push_back(traitCondEntry);
+
+ std::unordered_map<int32, std::vector<TraitCostEntry const*>> nodeGroupCosts;
+ for (TraitNodeGroupXTraitCostEntry const* traitNodeGroupXTraitCostEntry : sTraitNodeGroupXTraitCostStore)
+ if (TraitCostEntry const* traitCondEntry = sTraitCostStore.LookupEntry(traitNodeGroupXTraitCostEntry->TraitCostID))
+ nodeGroupCosts[traitNodeGroupXTraitCostEntry->TraitNodeGroupID].push_back(traitCondEntry);
+
+ std::unordered_multimap<int32, int32> nodeGroups;
+ for (TraitNodeGroupXTraitNodeEntry const* traitNodeGroupXTraitNodeEntry : sTraitNodeGroupXTraitNodeStore)
+ nodeGroups.emplace(traitNodeGroupXTraitNodeEntry->TraitNodeID, traitNodeGroupXTraitNodeEntry->TraitNodeGroupID);
+
+ std::unordered_map<int32, std::vector<TraitCondEntry const*>> nodeConditions;
+ for (TraitNodeXTraitCondEntry const* traitNodeXTraitCondEntry : sTraitNodeXTraitCondStore)
+ if (TraitCondEntry const* traitCondEntry = sTraitCondStore.LookupEntry(traitNodeXTraitCondEntry->TraitCondID))
+ nodeConditions[traitNodeXTraitCondEntry->TraitNodeID].push_back(traitCondEntry);
+
+ std::unordered_map<int32, std::vector<TraitCostEntry const*>> nodeCosts;
+ for (TraitNodeXTraitCostEntry const* traitNodeXTraitCostEntry : sTraitNodeXTraitCostStore)
+ if (TraitCostEntry const* traitCostEntry = sTraitCostStore.LookupEntry(traitNodeXTraitCostEntry->TraitCostID))
+ nodeCosts[traitNodeXTraitCostEntry->TraitNodeID].push_back(traitCostEntry);
+
+ std::unordered_multimap<int32, TraitNodeEntryEntry const*> nodeEntries;
+ for (TraitNodeXTraitNodeEntryEntry const* traitNodeXTraitNodeEntryEntry : sTraitNodeXTraitNodeEntryStore)
+ if (TraitNodeEntryEntry const* traitNodeEntryEntry = sTraitNodeEntryStore.LookupEntry(traitNodeXTraitNodeEntryEntry->TraitNodeEntryID))
+ nodeEntries.emplace(traitNodeXTraitNodeEntryEntry->TraitNodeID, traitNodeEntryEntry);
+
+ std::unordered_map<int32, std::vector<TraitCostEntry const*>> treeCosts;
+ for (TraitTreeXTraitCostEntry const* traitTreeXTraitCostEntry : sTraitTreeXTraitCostStore)
+ if (TraitCostEntry const* traitCostEntry = sTraitCostStore.LookupEntry(traitTreeXTraitCostEntry->TraitCostID))
+ treeCosts[traitTreeXTraitCostEntry->TraitTreeID].push_back(traitCostEntry);
+
+ std::unordered_map<int32, std::vector<TraitCurrencyEntry const*>> treeCurrencies;
+ for (TraitTreeXTraitCurrencyEntry const* traitTreeXTraitCurrencyEntry : sTraitTreeXTraitCurrencyStore)
+ if (TraitCurrencyEntry const* traitCurrencyEntry = sTraitCurrencyStore.LookupEntry(traitTreeXTraitCurrencyEntry->TraitCurrencyID))
+ treeCurrencies[traitTreeXTraitCurrencyEntry->TraitTreeID].push_back(traitCurrencyEntry);
+
+ std::unordered_map<int32, std::vector<int32>> traitTreesIdsByTraitSystem;
+
+ for (TraitTreeEntry const* traitTree : sTraitTreeStore)
+ {
+ Tree& tree = _traitTrees[traitTree->ID];
+ tree.Data = traitTree;
+
+ if (std::vector<TraitCostEntry const*>* costs = Trinity::Containers::MapGetValuePtr(treeCosts, traitTree->ID))
+ tree.Costs = std::move(*costs);
+
+ if (std::vector<TraitCurrencyEntry const*>* currencies = Trinity::Containers::MapGetValuePtr(treeCurrencies, traitTree->ID))
+ tree.Currencies = std::move(*currencies);
+
+ if (traitTree->TraitSystemID)
+ {
+ traitTreesIdsByTraitSystem[traitTree->TraitSystemID].push_back(traitTree->ID);
+ tree.ConfigType = TraitConfigType::Generic;
+ }
+ }
+
+ for (TraitNodeGroupEntry const* traitNodeGroup : sTraitNodeGroupStore)
+ {
+ NodeGroup& nodeGroup = _traitGroups[traitNodeGroup->ID];
+ nodeGroup.Data = traitNodeGroup;
+
+ if (std::vector<TraitCondEntry const*>* conditions = Trinity::Containers::MapGetValuePtr(nodeGroupConditions, traitNodeGroup->ID))
+ nodeGroup.Conditions = std::move(*conditions);
+
+ if (std::vector<TraitCostEntry const*>* costs = Trinity::Containers::MapGetValuePtr(nodeGroupCosts, traitNodeGroup->ID))
+ nodeGroup.Costs = std::move(*costs);
+ }
+
+ for (TraitNodeEntry const* traitNode : sTraitNodeStore)
+ {
+ Node& node = _traitNodes[traitNode->ID];
+ node.Data = traitNode;
+
+ if (Tree* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, traitNode->TraitTreeID))
+ tree->Nodes.push_back(&node);
+
+ for (auto&& [_, traitNodeEntry] : Trinity::Containers::MapEqualRange(nodeEntries, traitNode->ID))
+ {
+ NodeEntry& entry = node.Entries.emplace_back();
+ entry.Data = traitNodeEntry;
+
+ if (std::vector<TraitCondEntry const*>* conditions = Trinity::Containers::MapGetValuePtr(nodeEntryConditions, traitNodeEntry->ID))
+ entry.Conditions = std::move(*conditions);
+
+ if (std::vector<TraitCostEntry const*>* costs = Trinity::Containers::MapGetValuePtr(nodeEntryCosts, traitNodeEntry->ID))
+ entry.Costs = std::move(*costs);
+ }
+
+ for (auto&& [_, nodeGroupId] : Trinity::Containers::MapEqualRange(nodeGroups, traitNode->ID))
+ {
+ NodeGroup* nodeGroup = Trinity::Containers::MapGetValuePtr(_traitGroups, nodeGroupId);
+ if (!nodeGroup)
+ continue;
+
+ nodeGroup->Nodes.push_back(&node);
+ node.Groups.push_back(nodeGroup);
+ }
+
+ if (std::vector<TraitCondEntry const*>* conditions = Trinity::Containers::MapGetValuePtr(nodeConditions, traitNode->ID))
+ node.Conditions = std::move(*conditions);
+
+ if (std::vector<TraitCostEntry const*>* costs = Trinity::Containers::MapGetValuePtr(nodeCosts, traitNode->ID))
+ node.Costs = std::move(*costs);
+ }
+
+ for (TraitEdgeEntry const* traitEdgeEntry : sTraitEdgeStore)
+ {
+ Node* left = Trinity::Containers::MapGetValuePtr(_traitNodes, traitEdgeEntry->LeftTraitNodeID);
+ Node* right = Trinity::Containers::MapGetValuePtr(_traitNodes, traitEdgeEntry->RightTraitNodeID);
+ if (!left || !right)
+ continue;
+
+ right->ParentNodes.emplace_back(left, static_cast<TraitEdgeType>(traitEdgeEntry->Type));
+ }
+
+ for (SkillLineXTraitTreeEntry const* skillLineXTraitTreeEntry : sSkillLineXTraitTreeStore)
+ {
+ Tree* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, skillLineXTraitTreeEntry->TraitTreeID);
+ if (!tree)
+ continue;
+
+ SkillLineEntry const* skillLineEntry = sSkillLineStore.LookupEntry(skillLineXTraitTreeEntry->SkillLineID);
+ if (!skillLineEntry)
+ continue;
+
+ _traitTreesBySkillLine[skillLineXTraitTreeEntry->SkillLineID].push_back(tree);
+ if (skillLineEntry->CategoryID == SKILL_CATEGORY_CLASS)
+ {
+ for (SkillRaceClassInfoEntry const* skillRaceClassInfo : sDB2Manager.GetSkillRaceClassInfo(skillLineEntry->ID))
+ for (int32 i = 1; i < MAX_CLASSES; ++i)
+ if (skillRaceClassInfo->ClassMask & (1 << (i - 1)))
+ _skillLinesByClass[i] = skillLineXTraitTreeEntry->SkillLineID;
+
+ tree->ConfigType = TraitConfigType::Combat;
+ }
+ else
+ tree->ConfigType = TraitConfigType::Profession;
+ }
+
+ for (auto&& [traitSystemId, traitTreeIds] : traitTreesIdsByTraitSystem)
+ for (int32 traitTreeId : traitTreeIds)
+ if (Tree* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, traitTreeId))
+ _traitTreesByTraitSystem[traitSystemId].push_back(tree);
+
+ for (TraitCurrencySourceEntry const* traitCurrencySource : sTraitCurrencySourceStore)
+ _traitCurrencySourcesByCurrency[traitCurrencySource->TraitCurrencyID].push_back(traitCurrencySource);
+
+ for (TraitDefinitionEffectPointsEntry const* traitDefinitionEffectPoints : sTraitDefinitionEffectPointsStore)
+ _traitDefinitionEffectPointModifiers[traitDefinitionEffectPoints->TraitDefinitionID].push_back(traitDefinitionEffectPoints);
+
+ std::unordered_map<int32, std::vector<TraitTreeLoadoutEntryEntry const*>> traitTreeLoadoutEntries;
+ for (TraitTreeLoadoutEntryEntry const* traitTreeLoadoutEntry : sTraitTreeLoadoutEntryStore)
+ traitTreeLoadoutEntries[traitTreeLoadoutEntry->TraitTreeLoadoutID].push_back(traitTreeLoadoutEntry);
+
+ for (TraitTreeLoadoutEntry const* traitTreeLoadout : sTraitTreeLoadoutStore)
+ {
+ if (std::vector<TraitTreeLoadoutEntryEntry const*>* entries = Trinity::Containers::MapGetValuePtr(traitTreeLoadoutEntries, traitTreeLoadout->ID))
+ {
+ std::sort(entries->begin(), entries->end(), [](TraitTreeLoadoutEntryEntry const* left, TraitTreeLoadoutEntryEntry const* right)
+ {
+ return left->OrderIndex < right->OrderIndex;
+ });
+ // there should be only one loadout per spec, we take last one encountered
+ _traitTreeLoadoutsByChrSpecialization[traitTreeLoadout->ChrSpecializationID] = std::move(*entries);
+ }
+ }
+}
+
+/**
+ * Generates new TraitConfig identifier.
+ * Because this only needs to be unique for each character we let it overflow
+*/
+int32 GenerateNewTraitConfigId()
+{
+ if (_configIdGenerator == std::numeric_limits<int32>::max())
+ _configIdGenerator = 0;
+
+ return ++_configIdGenerator;
+}
+
+TraitConfigType GetConfigTypeForTree(int32 traitTreeId)
+{
+ Tree const* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, traitTreeId);
+ if (!tree)
+ return TraitConfigType::Invalid;
+
+ return tree->ConfigType;
+}
+
+/**
+ * @brief Finds relevant TraitTree identifiers
+ * @param traitConfig config data
+ * @return Trait tree data
+*/
+std::vector<Tree const*> const* GetTreesForConfig(WorldPackets::Traits::TraitConfig const& traitConfig)
+{
+ switch (traitConfig.Type)
+ {
+ case TraitConfigType::Combat:
+ if (ChrSpecializationEntry const* chrSpecializationEntry = sChrSpecializationStore.LookupEntry(traitConfig.ChrSpecializationID))
+ return Trinity::Containers::MapGetValuePtr(_traitTreesBySkillLine, _skillLinesByClass[chrSpecializationEntry->ClassID]);
+ break;
+ case TraitConfigType::Profession:
+ return Trinity::Containers::MapGetValuePtr(_traitTreesBySkillLine, traitConfig.SkillLineID);
+ case TraitConfigType::Generic:
+ return Trinity::Containers::MapGetValuePtr(_traitTreesByTraitSystem, traitConfig.TraitSystemID);
+ default:
+ break;
+ }
+
+ return nullptr;
+}
+
+bool HasEnoughCurrency(WorldPackets::Traits::TraitEntry const& entry, std::map<int32, int32> const& currencies)
+{
+ auto getCurrencyCount = [&](int32 currencyId)
+ {
+ int32 const* count = Trinity::Containers::MapGetValuePtr(currencies, currencyId);
+ return count ? *count : 0;
+ };
+
+ Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, entry.TraitNodeID);
+ for (NodeGroup const* group : node->Groups)
+ for (TraitCostEntry const* cost : group->Costs)
+ if (getCurrencyCount(cost->TraitCurrencyID) < cost->Amount * entry.Rank)
+ return false;
+
+ auto nodeEntryItr = std::find_if(node->Entries.begin(), node->Entries.end(), [&entry](NodeEntry const& nodeEntry) { return int32(nodeEntry.Data->ID) == entry.TraitNodeEntryID; });
+ if (nodeEntryItr != node->Entries.end())
+ for (TraitCostEntry const* cost : nodeEntryItr->Costs)
+ if (getCurrencyCount(cost->TraitCurrencyID) < cost->Amount * entry.Rank)
+ return false;
+
+ for (TraitCostEntry const* cost : node->Costs)
+ if (getCurrencyCount(cost->TraitCurrencyID) < cost->Amount * entry.Rank)
+ return false;
+
+ if (Tree const* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, node->Data->TraitTreeID))
+ for (TraitCostEntry const* cost : tree->Costs)
+ if (getCurrencyCount(cost->TraitCurrencyID) < cost->Amount * entry.Rank)
+ return false;
+
+ return true;
+}
+
+void TakeCurrencyCost(WorldPackets::Traits::TraitEntry const& entry, std::map<int32, int32>& currencies)
+{
+ Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, entry.TraitNodeID);
+ for (NodeGroup const* group : node->Groups)
+ for (TraitCostEntry const* cost : group->Costs)
+ currencies[cost->TraitCurrencyID] -= cost->Amount * entry.Rank;
+
+ auto nodeEntryItr = std::find_if(node->Entries.begin(), node->Entries.end(), [&entry](NodeEntry const& nodeEntry) { return int32(nodeEntry.Data->ID) == entry.TraitNodeEntryID; });
+ if (nodeEntryItr != node->Entries.end())
+ for (TraitCostEntry const* cost : nodeEntryItr->Costs)
+ currencies[cost->TraitCurrencyID] -= cost->Amount * entry.Rank;
+
+ for (TraitCostEntry const* cost : node->Costs)
+ currencies[cost->TraitCurrencyID] -= cost->Amount * entry.Rank;
+
+ if (Tree const* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, node->Data->TraitTreeID))
+ for (TraitCostEntry const* cost : tree->Costs)
+ currencies[cost->TraitCurrencyID] -= cost->Amount * entry.Rank;
+}
+
+void FillOwnedCurrenciesMap(WorldPackets::Traits::TraitConfig const& traitConfig, Player const* player, std::map<int32, int32>& currencies)
+{
+ std::vector<Tree const*> const* trees = GetTreesForConfig(traitConfig);
+ if (!trees)
+ return;
+
+ auto hasTraitNodeEntry = [&traitConfig](int32 traitNodeEntryId)
+ {
+ return std::find_if(traitConfig.Entries.begin(), traitConfig.Entries.end(), [traitNodeEntryId](WorldPackets::Traits::TraitEntry const& traitEntry)
+ {
+ return traitEntry.TraitNodeEntryID == traitNodeEntryId && (traitEntry.Rank > 0 || traitEntry.GrantedRanks > 0);
+ }) != traitConfig.Entries.end();
+ };
+
+ for (Tree const* tree : *trees)
+ {
+ for (TraitCurrencyEntry const* currency : tree->Currencies)
+ {
+ switch (currency->GetType())
+ {
+ case TraitCurrencyType::Gold:
+ {
+ int32& amount = currencies[currency->ID];
+ if (player->GetMoney() > uint64(std::numeric_limits<int32>::max() - amount))
+ amount = std::numeric_limits<int32>::max();
+ else
+ amount += player->GetMoney();
+ break;
+ }
+ case TraitCurrencyType::CurrencyTypesBased:
+ currencies[currency->ID] += player->GetCurrency(currency->CurrencyTypesID);
+ break;
+ case TraitCurrencyType::TraitSourced:
+ if (std::vector<TraitCurrencySourceEntry const*>* currencySources = Trinity::Containers::MapGetValuePtr(_traitCurrencySourcesByCurrency, currency->ID))
+ {
+ for (TraitCurrencySourceEntry const* currencySource : *currencySources)
+ {
+ if (currencySource->QuestID && !player->IsQuestRewarded(currencySource->QuestID))
+ continue;
+
+ if (currencySource->AchievementID && !player->HasAchieved(currencySource->AchievementID))
+ continue;
+
+ if (currencySource->PlayerLevel && player->GetLevel() < currencySource->PlayerLevel)
+ continue;
+
+ if (currencySource->TraitNodeEntryID && !hasTraitNodeEntry(currencySource->TraitNodeEntryID))
+ continue;
+
+ currencies[currencySource->TraitCurrencyID] += currencySource->Amount;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+}
+
+void FillSpentCurrenciesMap(WorldPackets::Traits::TraitEntry const& entry, std::map<int32, int32>& cachedCurrencies)
+{
+ Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, entry.TraitNodeID);
+ for (NodeGroup const* group : node->Groups)
+ for (TraitCostEntry const* cost : group->Costs)
+ cachedCurrencies[cost->TraitCurrencyID] += cost->Amount * entry.Rank;
+
+ auto nodeEntryItr = std::find_if(node->Entries.begin(), node->Entries.end(), [&entry](NodeEntry const& nodeEntry) { return int32(nodeEntry.Data->ID) == entry.TraitNodeEntryID; });
+ if (nodeEntryItr != node->Entries.end())
+ for (TraitCostEntry const* cost : nodeEntryItr->Costs)
+ cachedCurrencies[cost->TraitCurrencyID] += cost->Amount * entry.Rank;
+
+ for (TraitCostEntry const* cost : node->Costs)
+ cachedCurrencies[cost->TraitCurrencyID] += cost->Amount * entry.Rank;
+
+ if (Tree const* tree = Trinity::Containers::MapGetValuePtr(_traitTrees, node->Data->TraitTreeID))
+ for (TraitCostEntry const* cost : tree->Costs)
+ cachedCurrencies[cost->TraitCurrencyID] += cost->Amount * entry.Rank;
+}
+
+void FillSpentCurrenciesMap(WorldPackets::Traits::TraitConfig const& traitConfig, std::map<int32, int32>& cachedCurrencies)
+{
+ for (WorldPackets::Traits::TraitEntry const& entry : traitConfig.Entries)
+ FillSpentCurrenciesMap(entry, cachedCurrencies);
+}
+
+bool MeetsTraitCondition(WorldPackets::Traits::TraitConfig const& traitConfig, Player const* player, TraitCondEntry const* condition,
+ Optional<std::map<int32, int32>>& cachedCurrencies)
+{
+ if (condition->QuestID && !player->IsQuestRewarded(condition->QuestID))
+ return false;
+
+ if (condition->AchievementID && !player->HasAchieved(condition->AchievementID))
+ return false;
+
+ if (condition->SpecSetID && !sDB2Manager.IsSpecSetMember(condition->SpecSetID, player->GetPrimarySpecialization()))
+ return false;
+
+ if (condition->TraitCurrencyID && condition->SpentAmountRequired)
+ {
+ if (!cachedCurrencies)
+ FillSpentCurrenciesMap(traitConfig, cachedCurrencies.emplace());
+
+ if (condition->TraitNodeGroupID)
+ {
+ auto itr = cachedCurrencies->try_emplace(condition->TraitCurrencyID, 0).first;
+ if (itr->second < condition->SpentAmountRequired)
+ return false;
+ }
+ else if (condition->TraitNodeID)
+ {
+ auto itr = cachedCurrencies->try_emplace(condition->TraitCurrencyID, 0).first;
+ if (itr->second < condition->SpentAmountRequired)
+ return false;
+ }
+ }
+
+ if (condition->RequiredLevel && int32(player->GetLevel()) < condition->RequiredLevel)
+ return false;
+
+ return true;
+}
+
+std::vector<UF::TraitEntry> GetGrantedTraitEntriesForConfig(WorldPackets::Traits::TraitConfig const& traitConfig, Player const* player)
+{
+ std::vector<UF::TraitEntry> entries;
+ std::vector<Tree const*> const* trees = GetTreesForConfig(traitConfig);
+ if (!trees)
+ return entries;
+
+ auto getOrCreateEntry = [&entries](int32 nodeId, int32 entryId)
+ {
+ auto itr = std::find_if(entries.begin(), entries.end(), [&](UF::TraitEntry const& traitEntry)
+ {
+ return traitEntry.TraitNodeID == nodeId && traitEntry.TraitNodeEntryID == entryId;
+ });
+ if (itr == entries.end())
+ {
+ itr = entries.emplace(entries.end());
+ itr->TraitNodeID = nodeId;
+ itr->TraitNodeEntryID = entryId;
+ itr->Rank = 0;
+ itr->GrantedRanks = 0;
+ }
+ return &*itr;
+ };
+
+ Optional<std::map<int32, int32>> cachedCurrencies;
+
+ for (Tree const* tree : *trees)
+ {
+ for (Node const* node : tree->Nodes)
+ {
+ for (NodeEntry const& entry : node->Entries)
+ for (TraitCondEntry const* condition : entry.Conditions)
+ if (condition->GetCondType() == TraitConditionType::Granted && MeetsTraitCondition(traitConfig, player, condition, cachedCurrencies))
+ getOrCreateEntry(node->Data->ID, entry.Data->ID)->GrantedRanks += condition->GrantedRanks;
+
+ for (TraitCondEntry const* condition : node->Conditions)
+ if (condition->GetCondType() == TraitConditionType::Granted && MeetsTraitCondition(traitConfig, player, condition, cachedCurrencies))
+ for (NodeEntry const& entry : node->Entries)
+ getOrCreateEntry(node->Data->ID, entry.Data->ID)->GrantedRanks += condition->GrantedRanks;
+
+ for (NodeGroup const* group : node->Groups)
+ for (TraitCondEntry const* condition : group->Conditions)
+ if (condition->GetCondType() == TraitConditionType::Granted && MeetsTraitCondition(traitConfig, player, condition, cachedCurrencies))
+ for (NodeEntry const& entry : node->Entries)
+ getOrCreateEntry(node->Data->ID, entry.Data->ID)->GrantedRanks += condition->GrantedRanks;
+ }
+ }
+
+ return entries;
+}
+
+bool IsValidEntry(WorldPackets::Traits::TraitEntry const& traitEntry)
+{
+ Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, traitEntry.TraitNodeID);
+ if (!node)
+ return false;
+
+ auto entryItr = std::find_if(node->Entries.begin(), node->Entries.end(), [&](NodeEntry const& entry) { return entry.Data->ID == uint32(traitEntry.TraitNodeEntryID); });
+ if (entryItr == node->Entries.end())
+ return false;
+
+ if (entryItr->Data->MaxRanks < traitEntry.Rank + traitEntry.GrantedRanks)
+ return false;
+
+ return true;
+}
+
+TalentLearnResult ValidateConfig(WorldPackets::Traits::TraitConfig const& traitConfig, Player const* player, bool requireSpendingAllCurrencies /*= false*/)
+{
+ auto getNodeEntryCount = [&](int32 traitNodeId)
+ {
+ return std::count_if(traitConfig.Entries.begin(), traitConfig.Entries.end(), [traitNodeId](WorldPackets::Traits::TraitEntry const& traitEntry)
+ {
+ return traitEntry.TraitNodeID == traitNodeId;
+ });
+ };
+
+ auto getNodeEntry = [&](int32 traitNodeId, int32 traitNodeEntryId)
+ {
+ auto entryItr = std::find_if(traitConfig.Entries.begin(), traitConfig.Entries.end(), [=](WorldPackets::Traits::TraitEntry const& traitEntry)
+ {
+ return traitEntry.TraitNodeID == traitNodeId && traitEntry.TraitNodeEntryID == traitNodeEntryId;
+ });
+ return entryItr != traitConfig.Entries.end() ? &*entryItr : nullptr;
+ };
+
+ auto isNodeFullyFilled = [&](Node const* node)
+ {
+ if (node->Data->GetType() == TraitNodeType::Selection)
+ return std::any_of(node->Entries.begin(), node->Entries.end(), [&](NodeEntry const& nodeEntry)
+ {
+ WorldPackets::Traits::TraitEntry const* traitEntry = getNodeEntry(node->Data->ID, nodeEntry.Data->ID);
+ return traitEntry && (traitEntry->Rank + traitEntry->GrantedRanks) == nodeEntry.Data->MaxRanks;
+ });
+
+ return std::all_of(node->Entries.begin(), node->Entries.end(), [&](NodeEntry const& nodeEntry)
+ {
+ WorldPackets::Traits::TraitEntry const* traitEntry = getNodeEntry(node->Data->ID, nodeEntry.Data->ID);
+ return traitEntry && (traitEntry->Rank + traitEntry->GrantedRanks) == nodeEntry.Data->MaxRanks;
+ });
+ };
+
+ Optional<std::map<int32, int32>> spentCurrencies;
+ FillSpentCurrenciesMap(traitConfig, spentCurrencies.emplace());
+
+ for (WorldPackets::Traits::TraitEntry const& traitEntry : traitConfig.Entries)
+ {
+ if (!IsValidEntry(traitEntry))
+ return TALENT_FAILED_UNKNOWN;
+
+ Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, traitEntry.TraitNodeID);
+ if (node->Data->GetType() == TraitNodeType::Selection)
+ if (getNodeEntryCount(traitEntry.TraitNodeID) != 1)
+ return TALENT_FAILED_UNKNOWN;
+
+ for (NodeEntry const& entry : node->Entries)
+ for (TraitCondEntry const* condition : entry.Conditions)
+ if ((condition->GetCondType() == TraitConditionType::Available || condition->GetCondType() == TraitConditionType::Visible)
+ && !MeetsTraitCondition(traitConfig, player, condition, spentCurrencies))
+ return TALENT_FAILED_UNKNOWN;
+
+ for (TraitCondEntry const* condition : node->Conditions)
+ if ((condition->GetCondType() == TraitConditionType::Available || condition->GetCondType() == TraitConditionType::Visible)
+ && !MeetsTraitCondition(traitConfig, player, condition, spentCurrencies))
+ return TALENT_FAILED_UNKNOWN;
+
+ for (NodeGroup const* group : node->Groups)
+ for (TraitCondEntry const* condition : group->Conditions)
+ if ((condition->GetCondType() == TraitConditionType::Available || condition->GetCondType() == TraitConditionType::Visible)
+ && !MeetsTraitCondition(traitConfig, player, condition, spentCurrencies))
+ return TALENT_FAILED_UNKNOWN;
+
+ if (!node->ParentNodes.empty())
+ {
+ bool hasAnyParentTrait = false;
+ for (auto const& [parentNode, edgeType] : node->ParentNodes)
+ {
+ if (!isNodeFullyFilled(parentNode))
+ {
+ if (edgeType == TraitEdgeType::RequiredForAvailability)
+ return TALENT_FAILED_NOT_ENOUGH_TALENTS_IN_PRIMARY_TREE;
+
+ continue;
+ }
+
+ hasAnyParentTrait = true;
+ }
+
+ if (!hasAnyParentTrait)
+ return TALENT_FAILED_NOT_ENOUGH_TALENTS_IN_PRIMARY_TREE;
+ }
+ }
+
+ std::map<int32, int32> grantedCurrencies;
+ FillOwnedCurrenciesMap(traitConfig, player, grantedCurrencies);
+
+ for (auto [traitCurrencyId, spentAmount] : *spentCurrencies)
+ {
+ if (sTraitCurrencyStore.AssertEntry(traitCurrencyId)->GetType() != TraitCurrencyType::TraitSourced)
+ continue;
+
+ if (!spentAmount)
+ continue;
+
+ int32* grantedCount = Trinity::Containers::MapGetValuePtr(grantedCurrencies, traitCurrencyId);
+ if (!grantedCount || *grantedCount < spentAmount)
+ return TALENT_FAILED_NOT_ENOUGH_TALENTS_IN_PRIMARY_TREE;
+
+ }
+
+ if (requireSpendingAllCurrencies && traitConfig.Type == TraitConfigType::Combat)
+ {
+ for (auto [traitCurrencyId, grantedAmount] : grantedCurrencies)
+ {
+ if (!grantedAmount)
+ continue;
+
+ int32* spentAmount = Trinity::Containers::MapGetValuePtr(*spentCurrencies, traitCurrencyId);
+ if (!spentAmount || *spentAmount != grantedAmount)
+ return TALENT_FAILED_UNSPENT_TALENT_POINTS;
+ }
+ }
+
+ return TALENT_LEARN_OK;
+}
+
+std::vector<TraitDefinitionEffectPointsEntry const*> const* GetTraitDefinitionEffectPointModifiers(int32 traitDefinitionId)
+{
+ return Trinity::Containers::MapGetValuePtr(_traitDefinitionEffectPointModifiers, traitDefinitionId);
+}
+
+void InitializeStarterBuildTraitConfig(WorldPackets::Traits::TraitConfig& traitConfig, Player const* player)
+{
+ traitConfig.Entries.clear();
+ std::vector<Tree const*> const* trees = GetTreesForConfig(traitConfig);
+ if (!trees)
+ return;
+
+ for (UF::TraitEntry const& grant : GetGrantedTraitEntriesForConfig(traitConfig, player))
+ {
+ WorldPackets::Traits::TraitEntry& newEntry = traitConfig.Entries.emplace_back();
+ newEntry.TraitNodeID = grant.TraitNodeID;
+ newEntry.TraitNodeEntryID = grant.TraitNodeEntryID;
+ newEntry.GrantedRanks = grant.GrantedRanks;
+ }
+
+ std::map<int32, int32> currencies;
+ FillOwnedCurrenciesMap(traitConfig, player, currencies);
+
+ if (std::vector<TraitTreeLoadoutEntryEntry const*> const* loadoutEntries = Trinity::Containers::MapGetValuePtr(_traitTreeLoadoutsByChrSpecialization, traitConfig.ChrSpecializationID))
+ {
+ auto findEntry = [](WorldPackets::Traits::TraitConfig& config, int32 traitNodeId, int32 traitNodeEntryId) -> WorldPackets::Traits::TraitEntry*
+ {
+ auto entryItr = std::find_if(config.Entries.begin(), config.Entries.end(), [=](WorldPackets::Traits::TraitEntry const& traitEntry)
+ {
+ return traitEntry.TraitNodeID == traitNodeId && traitEntry.TraitNodeEntryID == traitNodeEntryId;
+ });
+ return entryItr != config.Entries.end() ? &*entryItr : nullptr;
+ };
+
+ for (TraitTreeLoadoutEntryEntry const* loadoutEntry : *loadoutEntries)
+ {
+ int32 addedRanks = loadoutEntry->NumPoints;
+ Node const* node = Trinity::Containers::MapGetValuePtr(_traitNodes, loadoutEntry->SelectedTraitNodeID);
+
+ WorldPackets::Traits::TraitEntry newEntry;
+ newEntry.TraitNodeID = loadoutEntry->SelectedTraitNodeID;
+ newEntry.TraitNodeEntryID = loadoutEntry->SelectedTraitNodeEntryID;
+ if (!newEntry.TraitNodeEntryID)
+ newEntry.TraitNodeEntryID = node->Entries[0].Data->ID;
+
+ WorldPackets::Traits::TraitEntry* entryInConfig = findEntry(traitConfig, newEntry.TraitNodeID, newEntry.TraitNodeEntryID);
+
+ if (entryInConfig)
+ addedRanks -= entryInConfig->Rank;
+
+ newEntry.Rank = addedRanks;
+
+ if (!HasEnoughCurrency(newEntry, currencies))
+ break;
+
+ if (entryInConfig)
+ entryInConfig->Rank += addedRanks;
+ else
+ traitConfig.Entries.push_back(newEntry);
+
+ TakeCurrencyCost(newEntry, currencies);
+ }
+ }
+}
+}
diff --git a/src/server/game/Spells/TraitMgr.h b/src/server/game/Spells/TraitMgr.h
new file mode 100644
index 00000000000..2350f2bb92d
--- /dev/null
+++ b/src/server/game/Spells/TraitMgr.h
@@ -0,0 +1,57 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TRINITY_TRAIT_MGR_H
+#define TRINITY_TRAIT_MGR_H
+
+#include "Define.h"
+#include <map>
+#include <vector>
+
+class Player;
+struct TraitDefinitionEffectPointsEntry;
+enum class TraitConfigType : int32;
+enum TalentLearnResult : int32;
+
+namespace UF
+{
+struct TraitEntry;
+}
+
+namespace WorldPackets::Traits
+{
+struct TraitConfig;
+struct TraitEntry;
+}
+
+namespace TraitMgr
+{
+constexpr uint32 COMMIT_COMBAT_TRAIT_CONFIG_CHANGES_SPELL_ID = 384255u;
+constexpr uint32 MAX_COMBAT_TRAIT_CONFIGS = 10u;
+
+void Load();
+int32 GenerateNewTraitConfigId();
+TraitConfigType GetConfigTypeForTree(int32 traitTreeId);
+void FillSpentCurrenciesMap(WorldPackets::Traits::TraitEntry const& entry, std::map<int32, int32>& cachedCurrencies);
+std::vector<UF::TraitEntry> GetGrantedTraitEntriesForConfig(WorldPackets::Traits::TraitConfig const& traitConfig, Player const* player);
+bool IsValidEntry(WorldPackets::Traits::TraitEntry const& traitEntry);
+TalentLearnResult ValidateConfig(WorldPackets::Traits::TraitConfig const& traitConfig, Player const* player, bool requireSpendingAllCurrencies = false);
+std::vector<TraitDefinitionEffectPointsEntry const*> const* GetTraitDefinitionEffectPointModifiers(int32 traitDefinitionId);
+void InitializeStarterBuildTraitConfig(WorldPackets::Traits::TraitConfig& traitConfig, Player const* player);
+}
+
+#endif // TRINITY_TRAIT_MGR_H
diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp
index d13d47f8564..2f4bb3ce75d 100644
--- a/src/server/game/World/World.cpp
+++ b/src/server/game/World/World.cpp
@@ -91,6 +91,7 @@
#include "SupportMgr.h"
#include "TaxiPathGraph.h"
#include "TerrainMgr.h"
+#include "TraitMgr.h"
#include "TransportMgr.h"
#include "Unit.h"
#include "UpdateTime.h"
@@ -1872,6 +1873,9 @@ void World::SetInitialWorldSettings()
TC_LOG_INFO("server.loading", "Loading Spell Totem models...");
sSpellMgr->LoadSpellTotemModel();
+ TC_LOG_INFO("server.loading", "Loading Traits...");
+ TraitMgr::Load();
+
TC_LOG_INFO("server.loading", "Loading languages..."); // must be after LoadSpellInfoStore and LoadSkillLineAbilityMap
sLanguageMgr->LoadLanguages();