From c555eb566703f15d139d0fa1487e84a1e0dadc88 Mon Sep 17 00:00:00 2001 From: Niam5 Date: Thu, 8 Aug 2024 11:32:47 -0700 Subject: [PATCH] Add dual spec mod --- CMakeLists.txt | 4 + cmake/options.cmake | 2 + cmake/showoptions.cmake | 6 + .../z9999_02_characters_custom_dualspec.sql | 33 ++++ src/game/Chat/Chat.cpp | 3 + src/game/Chat/Chat.h | 3 + src/game/Chat/Level0.cpp | 22 +++ src/game/Entities/Player.cpp | 178 ++++++++++++++++++ src/game/Entities/Player.h | 15 ++ src/game/World/World.cpp | 4 + src/game/World/World.h | 3 + src/mangosd/mangosd.conf.dist.in | 4 + 12 files changed, 277 insertions(+) create mode 100644 sql/updates/characters/z9999_02_characters_custom_dualspec.sql diff --git a/CMakeLists.txt b/CMakeLists.txt index d0b408f72e..cb67d8da63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -462,6 +462,10 @@ if(BUILD_SOLOCRAFT) endif() endif() +if(BUILD_DUAL_SPEC) + set_property(TARGET game APPEND PROPERTY COMPILE_DEFINITIONS BUILD_DUAL_SPEC) +endif() + # if(SQL) # add_subdirectory(sql) # endif() diff --git a/cmake/options.cmake b/cmake/options.cmake index 53add2acdb..0b356c0e98 100644 --- a/cmake/options.cmake +++ b/cmake/options.cmake @@ -9,6 +9,7 @@ option(BUILD_SCRIPTDEV "Build ScriptDev. (OFF Speedup build option(BUILD_PLAYERBOTS "Build Playerbots mod" OFF) option(BUILD_ELUNA "Build Eluna Lua Engine" OFF) option(BUILD_SOLOCRAFT "Build SoloCraft mod" OFF) +option(BUILD_DUAL_SPEC "Build Dual Spec mod" OFF) option(BUILD_AHBOT "Build Auction House Bot mod" OFF) option(BUILD_METRICS "Build Metrics, generate data for Grafana" OFF) option(BUILD_RECASTDEMOMOD "Build map/vmap/mmap viewer" OFF) @@ -39,6 +40,7 @@ message(STATUS BUILD_PLAYERBOTS Build Playerbots mod BUILD_ELUNA Build Eluna Lua Engine BUILD_SOLOCRAFT Build SoloCraft Mod + BUILD_DUAL_SPEC Build Dual Spec mod BUILD_AHBOT Build Auction House Bot mod BUILD_METRICS Build Metrics, generate data for Grafana BUILD_RECASTDEMOMOD Build map/vmap/mmap viewer diff --git a/cmake/showoptions.cmake b/cmake/showoptions.cmake index 65a4f40037..7bd971089c 100644 --- a/cmake/showoptions.cmake +++ b/cmake/showoptions.cmake @@ -61,6 +61,12 @@ else() message(STATUS "Build SoloCraft Mod : No (default)") endif() +if(BUILD_DUAL_SPEC) + message(STATUS "Build Dual Spec mod : Yes") +else() + message(STATUS "Build Dual Spec mod : No (default)") +endif() + if(BUILD_AHBOT) message(STATUS "Build AHBot : Yes") else() diff --git a/sql/updates/characters/z9999_02_characters_custom_dualspec.sql b/sql/updates/characters/z9999_02_characters_custom_dualspec.sql new file mode 100644 index 0000000000..c62ca6b3ce --- /dev/null +++ b/sql/updates/characters/z9999_02_characters_custom_dualspec.sql @@ -0,0 +1,33 @@ +DROP TABLE IF EXISTS `character_altspec`; +CREATE TABLE `character_altspec` ( + `guid` int(11) unsigned NOT NULL default '0' COMMENT 'Global Unique Identifier', + `altspells` int(11) unsigned NOT NULL default '0', + PRIMARY KEY (`guid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='Player Dual Spec System'; + +-- +-- Dumping data for table `characters_altspec` +-- + +LOCK TABLES `character_altspec` WRITE; +/*!40000 ALTER TABLE `character_altspec` DISABLE KEYS */; +/*!40000 ALTER TABLE `character_altspec` ENABLE KEYS */; +UNLOCK TABLES; + +DROP TABLE IF EXISTS `character_altspec_action`; +CREATE TABLE `character_altspec_action` ( + `guid` int(11) unsigned NOT NULL default '0' COMMENT 'Global Unique Identifier', + `button` tinyint(3) unsigned NOT NULL default '0', + `action` smallint(5) unsigned NOT NULL default '0', + `type` tinyint(3) unsigned NOT NULL default '0', + PRIMARY KEY (`guid`,`button`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='Player Dual Spec System, table for actions'; + +-- +-- Dumping data for table `character_altspec_action` +-- + +LOCK TABLES `character_altspec_action` WRITE; +/*!40000 ALTER TABLE `character_altspec_action` DISABLE KEYS */; +/*!40000 ALTER TABLE `character_altspec_action` ENABLE KEYS */; +UNLOCK TABLES; \ No newline at end of file diff --git a/src/game/Chat/Chat.cpp b/src/game/Chat/Chat.cpp index 33d1e42851..d6ef4b2dfe 100644 --- a/src/game/Chat/Chat.cpp +++ b/src/game/Chat/Chat.cpp @@ -952,6 +952,9 @@ ChatCommand* ChatHandler::getCommandTable() { "aura", SEC_ADMINISTRATOR, false, &ChatHandler::HandleAuraCommand, "", nullptr }, { "unaura", SEC_ADMINISTRATOR, false, &ChatHandler::HandleUnAuraCommand, "", nullptr }, { "announce", SEC_MODERATOR, true, &ChatHandler::HandleAnnounceCommand, "", nullptr }, +#ifdef BUILD_DUAL_SPEC + { "swapspec", SEC_PLAYER, false, &ChatHandler::HandleSwapSpec, "", nullptr }, +#endif { "notify", SEC_MODERATOR, true, &ChatHandler::HandleNotifyCommand, "", nullptr }, { "goname", SEC_MODERATOR, false, &ChatHandler::HandleGonameCommand, "", nullptr }, { "appear", SEC_MODERATOR, false, &ChatHandler::HandleGonameCommand, "", nullptr }, diff --git a/src/game/Chat/Chat.h b/src/game/Chat/Chat.h index 47d260033c..6439cfaa6a 100644 --- a/src/game/Chat/Chat.h +++ b/src/game/Chat/Chat.h @@ -675,6 +675,9 @@ class ChatHandler bool HandleGroupgoCommand(char* args); bool HandleRecallCommand(char* args); bool HandleAnnounceCommand(char* args); +#ifdef BUILD_DUAL_SPEC + bool HandleSwapSpec(char* args); +#endif bool HandleNotifyCommand(char* args); bool HandleGPSCommand(char* args); bool HandleTaxiCheatCommand(char* args); diff --git a/src/game/Chat/Level0.cpp b/src/game/Chat/Level0.cpp index 041c6bde7f..0f57961a3f 100644 --- a/src/game/Chat/Level0.cpp +++ b/src/game/Chat/Level0.cpp @@ -304,3 +304,25 @@ bool ChatHandler::HandleWhisperRestrictionCommand(char* args) return true; } + +#ifdef BUILD_DUAL_SPEC +bool ChatHandler::HandleSwapSpec(char* /*args*/) +{ + uint32 res = m_session->GetPlayer()->SwapSpec(); + switch (res) { + case 3: { + PSendSysMessage("Oh, wait a bit, please!"); + break; + } + case 2: { + PSendSysMessage("Too low level"); + break; + } + case 1: { + PSendSysMessage("Swapped!"); + break; + } + } + return true; +} +#endif \ No newline at end of file diff --git a/src/game/Entities/Player.cpp b/src/game/Entities/Player.cpp index f397308bf6..1f724c4dae 100644 --- a/src/game/Entities/Player.cpp +++ b/src/game/Entities/Player.cpp @@ -20768,3 +20768,181 @@ uint32 Player::LookupHighestLearnedRank(uint32 spellId) } while ((higherRank = sSpellMgr.GetNextSpellInChain(ownedRank))); return ownedRank; } + +#ifdef BUILD_DUAL_SPEC +void Player::SaveAlternativeSpec() +{ + sLog.outDebug("Saving spec"); + //Save current spec + for (SpellIDList::iterator it = m_altspec_talents.begin(); it != m_altspec_talents.end(); it++) + { + CharacterDatabase.PExecute("INSERT INTO `character_altspec` (`guid`, `altspells`) VALUES (%u, %u)", GetGUIDLow(), *it); + } + sLog.outDebug("Spec saved"); + + //Remove previous spec action buttons + CharacterDatabase.PExecute("DELETE FROM character_altspec_action WHERE guid = '%u'", GetGUIDLow()); + sLog.outDebug("Old buttons removed"); + + //Now - seek all needed keys and insert into DB + for (uint32 button = 0; button < MAX_ACTION_BUTTONS; ++button) + { + //Find button by button id + ActionButtonList::const_iterator itr = m_altspec_actionButtons.find(button); + //STL returns end() if not found. We need to skip it. + if (itr != m_altspec_actionButtons.end()) + { + //Okay, now we're ready to insert it + CharacterDatabase.PExecute("INSERT INTO character_altspec_action (guid,button,action,type) VALUES ('%u', '%u', '%u', '%u')", + GetGUIDLow(), button, itr->second.GetAction(), itr->second.GetType()); + } + } +} + +void Player::LoadAlternativeSpec() +{ + //Load talents from database + auto queryResult = CharacterDatabase.PQuery("SELECT `altspells` FROM character_altspec WHERE guid = %u", GetGUIDLow()); + + if (queryResult) + { + Field* fields = queryResult->Fetch(); + do + { + m_loaded_talents.push_back(fields[0].GetUInt32()); + } while (queryResult->NextRow()); + } + + //Delete loaded spec, will be replaced with current spec + CharacterDatabase.PExecute("DELETE FROM `character_altspec` WHERE guid = %u", GetGUIDLow()); + + sLog.outDebug("Loaded spec"); +} + +uint32 Player::SwapSpec() +{ + /* + Error codes: + 2 - Too low level + 3 - Too fast + Strategy: + 1) save keys + 2) save talents + 3) load talents + 4) load keys + */ + + //Level check + if (GetLevel() <= 10) + return 2; + + //Time check + if (uint32(time(NULL) - m_altspec_lastswap) < sWorld.getConfig(CONFIG_UINT32_DUAL_SPEC_TIME_DELTA)) + return 3; + + /*********************************************************/ + /*** SAVE ACTIONBUTTONS ***/ + /*********************************************************/ + sLog.outDebug("Save action buttons"); + //Save buttons before + ActionButtonList tmp_buttons = m_altspec_actionButtons; + + //Just copy current content + m_altspec_actionButtons = m_actionButtons; + + /*********************************************************/ + /*** SAVE TALENTS ***/ + /*********************************************************/ + sLog.outDebug("Save talents"); + //erase it for populating using current talents + m_altspec_talents.clear(); + m_loaded_talents.clear(); + + //Find all talents, general idea from Player::resetTalents + for (unsigned int i = 0; i < sTalentStore.GetNumRows(); ++i) { + TalentEntry const* talentInfo = sTalentStore.LookupEntry(i); + if (!talentInfo) continue; + TalentTabEntry const* talentTabInfo = sTalentTabStore.LookupEntry(talentInfo->TalentTab); + if (!talentTabInfo) continue; + if ((getClassMask() & talentTabInfo->ClassMask) == 0) continue; + for (int j = 0; j < 5; ++j) { + for (PlayerSpellMap::iterator itr = GetSpellMap().begin(); itr != GetSpellMap().end();) { + + //skip disabled talents like Pyroblast or some else + if (itr->second.state == PLAYERSPELL_REMOVED || itr->second.disabled) + { + ++itr; + continue; + } + + //for spells, which can be updated via trainers(like Pyroblast), we can'n just compare, cuz + // >1 ranks are not in talens store. So, first rank it is. We can just get lowerest rank of skill + // and search it in the talents storage. + uint32 itrFirstId = sSpellMgr.GetFirstSpellInChain(itr->first); + + //now - just compare. Also, it make sense to add "|| spellmgr.IsSpellLearnToSpell(talentInfo->RankID[j],itrFirstId)" + //but i have no idea what it is, it uses in the Player::resetTalents function and it may be needed. + //Also, there is a some spells like Prayer of Spirit, which not in talents tree, but its depends on talents. + //So, we need just to get required spell by current spell and find is it in player spellbook. + if (itrFirstId == talentInfo->RankID[j] + || sSpellMgr.IsSpellLearnToSpell(talentInfo->RankID[j], itrFirstId) + )//|| HasSpell(spellmgr.GetSpellRequired(itrFirstId))) + m_altspec_talents.push_back(itr->first); + ++itr; + } + } + } + //Must call load first since old spec will be deleted + LoadAlternativeSpec(); + auto queryResultButtons = CharacterDatabase.PQuery("SELECT `button`, `action`, `type` FROM character_altspec_action WHERE guid = %u", GetGUIDLow()); + SaveAlternativeSpec(); + + /*********************************************************/ + /*** LOAD TALENTS ***/ + /*********************************************************/ + sLog.outDebug("Load talents"); + resetTalents(true); + for (SpellIDList::iterator it = m_loaded_talents.begin(); it != m_loaded_talents.end(); it++) + { + learnSpell(*it, false); + } + InitTalentForLevel(); + //learnSkillRewardedSpells(); + + /*********************************************************/ + /*** LOAD ACTIONBUTTONS ***/ + /*********************************************************/ + sLog.outDebug("Load action buttons"); + //Clean up + for (int button = 0; button < MAX_ACTION_BUTTONS; ++button) + removeActionButton(button); + sLog.outDebug("Action buttons cleaned"); + + //Add new actions buttons for new spec + if (queryResultButtons) + { + Field* fields = queryResultButtons->Fetch(); + + do + { + uint8 button = fields[0].GetUInt8(); + uint32 action = fields[1].GetUInt32(); + uint8 type = fields[2].GetUInt8(); + + addActionButton(button, action, type); + } while (queryResultButtons->NextRow()); + } + + sLog.outDebug("Action buttons added"); + + //SendInitialActionButtons();//doesnt work + + //Drop mana and health to minimum for preventing of profit from swappings + SetHealth(1); + SetPower(POWER_MANA, 1); + SetPower(POWER_RAGE, 1); + SetPower(POWER_ENERGY, 1); + GetSession()->LogoutPlayer(); //Action bars won't reload unless player is kicked then relogs + return 1; //Okay +} +#endif \ No newline at end of file diff --git a/src/game/Entities/Player.h b/src/game/Entities/Player.h index fa3af71fd3..d6a484f382 100644 --- a/src/game/Entities/Player.h +++ b/src/game/Entities/Player.h @@ -2166,6 +2166,21 @@ class Player : public Unit AreaLockStatus GetAreaTriggerLockStatus(AreaTrigger const* at, uint32& miscRequirement, bool forceAllChecks = false); void SendTransferAbortedByLockStatus(MapEntry const* mapEntry, AreaTrigger const* at, AreaLockStatus lockStatus, uint32 miscRequirement = 0) const; +#ifdef BUILD_DUAL_SPEC + /*********************************************************/ + /*** DUEL SPEC SYSTEM ***/ + /*********************************************************/ + + void LoadAlternativeSpec(); + void SaveAlternativeSpec(); + typedef std::list SpellIDList; + SpellIDList m_altspec_talents; + SpellIDList m_loaded_talents; + ActionButtonList m_altspec_actionButtons; + time_t m_altspec_lastswap; + uint32 SwapSpec(); +#endif + /*********************************************************/ /*** GROUP SYSTEM ***/ /*********************************************************/ diff --git a/src/game/World/World.cpp b/src/game/World/World.cpp index ca59b87535..fb2ce534cf 100644 --- a/src/game/World/World.cpp +++ b/src/game/World/World.cpp @@ -926,6 +926,10 @@ void World::LoadConfigSettings(bool reload) //End Solocraft Config #endif +#ifdef BUILD_DUAL_SPEC + setConfig(CONFIG_UINT32_DUAL_SPEC_TIME_DELTA, "DualSpecTimeDelta", 300); +#endif + sLog.outString(); } diff --git a/src/game/World/World.h b/src/game/World/World.h index 86c6597483..593e0537be 100644 --- a/src/game/World/World.h +++ b/src/game/World/World.h @@ -234,6 +234,9 @@ enum eConfigUInt32Values CONFIG_UINT32_AHNQIRAJ_LEVEL, CONFIG_UINT32_AHNQIRAJTEMPLE_LEVEL, CONFIG_UINT32_STRATHOLMERAID_LEVEL, +#endif +#ifdef BUILD_DUAL_SPEC + CONFIG_UINT32_DUAL_SPEC_TIME_DELTA, #endif CONFIG_UINT32_VALUE_COUNT }; diff --git a/src/mangosd/mangosd.conf.dist.in b/src/mangosd/mangosd.conf.dist.in index bb93a0afbc..6ad00eb6ae 100644 --- a/src/mangosd/mangosd.conf.dist.in +++ b/src/mangosd/mangosd.conf.dist.in @@ -1480,6 +1480,9 @@ Visibility.AIRelocationNotifyDelay = 1000 # Modifies the speed of player's ghosts, removed upon reviving, not permanent/saved, in non-BG and BG maps # Default: 1.0 (normal speed) # +# DualSpecTimeDelta +# Time duration to switch talen specs, dual spec command .swapspec +# ################################################################################################################### Rate.Health = 1 @@ -1544,6 +1547,7 @@ Death.Bones.World = 1 Death.Bones.Battleground = 1 Death.Ghost.RunSpeed.World = 1.0 Death.Ghost.RunSpeed.Battleground = 1.0 +DualSpecTimeDelta = 300 ################################################################################################################### # BATTLEGROUND CONFIG