From 6a1f8eec897043d8e2bd267c1fcb48b2905f1f46 Mon Sep 17 00:00:00 2001 From: Docker Build Date: Tue, 12 May 2026 23:02:11 -0400 Subject: [PATCH] Paragon tester hunter BiS, mount cast QoL, learn all mounts RBAC, trade cap 11 - mod-paragon: .paragon tester bis hunter (Sanctified Ahn'Kahar Blood Hunter + Windrunner's Heartseeker), bis gems kits, AGI bow vs ranged/gun/crossbow, ranged for spi/hybrid weapons. - .learn all mounts: RBAC 916 + db_auth migration 2026_05_12_00.sql. - Cast-time mount spells: allow start/complete while moving; block in combat; interrupt mount cast on combat enter; relax movement prevention for NPCs/units. - MaxPrimaryTradeSkill default 11 (all WotLK primary professions) in WorldConfig + worldserver.conf.dist. Co-authored-by: Cursor --- data/sql/updates/db_auth/2026_05_12_00.sql | 10 + modules/mod-paragon/src/Paragon_Essence.cpp | 750 ++++++++++++++++++ .../apps/worldserver/worldserver.conf.dist | 4 +- src/server/game/Accounts/RBAC.h | 1 + .../game/Entities/Creature/Creature.cpp | 7 + src/server/game/Entities/Player/Player.h | 1 + .../game/Entities/Player/PlayerUpdates.cpp | 22 + src/server/game/Entities/Unit/Unit.cpp | 7 + src/server/game/Spells/Spell.cpp | 34 +- src/server/game/Spells/SpellInfo.cpp | 9 + src/server/game/Spells/SpellInfo.h | 3 + src/server/game/World/WorldConfig.cpp | 3 +- src/server/scripts/Commands/cs_learn.cpp | 62 ++ 13 files changed, 900 insertions(+), 13 deletions(-) create mode 100644 data/sql/updates/db_auth/2026_05_12_00.sql diff --git a/data/sql/updates/db_auth/2026_05_12_00.sql b/data/sql/updates/db_auth/2026_05_12_00.sql new file mode 100644 index 0000000..95ff073 --- /dev/null +++ b/data/sql/updates/db_auth/2026_05_12_00.sql @@ -0,0 +1,10 @@ +-- DB update 2026_05_03_00 -> 2026_05_12_00 +-- RBAC permission for .learn all mounts (Admin 196, Gamemaster 197). +DELETE FROM `rbac_permissions` WHERE `id` = 916; +INSERT INTO `rbac_permissions` (`id`, `name`) VALUES +(916, 'Command: learn all mounts'); + +DELETE FROM `rbac_linked_permissions` WHERE `linkedId` = 916; +INSERT INTO `rbac_linked_permissions` (`id`, `linkedId`) VALUES +(196, 916), +(197, 916); diff --git a/modules/mod-paragon/src/Paragon_Essence.cpp b/modules/mod-paragon/src/Paragon_Essence.cpp index b4ffaed..cd548ab 100644 --- a/modules/mod-paragon/src/Paragon_Essence.cpp +++ b/modules/mod-paragon/src/Paragon_Essence.cpp @@ -8,9 +8,11 @@ * separate (see README). */ +#include "Bag.h" #include "CharacterDatabase.h" #include "Chat.h" #include "CommandScript.h" +#include "Language.h" #include "Config.h" #include "Pet.h" #include "Player.h" @@ -23,6 +25,7 @@ #include "SpellMgr.h" #include "WorldDatabase.h" #include "WorldPacket.h" +#include "ObjectMgr.h" #include "Log.h" #include "DBCEnums.h" #include "DBCStores.h" @@ -32,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -4592,6 +4596,487 @@ public: } }; +// --- Paragon tester inventory helpers (ICC 25H-style curated lists; edit ids here for your fork) --- +// Paragon can equip any armor weight; BiS picks below intentionally mix plate/mail/leather/cloth per slot. + +uint32 ParagonTesterSelectLargestUsableBagItemId(Player const* player) +{ + static constexpr uint32 candidates[] = { 51809, 41600, 41599, 38082 }; + for (uint32 id : candidates) + { + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(id); + if (!proto || proto->Class != ITEM_CLASS_CONTAINER || proto->InventoryType != INVTYPE_BAG) + continue; + if (player->CanUseItem(proto) == EQUIP_ERR_OK) + return id; + } + return 0; +} + +uint32 ParagonTesterGrantItemList(Player* target, uint32 const* ids, size_t count, ChatHandler* handler) +{ + uint32 granted = 0; + for (size_t i = 0; i < count; ++i) + { + uint32 const itemId = ids[i]; + if (!itemId) + continue; + + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (!proto) + { + handler->PSendSysMessage("Paragon tester kit: item {} is not defined (skipped).", itemId); + continue; + } + + ItemPosCountVec dest; + InventoryResult const msg = target->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, itemId, 1); + if (msg != EQUIP_ERR_OK || dest.empty()) + { + handler->PSendSysMessage("Paragon tester kit: cannot store {} ({}) — free bag space?", itemId, uint32(msg)); + continue; + } + + if (Item* item = target->StoreNewItem(dest, itemId, true)) + { + item->SetBinding(false); + ++granted; + } + } + return granted; +} + +struct ParagonTesterStackedGrant +{ + uint32 itemId; + uint32 count; +}; + +// Grants stackable/consumable items in one StoreNewItem per line (gems, scrolls, scopes, etc.). +uint32 ParagonTesterGrantStackedItemList(Player* target, ParagonTesterStackedGrant const* grants, size_t grantCount, ChatHandler* handler) +{ + uint32 totalPieces = 0; + for (size_t i = 0; i < grantCount; ++i) + { + uint32 const itemId = grants[i].itemId; + uint32 count = grants[i].count; + if (!itemId || !count) + continue; + + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (!proto) + { + handler->PSendSysMessage("Paragon tester kit: item {} is not defined (skipped).", itemId); + continue; + } + + uint32 noSpaceForCount = 0; + ItemPosCountVec dest; + InventoryResult const msg = target->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, itemId, count, &noSpaceForCount); + if (msg != EQUIP_ERR_OK) + count -= noSpaceForCount; + + if (!count || dest.empty()) + { + handler->PSendSysMessage("Paragon tester kit: cannot store {} x{} ({}) — free bag space?", itemId, grants[i].count, uint32(msg)); + continue; + } + + if (Item* item = target->StoreNewItem(dest, itemId, true)) + { + item->SetBinding(false); + totalPieces += count; + } + } + return totalPieces; +} + +void ParagonTesterClearNonEquipmentInventory(Player* player) +{ + for (uint8 bagSlot = INVENTORY_SLOT_BAG_START; bagSlot < INVENTORY_SLOT_BAG_END; ++bagSlot) + { + if (Bag* bag = player->GetBagByPos(bagSlot)) + { + for (uint32 j = 0; j < bag->GetBagSize(); ++j) + { + if (bag->GetItemByPos(j)) + player->DestroyItem(bagSlot, j, true); + } + } + + if (player->GetItemByPos(INVENTORY_SLOT_BAG_0, bagSlot)) + player->DestroyItem(INVENTORY_SLOT_BAG_0, bagSlot, true); + } + + for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot) + { + if (player->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + player->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true); + } +} + +void ParagonTesterStringToLowerAscii(std::string& s) +{ + for (char& c : s) + c = static_cast(std::tolower(static_cast(c))); +} + +std::string ParagonTesterNormalizeWeaponTypeKey(std::string_view raw) +{ + std::string t; + for (unsigned char ch : raw) + { + if (ch == ' ' || ch == '\t' || ch == '-' || ch == '_' || ch == '/') + continue; + t.push_back(static_cast(std::tolower(ch))); + } + return t; +} + +// If any bag slot 19–22 is free and the item is a container, move it from inventory onto that slot (Player.cpp pattern). +bool ParagonTesterTryEquipBagToFirstEmptySlot(Player* player, Item* bag) +{ + if (!player || !bag) + return false; + + ItemTemplate const* proto = bag->GetTemplate(); + if (!proto || proto->Class != ITEM_CLASS_CONTAINER || proto->InventoryType != INVTYPE_BAG) + return false; + + uint16 eDest = 0; + if (player->CanEquipItem(NULL_SLOT, eDest, bag, false) != EQUIP_ERR_OK) + return false; + + uint8 const srcBag = bag->GetBagSlot(); + uint8 const srcSlot = bag->GetSlot(); + player->RemoveItem(srcBag, srcSlot, true); + player->EquipItem(eDest, bag, true); + return true; +} + +// Curated ICC-era ids (db_world item_template). Extend as needed for your fork. +bool ParagonTesterResolveWeaponKit(std::string statRaw, std::string typeRaw, std::vector& out, std::string& err) +{ + out.clear(); + ParagonTesterStringToLowerAscii(statRaw); + // trim stat + while (!statRaw.empty() && statRaw.front() == ' ') + statRaw.erase(statRaw.begin()); + while (!statRaw.empty() && statRaw.back() == ' ') + statRaw.pop_back(); + + std::string stat = statRaw; + if (stat == "strength") + stat = "str"; + else if (stat == "agility" || stat == "dex" || stat == "dexterity") + stat = "agi"; + else if (stat == "intellect") + stat = "int"; + else if (stat == "spirit") + stat = "spi"; + else if (stat == "apsp" || stat == "spellstrike") + stat = "hybrid"; + + std::string const wkey = ParagonTesterNormalizeWeaponTypeKey(typeRaw); + if (stat.empty() || wkey.empty()) + { + err = "usage: .paragon tester weapons — see `.paragon tester weapons` with no args for help."; + return false; + } + + auto push = [&](std::initializer_list ids) + { + for (uint32 id : ids) + if (id) + out.push_back(id); + }; + + if (stat == "str") + { + // "2h sword", "2h/sword", "2h axe" → 2hsword / 2haxe after normalize (slashes stripped like spaces). + if (wkey == "2hsword" || wkey == "twohandsword") + push({ 50730 }); // Glorenzelg (2H sword) + else if (wkey == "2haxe" || wkey == "twohandaxe") + push({ 50709 }); // Bryntroll (2H axe) + else if (wkey == "2hmace" || wkey == "twohandmace") + push({ 50603 }); // Cryptmaker (2H mace) + else if (wkey == "1hsword" || wkey == "onehandsword") + push({ 50737 }); // Havoc's Call (1H sword) + else if (wkey == "1haxe" || wkey == "onehandaxe") + push({ 50654 }); // Scourgeborne Waraxe (1H axe) + else if (wkey == "1hmace" || wkey == "onehandmace" || wkey == "1hhammer") + push({ 50738 }); // Mithrios (1H mace) + else if (wkey == "2h" || wkey == "twohand" || wkey == "zwei" || wkey == "great" || wkey == "polearm") + push({ 50730 }); + else if (wkey == "dual" || wkey == "dw" || wkey == "dualwield" || wkey == "dualwielding") + push({ 50738, 50737 }); + else if (wkey == "sword" || wkey == "swords" || wkey == "1h") + push({ 50737 }); + else if (wkey == "mace" || wkey == "hammer") + push({ 50738 }); + else if (wkey == "axe") + push({ 50654 }); + else if (wkey == "ranged" || wkey == "bow" || wkey == "gun" || wkey == "crossbow" || wkey == "thrown") + push({ 50733 }); + else + { + err = fmt::format( + "unknown STR weapon type \"{}\" (try 2h sword, 2h axe, 2h mace, 1h sword, 1h axe, 1h mace, 2h, dual, sword, mace, axe, ranged).", + typeRaw); + return false; + } + return true; + } + + if (stat == "agi") + { + if (wkey == "2h" || wkey == "twohand" || wkey == "polearm") + push({ 50735 }); + else if (wkey == "dual" || wkey == "dw" || wkey == "dualwield" || wkey == "daggers") + push({ 50736, 50676 }); + else if (wkey == "dagger") + push({ 50736 }); + else if (wkey == "sword" || wkey == "swords" || wkey == "1h") + push({ 50672 }); + else if (wkey == "fist" || wkey == "fistweapon" || wkey == "claw") + push({ 50676 }); + else if (wkey == "staff" || wkey == "staves") + push({ 50731 }); // caster staff; use as generic high-ilvl staff for testers + else if (wkey == "bow") + push({ 51940 }); // Windrunner's Heartseeker (hunter-style bow) + else if (wkey == "ranged" || wkey == "gun" || wkey == "crossbow" || wkey == "thrown") + push({ 50733 }); // Fal'inrush (BiS gun) + else + { + err = fmt::format("unknown AGI weapon type \"{}\" (try 2h, dual, dagger, sword, fist, staff, bow, ranged).", typeRaw); + return false; + } + return true; + } + + if (stat == "int") + { + if (wkey == "staff" || wkey == "staves") + push({ 50731 }); + else if (wkey == "wand") + push({ 50684 }); + else if (wkey == "mhoh" || wkey == "ohmh" || wkey == "dual" || wkey == "dw" || wkey == "moh") + push({ 50732, 50734 }); + else if (wkey == "mh" || wkey == "mainhand" || wkey == "sword" || wkey == "mace" || wkey == "dagger") + push({ 50732 }); + else if (wkey == "oh" || wkey == "offhand") + push({ 50734 }); + else if (wkey == "shield") + push({ 50729 }); + else if (wkey == "ranged" || wkey == "bow" || wkey == "gun" || wkey == "crossbow") + push({ 50733 }); + else + { + err = fmt::format("unknown INT weapon type \"{}\" (try staff, wand, mhoh, mh, oh, shield, ranged).", typeRaw); + return false; + } + return true; + } + + if (stat == "spi") + { + if (wkey == "staff" || wkey == "staves") + push({ 50725 }); + else if (wkey == "wand") + push({ 50684 }); + else if (wkey == "mace" || wkey == "mh") + push({ 50732 }); + else if (wkey == "mhoh" || wkey == "ohmh") + push({ 50732, 50734 }); + else if (wkey == "shield") + push({ 50729 }); + else if (wkey == "ranged" || wkey == "bow" || wkey == "gun" || wkey == "crossbow" || wkey == "thrown") + push({ 50733 }); + else + { + err = fmt::format("unknown SPI weapon type \"{}\" (try staff, wand, mace, mhoh, shield, ranged).", typeRaw); + return false; + } + return true; + } + + if (stat == "tank") + { + if (wkey == "shield") + push({ 50729 }); + else if (wkey == "sword" || wkey == "swords" || wkey == "1h") + push({ 50738 }); + else if (wkey == "mace" || wkey == "hammer") + push({ 50738 }); + else if (wkey == "swordboard" || wkey == "sb" || wkey == "mit" || wkey == "1hshield" || wkey == "threat") + push({ 50738, 50729 }); + else if (wkey == "dual" || wkey == "dw") + push({ 50738, 50737 }); + else if (wkey == "2h" || wkey == "twohand") + push({ 50730 }); + else if (wkey == "ranged" || wkey == "bow" || wkey == "gun" || wkey == "crossbow" || wkey == "thrown") + push({ 50733 }); + else if (wkey == "sigil" || wkey == "relic") + push({ 50462 }); + else + { + err = fmt::format("unknown tank weapon type \"{}\" (try swordboard, shield, sword, mace, dual, 2h, ranged, sigil).", typeRaw); + return false; + } + return true; + } + + if (stat == "hybrid") + { + if (wkey == "staff") + push({ 50731 }); + else if (wkey == "wand") + push({ 50684 }); + else if (wkey == "shield") + push({ 50729 }); + else if (wkey == "2h" || wkey == "twohand") + push({ 50735 }); + else if (wkey == "dual" || wkey == "dw" || wkey == "mhoh" || wkey == "default") + push({ 50732, 50734 }); + else if (wkey == "ranged" || wkey == "bow" || wkey == "gun" || wkey == "crossbow" || wkey == "thrown") + push({ 50733 }); + else + { + err = fmt::format("unknown hybrid weapon type \"{}\" (try mhoh, staff, wand, shield, 2h, ranged).", typeRaw); + return false; + } + return true; + } + + err = fmt::format("unknown stat \"{}\" (use str, agi, int, spi, tank, hybrid).", statRaw); + return false; +} + +// Order: head, shoulders, chest, hands, legs, bracers, belt, boots, neck, cloak, weapons…, trinkets, rings. +// Item ids verified against stock AC db_world item_template (3.3.5a). +static constexpr uint32 kTesterBisStr[] = { + 50712, 51229, 50656, 50675, 50624, // helm, shoulders (51229), chest, hands, legs — not 51211 (that id is Ymirjar legs) + 54580, 50620, 54578, 54581, 50653, // Umbrage + Coldwraith + Apocalypse's Advance + Penumbra + Shadowvault + 50730, 50733, + 50363, 54590, + 50657, 54576, +}; + +static constexpr uint32 kTesterBisAgi[] = { + 51242, 51299, 51298, 51243, 51241, // Frost Witch (mail) + Lasherweave (leather) mix + 50670, 50688, 50607, 50633, 50653, + 50736, 50733, + 50363, 54590, + 50657, 54576, +}; + +static constexpr uint32 kTesterBisInt[] = { + 51281, 51245, 51283, 51280, 51246, // Bloodmage cloth + Frost Witch (mail) shoulders/legs + 50686, 50702, 50699, 50724, 50628, + 50732, 50734, 50684, + 50346, 50360, + 50610, 50664, +}; + +static constexpr uint32 kTesterBisSpi[] = { + 51237, 51257, 51239, 51256, 51258, // Resto Frost Witch (mail) + Crimson Acolyte (cloth) + 50686, 50702, 50699, 50724, 50628, + 50725, + 50360, 50366, + 50610, 50664, +}; + +static constexpr uint32 kTesterBisTank[] = { + 51306, 51309, 51305, 51307, 51308, // Sanctified Scourgelord (plate, DK tank profile) + 50611, 50620, 50625, 50609, 50677, + 50738, 50729, 50462, + 50364, 54591, + 50404, 50657, +}; + +// AP main-hand + SP off-hand + mail enhancer T10 (ICC); for hybrid battlemage-style testers. +static constexpr uint32 kTesterBisHybrid[] = { + 51242, 51240, 51244, 51243, 51241, + 54580, 50620, 54578, 54581, 50653, + 50732, 50734, + 50363, 50346, + 50657, 50610, +}; + +// Sanctified Ahn'Kahar Blood Hunter (277) + ICC phys offsets; ranged slot only (Windrunner's Heartseeker). +static constexpr uint32 kTesterBisHunter[] = { + 51286, 51288, 51289, 51285, 51287, + 50670, 50688, 50607, 50633, 50653, + 0, 51940, + 50363, 54590, + 50657, 54576, +}; + +// ICC-era gems (stacked), enchant scrolls, belt buckle, leg armor/spellthread, Sons of Hodir shoulders, Ebon Blade / Kirin Tor helms. +// Item ids from db_world item_template; tweak counts for your fork. +static constexpr uint32 kGemStack = 20; +static constexpr uint32 kGemStackMed = 12; +static constexpr uint32 kGemStackSmall = 8; +static constexpr uint32 kScrollPair = 2; +static constexpr uint32 kMetaCount = 3; +static constexpr uint32 kBeltBuckle = 4; +static constexpr uint32 kLegKit = 4; +static constexpr uint32 kAugmentPair = 2; +static constexpr uint32 kScopeKit = 4; + +static constexpr ParagonTesterStackedGrant kTesterGemsStr[] = { + { 40111, kGemStack }, { 40117, kGemStack }, { 40114, kGemStack }, { 40116, kGemStackMed }, { 40118, kGemStackMed }, + { 40119, kGemStackSmall }, { 40142, kGemStackMed }, { 40143, kGemStackMed }, { 40153, kGemStackMed }, { 40162, kGemStackMed }, + { 41285, kMetaCount }, { 41398, 2 }, + { 44493, kScrollPair }, { 44815, kScrollPair }, { 44465, kScrollPair }, { 39006, kScrollPair }, { 39003, kScrollPair }, + { 44458, kScrollPair }, { 41611, kBeltBuckle }, { 38374, kLegKit }, { 50335, kAugmentPair }, { 50367, kAugmentPair }, +}; + +static constexpr ParagonTesterStackedGrant kTesterGemsAgi[] = { + { 40112, kGemStack }, { 40117, kGemStack }, { 40114, kGemStackMed }, { 40142, kGemStackMed }, { 40152, kGemStackMed }, + { 40153, kGemStackMed }, { 40155, kGemStackMed }, { 40125, kGemStackMed }, { 41398, kMetaCount }, { 41285, 2 }, + { 44493, kScrollPair }, { 44815, kScrollPair }, { 44465, kScrollPair }, { 39006, kScrollPair }, { 39003, kScrollPair }, + { 44458, kScrollPair }, { 38986, kScrollPair }, { 41611, kBeltBuckle }, { 38373, kLegKit }, { 50335, kAugmentPair }, { 50367, kAugmentPair }, +}; + +static constexpr ParagonTesterStackedGrant kTesterGemsInt[] = { + { 40113, kGemStack }, { 40155, kGemStackMed }, { 40153, kGemStackMed }, { 40133, kGemStackMed }, { 40119, kGemStackSmall }, + { 40125, kGemStackMed }, { 41285, kMetaCount }, + { 44467, kScrollPair }, { 44470, kScrollPair }, { 38979, kScrollPair }, { 39003, kScrollPair }, { 39006, kScrollPair }, + { 44465, kScrollPair }, { 38973, kScrollPair }, { 41611, kBeltBuckle }, { 41602, kLegKit }, { 50338, kAugmentPair }, { 50368, kAugmentPair }, +}; + +static constexpr ParagonTesterStackedGrant kTesterGemsSpi[] = { + { 40113, kGemStackMed }, { 40133, kGemStack }, { 40120, kGemStackMed }, { 40119, kGemStackSmall }, { 40155, kGemStackMed }, + { 41285, kMetaCount }, + { 44470, kScrollPair }, { 38853, kScrollPair }, { 38961, kScrollPair }, { 38979, kScrollPair }, { 39006, kScrollPair }, + { 44465, kScrollPair }, { 41611, kBeltBuckle }, { 41601, kLegKit }, { 50336, kAugmentPair }, { 50370, kAugmentPair }, +}; + +static constexpr ParagonTesterStackedGrant kTesterGemsTank[] = { + { 40119, kGemStack }, { 40138, kGemStackMed }, { 40115, kGemStackMed }, { 40118, kGemStackMed }, { 40143, kGemStackMed }, + { 41285, 2 }, + { 38945, kScrollPair }, { 44489, kScrollPair }, { 38849, kScrollPair }, { 39006, kScrollPair }, { 44465, kScrollPair }, + { 41611, kBeltBuckle }, { 38373, kLegKit }, { 50337, kAugmentPair }, { 50369, kAugmentPair }, +}; + +static constexpr ParagonTesterStackedGrant kTesterGemsHybrid[] = { + { 40113, kGemStackMed }, { 40111, kGemStackMed }, { 40114, kGemStackMed }, { 40153, kGemStackMed }, { 40142, kGemStackMed }, + { 40155, kGemStackMed }, { 41285, kMetaCount }, { 41398, 2 }, + { 44467, kScrollPair }, { 44493, kScrollPair }, { 44815, kScrollPair }, { 44470, kScrollPair }, { 44465, kScrollPair }, + { 39006, kScrollPair }, { 39003, kScrollPair }, { 41611, kBeltBuckle }, { 41602, 2 }, { 38373, 2 }, { 38374, 2 }, + { 50338, kAugmentPair }, { 50335, kAugmentPair }, { 50368, 1 }, { 50367, 1 }, +}; + +// Hunter / physical ranged: scopes (engineering attach) + hit/agi gems + physical scrolls. +static constexpr ParagonTesterStackedGrant kTesterGemsRanged[] = { + { 44739, kScopeKit }, { 41167, kScopeKit }, { 41146, 2 }, + { 40112, kGemStack }, { 40117, kGemStack }, { 40125, kGemStack }, { 40142, kGemStackMed }, { 40152, kGemStackMed }, + { 40153, kGemStackMed }, { 41398, kMetaCount }, { 41285, 2 }, + { 44493, kScrollPair }, { 44815, kScrollPair }, { 44465, kScrollPair }, { 39006, kScrollPair }, { 39003, kScrollPair }, + { 44458, kScrollPair }, { 38986, kScrollPair }, { 41611, kBeltBuckle }, { 38373, kLegKit }, { 50335, kAugmentPair }, { 50367, kAugmentPair }, +}; + class Paragon_Essence_CommandScript : public CommandScript { public: @@ -4599,6 +5084,38 @@ public: ChatCommandTable GetCommands() const override { + static ChatCommandTable testerBisGemsTable = + { + { "str", HandleTesterBisGemsStr, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "agi", HandleTesterBisGemsAgi, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "int", HandleTesterBisGemsInt, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "spi", HandleTesterBisGemsSpi, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "tank", HandleTesterBisGemsTank, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "hybrid", HandleTesterBisGemsHybrid, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "ranged", HandleTesterBisGemsRanged, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "", HandleTesterBisGemsHelp, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + }; + + static ChatCommandTable testerBisTable = + { + { "str", HandleTesterBisStr, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "agi", HandleTesterBisAgi, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "int", HandleTesterBisInt, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "spi", HandleTesterBisSpi, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "tank", HandleTesterBisTank, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "hybrid", HandleTesterBisHybrid, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "hunter", HandleTesterBisHunter, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "gems", testerBisGemsTable }, + }; + + static ChatCommandTable testerTable = + { + { "bis", testerBisTable }, + { "bags", HandleTesterBags, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "weapons", HandleTesterWeapons, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + { "clearinv", HandleTesterClearInv, rbac::RBAC_PERM_COMMAND_ADDITEM, Console::No }, + }; + static ChatCommandTable paragonSubTable = { { "currency", HandleCurrency, rbac::RBAC_PERM_COMMAND_LEARN, Console::No }, @@ -4606,6 +5123,7 @@ public: { "runes", HandleRunes, rbac::RBAC_PERM_COMMAND_LEARN, Console::No }, { "hat", HandleHat, rbac::RBAC_PERM_COMMAND_LEARN, Console::No }, { "recalibrate", HandlePanelRecalibrate, rbac::RBAC_PERM_COMMAND_MODIFY, Console::No }, + { "tester", testerTable }, }; static ChatCommandTable commandTable = @@ -4616,6 +5134,238 @@ public: return commandTable; } + static bool HandleTesterBisKit(ChatHandler* handler, uint32 const* ids, size_t count, char const* label) + { + Player* target = handler->getSelectedPlayerOrSelf(); + if (!target) + { + handler->SendErrorMessage(LANG_PLAYER_NOT_FOUND); + return false; + } + + uint32 const n = ParagonTesterGrantItemList(target, ids, count, handler); + handler->PSendSysMessage("Paragon tester {} BiS: granted {} items to {}.", label, n, target->GetName()); + return n > 0; + } + + static bool HandleTesterBisStr(ChatHandler* handler) + { + return HandleTesterBisKit(handler, kTesterBisStr, sizeof(kTesterBisStr) / sizeof(kTesterBisStr[0]), "STR"); + } + + static bool HandleTesterBisAgi(ChatHandler* handler) + { + return HandleTesterBisKit(handler, kTesterBisAgi, sizeof(kTesterBisAgi) / sizeof(kTesterBisAgi[0]), "AGI"); + } + + static bool HandleTesterBisInt(ChatHandler* handler) + { + return HandleTesterBisKit(handler, kTesterBisInt, sizeof(kTesterBisInt) / sizeof(kTesterBisInt[0]), "INT"); + } + + static bool HandleTesterBisSpi(ChatHandler* handler) + { + return HandleTesterBisKit(handler, kTesterBisSpi, sizeof(kTesterBisSpi) / sizeof(kTesterBisSpi[0]), "SPI"); + } + + static bool HandleTesterBisTank(ChatHandler* handler) + { + return HandleTesterBisKit(handler, kTesterBisTank, sizeof(kTesterBisTank) / sizeof(kTesterBisTank[0]), "tank"); + } + + static bool HandleTesterBisHybrid(ChatHandler* handler) + { + return HandleTesterBisKit(handler, kTesterBisHybrid, sizeof(kTesterBisHybrid) / sizeof(kTesterBisHybrid[0]), "hybrid"); + } + + static bool HandleTesterBisHunter(ChatHandler* handler) + { + return HandleTesterBisKit(handler, kTesterBisHunter, sizeof(kTesterBisHunter) / sizeof(kTesterBisHunter[0]), "hunter (ranged)"); + } + + static bool HandleTesterBisGemsHelp(ChatHandler* handler) + { + handler->SendSysMessage( + "Paragon tester gems: .paragon tester bis gems \n" + " category: str | agi | int | spi | tank | hybrid | ranged\n" + " Grants stacked ICC-era gems, enchant scrolls, Eternal Belt Buckle, leg armor/spellthread, " + "Sons of Hodir shoulder inscriptions, and helm arcanums. " + "ranged adds engineering scopes (Diamond-cut Refractor, Heartseeker, Sun) plus AGI/hit gems."); + return false; + } + + static bool HandleTesterBisGemsKit(ChatHandler* handler, ParagonTesterStackedGrant const* grants, size_t count, char const* label) + { + Player* target = handler->getSelectedPlayerOrSelf(); + if (!target) + { + handler->SendErrorMessage(LANG_PLAYER_NOT_FOUND); + return false; + } + + uint32 const n = ParagonTesterGrantStackedItemList(target, grants, count, handler); + handler->PSendSysMessage("Paragon tester {} gems/enchants: granted {} item pieces (stacked lines) to {}.", + label, + n, + target->GetName()); + return n > 0; + } + + static bool HandleTesterBisGemsStr(ChatHandler* handler) + { + return HandleTesterBisGemsKit(handler, kTesterGemsStr, sizeof(kTesterGemsStr) / sizeof(kTesterGemsStr[0]), "STR"); + } + + static bool HandleTesterBisGemsAgi(ChatHandler* handler) + { + return HandleTesterBisGemsKit(handler, kTesterGemsAgi, sizeof(kTesterGemsAgi) / sizeof(kTesterGemsAgi[0]), "AGI"); + } + + static bool HandleTesterBisGemsInt(ChatHandler* handler) + { + return HandleTesterBisGemsKit(handler, kTesterGemsInt, sizeof(kTesterGemsInt) / sizeof(kTesterGemsInt[0]), "INT"); + } + + static bool HandleTesterBisGemsSpi(ChatHandler* handler) + { + return HandleTesterBisGemsKit(handler, kTesterGemsSpi, sizeof(kTesterGemsSpi) / sizeof(kTesterGemsSpi[0]), "SPI"); + } + + static bool HandleTesterBisGemsTank(ChatHandler* handler) + { + return HandleTesterBisGemsKit(handler, kTesterGemsTank, sizeof(kTesterGemsTank) / sizeof(kTesterGemsTank[0]), "tank"); + } + + static bool HandleTesterBisGemsHybrid(ChatHandler* handler) + { + return HandleTesterBisGemsKit(handler, kTesterGemsHybrid, sizeof(kTesterGemsHybrid) / sizeof(kTesterGemsHybrid[0]), "hybrid"); + } + + static bool HandleTesterBisGemsRanged(ChatHandler* handler) + { + return HandleTesterBisGemsKit(handler, kTesterGemsRanged, sizeof(kTesterGemsRanged) / sizeof(kTesterGemsRanged[0]), "RANGED"); + } + + static bool HandleTesterBags(ChatHandler* handler) + { + Player* target = handler->getSelectedPlayerOrSelf(); + if (!target) + { + handler->SendErrorMessage(LANG_PLAYER_NOT_FOUND); + return false; + } + + uint32 const bagId = ParagonTesterSelectLargestUsableBagItemId(target); + if (!bagId) + { + handler->SendSysMessage("Paragon tester bags: no bag template this character can use (check item ids)."); + return false; + } + + uint32 granted = 0; + uint32 equipped = 0; + for (int i = 0; i < 4; ++i) + { + ItemPosCountVec dest; + if (target->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, bagId, 1) != EQUIP_ERR_OK || dest.empty()) + { + handler->PSendSysMessage("Paragon tester bags: could only add {} bag(s); inventory full?", granted); + break; + } + + if (Item* item = target->StoreNewItem(dest, bagId, true)) + { + item->SetBinding(false); + ++granted; + if (ParagonTesterTryEquipBagToFirstEmptySlot(target, item)) + ++equipped; + } + } + + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(bagId); + handler->PSendSysMessage("Paragon tester bags: added {} x {} ({}); auto-equipped {} to bag bar for {}.", + granted, + bagId, + proto ? proto->Name1.c_str() : "?", + equipped, + target->GetName()); + return granted > 0; + } + + static bool HandleTesterWeapons(ChatHandler* handler, Tail tail) + { + std::string_view sv = tail; + while (!sv.empty() && sv.front() == ' ') + sv.remove_prefix(1); + + if (sv.empty()) + { + handler->SendSysMessage( + "Paragon tester weapons: .paragon tester weapons \n" + " stat: str | agi | int | spi | tank | hybrid (aliases: strength, agility, intellect, spirit, apsp)\n" + " type: depends on stat — e.g. str: 2h sword, 2h axe, 2h mace, 1h sword, 1h axe, 1h mace (or 2h/sword), dual, ranged | " + "agi: … bow (hunter bow) or ranged/gun/crossbow (Fal'inrush) | int: staff, wand, mhoh, shield, ranged | " + "spi/hybrid: … ranged"); + return false; + } + + std::size_t const sp = sv.find(' '); + if (sp == std::string_view::npos) + { + handler->SendSysMessage("Paragon tester weapons: need both and (see help with no args)."); + return false; + } + + std::string stat(sv.substr(0, sp)); + sv.remove_prefix(sp + 1); + while (!sv.empty() && sv.front() == ' ') + sv.remove_prefix(1); + if (sv.empty()) + { + handler->SendSysMessage("Paragon tester weapons: missing after stat."); + return false; + } + std::string weaponType(sv.begin(), sv.end()); + + Player* target = handler->getSelectedPlayerOrSelf(); + if (!target) + { + handler->SendErrorMessage(LANG_PLAYER_NOT_FOUND); + return false; + } + + std::vector ids; + std::string err; + if (!ParagonTesterResolveWeaponKit(stat, weaponType, ids, err)) + { + handler->PSendSysMessage("Paragon tester weapons: {}", err); + return false; + } + + uint32 const n = ParagonTesterGrantItemList(target, ids.data(), ids.size(), handler); + handler->PSendSysMessage("Paragon tester weapons [{} / {}]: granted {} item(s) to {}.", + stat, + weaponType, + n, + target->GetName()); + return n > 0; + } + + static bool HandleTesterClearInv(ChatHandler* handler) + { + Player* target = handler->getSelectedPlayerOrSelf(); + if (!target) + { + handler->SendErrorMessage(LANG_PLAYER_NOT_FOUND); + return false; + } + + ParagonTesterClearNonEquipmentInventory(target); + handler->PSendSysMessage("Paragon tester clearinv: removed backpack + bag contents (equipment untouched) for {}.", + target->GetName()); + return true; + } + // Full Character Advancement reset for the selected player (or self): // unlearn all panel spells/talents, clear panel DB + active build pointer, // then clamp AE/TE to the level-correct totals (same math as login diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index 1f34797..740961c 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -2260,9 +2260,9 @@ Achievement.RealmFirstKillWindow = 60 # MaxPrimaryTradeSkill # Description: Maximum number of primary professions a character can learn. # Range: 0-11 -# Default: 2 +# Default: 11 - (All WotLK primary professions; set 2 for retail-like two-slot cap.) -MaxPrimaryTradeSkill = 2 +MaxPrimaryTradeSkill = 11 # # SkillChance.Prospecting diff --git a/src/server/game/Accounts/RBAC.h b/src/server/game/Accounts/RBAC.h index de2419a..d7f8867 100644 --- a/src/server/game/Accounts/RBAC.h +++ b/src/server/game/Accounts/RBAC.h @@ -678,6 +678,7 @@ enum RBACPermissions RBAC_PERM_COMMAND_BF_QUEUE = 913, RBAC_PERM_COMMAND_PET_LIST = 914, RBAC_PERM_COMMAND_PET_DELETE = 915, + RBAC_PERM_COMMAND_LEARN_ALL_MOUNTS = 916, // custom permissions 1000+ RBAC_PERM_MAX }; diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index 304f8ef..25ead66 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -3640,6 +3640,13 @@ bool Creature::IsMovementPreventedByCasting() const return false; } + // Fractured: cast-time mount summon (player-style mount spells on NPCs are rare but supported). + if (Spell* genSpell = m_currentSpells[CURRENT_GENERIC_SPELL]) + { + if (genSpell->getState() == SPELL_STATE_PREPARING && genSpell->m_spellInfo->IsCastTimeRidingMountSpell()) + return false; + } + if (HasSpellFocus()) { return true; diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 1b1ca05..564d179 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -1828,6 +1828,7 @@ public: uint32 GetLastPotionId() { return m_lastPotionId; } void SetLastPotionId(uint32 item_id) { m_lastPotionId = item_id; } void UpdatePotionCooldown(Spell* spell = nullptr); + void AtEnterCombat() override; void AtExitCombat() override; void setResurrectRequestData(ObjectGuid guid, uint32 mapId, float X, float Y, float Z, uint32 health, uint32 mana) diff --git a/src/server/game/Entities/Player/PlayerUpdates.cpp b/src/server/game/Entities/Player/PlayerUpdates.cpp index e7a8291..1d4e106 100644 --- a/src/server/game/Entities/Player/PlayerUpdates.cpp +++ b/src/server/game/Entities/Player/PlayerUpdates.cpp @@ -32,6 +32,7 @@ #include "Player.h" #include "ScriptMgr.h" #include "SkillDiscovery.h" +#include "Spell.h" #include "SpellAuraEffects.h" #include "SpellMgr.h" #include "UpdateFieldFlags.h" @@ -1561,6 +1562,27 @@ void Player::UpdatePvP(bool state, bool _override) sScriptMgr->OnPlayerPVPFlagChange(this, state); } +void Player::AtEnterCombat() +{ + Unit::AtEnterCombat(); + + // Fractured: cancel cast-time mount summon if combat starts mid-cast. + for (uint32 spellType = CURRENT_FIRST_NON_MELEE_SPELL; spellType < CURRENT_MAX_SPELL; ++spellType) + { + if (Spell* spell = GetCurrentSpell(CurrentSpellTypes(spellType))) + { + if (SpellInfo const* info = spell->GetSpellInfo()) + { + if (info->IsCastTimeRidingMountSpell()) + { + InterruptSpell(CurrentSpellTypes(spellType), false, false); + break; + } + } + } + } +} + void Player::AtExitCombat() { Unit::AtExitCombat(); diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 8b13873..9828242 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -4503,6 +4503,13 @@ bool Unit::IsMovementPreventedByCasting() const return false; } + // Fractured: cast-time mount summon may be completed while moving. + if (Spell* genSpell = m_currentSpells[CURRENT_GENERIC_SPELL]) + { + if (genSpell->getState() == SPELL_STATE_PREPARING && genSpell->m_spellInfo->IsCastTimeRidingMountSpell()) + return false; + } + // channeled spells during channel stage (after the initial cast timer) allow movement with a specific spell attribute if (Spell* spell = m_currentSpells[CURRENT_CHANNELED_SPELL]) { diff --git a/src/server/game/Spells/Spell.cpp b/src/server/game/Spells/Spell.cpp index a73e9ed..03fb65a 100644 --- a/src/server/game/Spells/Spell.cpp +++ b/src/server/game/Spells/Spell.cpp @@ -3589,14 +3589,18 @@ SpellCastResult Spell::prepare(SpellCastTargets const* targets, AuraEffect const // don't allow channeled spells / spells with cast time to be casted while moving // (even if they are interrupted on moving, spells with almost immediate effect get to have their effect processed before movement interrupter kicks in) + // Fractured: cast-time mount summons (SPELL_AURA_MOUNTED + non-zero base cast) may be started while moving. if ((m_spellInfo->IsChanneled() || m_casttime) && m_caster->IsPlayer() && m_caster->isMoving() && m_spellInfo->InterruptFlags & SPELL_INTERRUPT_FLAG_MOVEMENT && !IsTriggered()) { - // 1. Has casttime, 2. Or doesn't have flag to allow action during channel - if (m_casttime || !m_spellInfo->IsActionAllowedChannel()) + if (!m_spellInfo->IsCastTimeRidingMountSpell()) { - SendCastResult(SPELL_FAILED_MOVING); - finish(false); - return SPELL_FAILED_MOVING; + // 1. Has casttime, 2. Or doesn't have flag to allow action during channel + if (m_casttime || !m_spellInfo->IsActionAllowedChannel()) + { + SendCastResult(SPELL_FAILED_MOVING); + finish(false); + return SPELL_FAILED_MOVING; + } } } @@ -4436,9 +4440,11 @@ void Spell::update(uint32 difftime) // check if the player caster has moved before the spell finished // xinef: added preparing state (real cast, skip channels as they have other flags for this) + // Fractured: cast-time mount summons are allowed to complete while moving. if ((m_caster->IsPlayer() && m_timer != 0) && m_caster->isMoving() && (m_spellInfo->InterruptFlags & SPELL_INTERRUPT_FLAG_MOVEMENT) && m_spellState == SPELL_STATE_PREPARING && - (m_spellInfo->Effects[0].Effect != SPELL_EFFECT_STUCK || !m_caster->HasUnitMovementFlag(MOVEMENTFLAG_FALLING_FAR))) + (m_spellInfo->Effects[0].Effect != SPELL_EFFECT_STUCK || !m_caster->HasUnitMovementFlag(MOVEMENTFLAG_FALLING_FAR)) && + !m_spellInfo->IsCastTimeRidingMountSpell()) { // don't cancel for melee, autorepeat, triggered and instant spells if (!IsNextMeleeSwingSpell() && !IsAutoRepeat() && !IsTriggered()) @@ -5815,6 +5821,10 @@ SpellCastResult Spell::CheckCast(bool strict, uint32* /*param1*/, uint32* /*para if (reqCombat && m_caster->IsInCombat() && !m_spellInfo->CanBeUsedInCombat()) return SPELL_FAILED_AFFECTING_COMBAT; + + // Fractured: cast-time mount summons cannot be used while in combat (even if DBC omits NOT_IN_COMBAT_ONLY_PEACEFUL). + if (reqCombat && m_caster->IsInCombat() && m_spellInfo->IsCastTimeRidingMountSpell()) + return SPELL_FAILED_AFFECTING_COMBAT; } // Xinef: exploit protection @@ -5842,10 +5852,14 @@ SpellCastResult Spell::CheckCast(bool strict, uint32* /*param1*/, uint32* /*para // (not wand currently autorepeat cast delayed to moving stop anyway in spell update code) if (m_caster->IsPlayer() && m_caster->ToPlayer()->isMoving() && !IsTriggered()) { - // skip stuck spell to allow use it in falling case and apply spell limitations at movement - if ((!m_caster->HasUnitMovementFlag(MOVEMENTFLAG_FALLING_FAR) || m_spellInfo->Effects[0].Effect != SPELL_EFFECT_STUCK) && - (IsAutoRepeat() || (m_spellInfo->AuraInterruptFlags & AURA_INTERRUPT_FLAG_NOT_SEATED) != 0)) - return SPELL_FAILED_MOVING; + // Fractured: cast-time mount summons may be started while moving. + if (!m_spellInfo->IsCastTimeRidingMountSpell()) + { + // skip stuck spell to allow use it in falling case and apply spell limitations at movement + if ((!m_caster->HasUnitMovementFlag(MOVEMENTFLAG_FALLING_FAR) || m_spellInfo->Effects[0].Effect != SPELL_EFFECT_STUCK) && + (IsAutoRepeat() || (m_spellInfo->AuraInterruptFlags & AURA_INTERRUPT_FLAG_NOT_SEATED) != 0)) + return SPELL_FAILED_MOVING; + } } Vehicle* vehicle = m_caster->GetVehicle(); diff --git a/src/server/game/Spells/SpellInfo.cpp b/src/server/game/Spells/SpellInfo.cpp index 0a60f02..9a56fc2 100644 --- a/src/server/game/Spells/SpellInfo.cpp +++ b/src/server/game/Spells/SpellInfo.cpp @@ -909,6 +909,15 @@ bool SpellInfo::HasAura(AuraType aura) const return false; } +bool SpellInfo::IsCastTimeRidingMountSpell() const +{ + if (IsChanneled()) + return false; + if (!HasAura(SPELL_AURA_MOUNTED)) + return false; + return CalcCastTime(nullptr, nullptr) > 0; +} + bool SpellInfo::HasAnyAura() const { for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) diff --git a/src/server/game/Spells/SpellInfo.h b/src/server/game/Spells/SpellInfo.h index 57bfd53..cea844d 100644 --- a/src/server/game/Spells/SpellInfo.h +++ b/src/server/game/Spells/SpellInfo.h @@ -434,6 +434,9 @@ public: bool HasEffect(SpellEffects effect) const; bool HasEffectMechanic(Mechanics mechanic) const; bool HasAura(AuraType aura) const; + /// Summon mount aura (SPELL_AURA_MOUNTED) with a non-zero base cast time from SpellCastTimes.dbc. + /// Used by Fractured mount rules: castable while moving, never in combat, interrupted on combat enter. + bool IsCastTimeRidingMountSpell() const; bool HasAnyAura() const; bool HasAreaAuraEffect() const; bool HasOnlyDamageEffects() const; diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp index 6cab706..e77bf86 100644 --- a/src/server/game/World/WorldConfig.cpp +++ b/src/server/game/World/WorldConfig.cpp @@ -270,7 +270,8 @@ void WorldConfig::BuildConfigCache() SetConfigValue(CONFIG_INSTANCE_RESET_TIME_RELATIVE_TIMESTAMP, "Instance.ResetTimeRelativeTimestamp", 1135814400); SetConfigValue(CONFIG_INSTANCE_UNLOAD_DELAY, "Instance.UnloadDelay", 1800000); - SetConfigValue(CONFIG_MAX_PRIMARY_TRADE_SKILL, "MaxPrimaryTradeSkill", 2); + // WotLK has 11 primary profession skill lines (gathering + crafting); secondary (Cooking, Fishing, First Aid) are not limited here. + SetConfigValue(CONFIG_MAX_PRIMARY_TRADE_SKILL, "MaxPrimaryTradeSkill", 11); SetConfigValue(CONFIG_MIN_PETITION_SIGNS, "MinPetitionSigns", 9, ConfigValueCache::Reloadable::Yes, [](uint32 const& value) { return value <= 9; }, "<= 9"); SetConfigValue(CONFIG_GM_LOGIN_STATE, "GM.LoginState", 2); diff --git a/src/server/scripts/Commands/cs_learn.cpp b/src/server/scripts/Commands/cs_learn.cpp index b86e566..9089829 100644 --- a/src/server/scripts/Commands/cs_learn.cpp +++ b/src/server/scripts/Commands/cs_learn.cpp @@ -16,6 +16,7 @@ */ #include "CommandScript.h" +#include "DBCStores.h" #include "Language.h" #include "ObjectMgr.h" #include "Pet.h" @@ -51,6 +52,7 @@ public: { "default", HandleLearnAllDefaultCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_DEFAULT, Console::No }, { "lang", HandleLearnAllLangCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_LANG, Console::No }, { "recipes", HandleLearnAllRecipesCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_RECIPES, Console::No }, + { "mounts", HandleLearnAllMountsCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_MOUNTS, Console::No }, }; static ChatCommandTable learnCommandTable = @@ -386,6 +388,66 @@ public: return true; } + static void SetRidingSkillToMaxForPlayer(Player* player) + { + SkillRaceClassInfoEntry const* rcInfo = GetSkillRaceClassInfo(SKILL_RIDING, player->getRace(), player->getClass()); + if (!rcInfo || GetSkillRangeType(rcInfo) != SKILL_RANGE_RANK) + return; + + SkillTiersEntry const* tier = sSkillTiersStore.LookupEntry(rcInfo->SkillTierID); + if (!tier) + return; + + uint8 rank = 0; + uint16 maxValue = 0; + for (uint8 i = 0; i < MAX_SKILL_STEP; ++i) + { + if (tier->Value[i] == 0) + continue; + rank = i + 1; + maxValue = tier->Value[i]; + } + + if (!rank || !maxValue) + return; + + player->SetSkill(SKILL_RIDING, rank, maxValue, maxValue); + } + + static bool HandleLearnAllMountsCommand(ChatHandler* handler) + { + Player* target = handler->getSelectedPlayer(); + if (!target) + { + handler->SendSysMessage(LANG_PLAYER_NOT_FOUND); + return false; + } + + SetRidingSkillToMaxForPlayer(target); + handler->PSendSysMessage("Set Riding skill to maximum for {}.", handler->GetNameLink(target)); + + uint32 learned = 0; + for (uint32 i = 0; i < sSpellMgr->GetSpellInfoStoreSize(); ++i) + { + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(i); + if (!spellInfo || !SpellMgr::IsSpellValid(spellInfo)) + continue; + + if (!spellInfo->HasAura(SPELL_AURA_MOUNTED)) + continue; + + if (target->HasSpell(i)) + continue; + + target->learnSpell(i, false); + if (target->HasSpell(i)) + ++learned; + } + + handler->PSendSysMessage("Learned {} mount spell(s) for {}.", learned, handler->GetNameLink(target)); + return true; + } + static void HandleLearnSkillRecipesHelper(Player* player, uint32 skillId) { uint32 classmask = player->getClassMask();