diff --git a/data/sql/updates/db_world/2022_03_14_00.sql b/data/sql/updates/db_world/2022_03_14_00.sql new file mode 100644 index 000000000..4aaf5834c --- /dev/null +++ b/data/sql/updates/db_world/2022_03_14_00.sql @@ -0,0 +1,66 @@ +-- DB update 2022_03_13_00 -> 2022_03_14_00 +DROP PROCEDURE IF EXISTS `updateDb`; +DELIMITER // +CREATE PROCEDURE updateDb () +proc:BEGIN DECLARE OK VARCHAR(100) DEFAULT 'FALSE'; +SELECT COUNT(*) INTO @COLEXISTS +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'version_db_world' AND COLUMN_NAME = '2022_03_13_00'; +IF @COLEXISTS = 0 THEN LEAVE proc; END IF; +START TRANSACTION; +ALTER TABLE version_db_world CHANGE COLUMN 2022_03_13_00 2022_03_14_00 bit; +SELECT sql_rev INTO OK FROM version_db_world WHERE sql_rev = '1643424341492582600'; IF OK <> 'FALSE' THEN LEAVE proc; END IF; +-- +-- START UPDATING QUERIES +-- + +INSERT INTO `version_db_world` (`sql_rev`) VALUES ('1643424341492582600'); + +DELETE FROM `npc_text` WHERE `ID` IN (10303, 10304); +INSERT INTO `npc_text` (`ID`, `text0_0`, `text0_1`, `BroadcastTextID0`) VALUES +(10303, 'Forgetting tribal leatherworking is not something to do lightly. If you choose to abandon it you will forget all recipes that require tribal leatherworking as well!', 'Forgetting tribal leatherworking is not something to do lightly. If you choose to abandon it you will forget all recipes that require tribal leatherworking as well!', 18974), +(10304, 'Forgetting dragonscale leatherworking is not something to do lightly. If you choose to abandon it you will forget all recipes that require dragonscale leatherworking as well!', 'Forgetting dragonscale leatherworking is not something to do lightly. If you choose to abandon it you will forget all recipes that require dragonscale leatherworking as well!', 18976); + +DELETE FROM `gossip_menu` WHERE `MenuID` IN (3068, 3069, 3073) AND `TextID` IN (3802, 3803, 3806); +DELETE FROM `gossip_menu` WHERE `MenuID` IN (3075, 3076, 3077) AND `TextID` IN (10302, 10303, 10304); +INSERT INTO `gossip_menu` (`MenuID`, `TextID`) VALUES +(3068, 3802), +(3069, 3803), +(3073, 3806), +(3075, 10304), -- dragonscale +(3076, 10302), -- elemental +(3077, 10303); -- tribal + +DELETE FROM `gossip_menu_option` WHERE `MenuID` IN (3067, 3068, 3069, 3070, 3072, 3073) AND `OptionID` = 0; +DELETE FROM `gossip_menu_option` WHERE `MenuID` IN (3067, 3068, 3069, 3070, 3072, 3073, 3075, 3076, 3077) AND `OptionID` = 1; +INSERT INTO `gossip_menu_option` (`MenuID`, `OptionID`, `OptionIcon`, `OptionText`, `OptionBroadcastTextID`, `OptionType`, `OptionNpcFlag`, `ActionMenuID`, `ActionPoiID`, `BoxCoded`, `BoxMoney`, `BoxText`, `BoxBroadcastTextID`, `VerifiedBuild`) VALUES +(3067, 0, 3, 'I would like to train.', 5597, 5, 16, 0, 0, 0, 0, '', 0, 0), +(3067, 1, 0, 'I wish to unlearn dragonscale leatherworking!', 18977, 1, 1, 3075, 0, 0, 0, '', 0, 0), +(3068, 0, 3, 'I would like to train.', 5597, 5, 16, 0, 0, 0, 0, '', 0, 0), +(3068, 1, 0, 'I wish to unlearn dragonscale leatherworking!', 18977, 1, 1, 3075, 0, 0, 0, '', 0, 0), +(3069, 0, 3, 'I would like to train.', 5597, 5, 16, 0, 0, 0, 0, '', 0, 0), +(3069, 1, 0, 'I wish to unlearn elemental leatherworking!', 18917, 1, 1, 3076, 0, 0, 0, '', 0, 0), +(3070, 0, 3, 'I would like to train.', 5597, 5, 16, 0, 0, 0, 0, '', 0, 0), +(3070, 1, 0, 'I wish to unlearn elemental leatherworking!', 18917, 1, 1, 3076, 0, 0, 0, '', 0, 0), +(3072, 0, 3, 'I would like to train.', 5597, 5, 16, 0, 0, 0, 0, '', 0, 0), +(3072, 1, 0, 'I wish to unlearn tribal leatherworking!', 18975, 1, 1, 3077, 0, 0, 0, '', 0, 0), +(3073, 0, 3, 'I would like to train.', 5597, 5, 16, 0, 0, 0, 0, '', 0, 0), +(3073, 1, 0, 'I wish to unlearn tribal leatherworking!', 18975, 1, 1, 3077, 0, 0, 0, '', 0, 0), +(3075, 1, 0, 'I wish to unlearn dragonscale leatherworking!', 18977, 1, 1, 0, 0, 0, 0, 'Do you really want to unlearn your leatherworking specialty and lose all associated recipes?', 18969, 0), +(3076, 1, 0, 'I wish to unlearn elemental leatherworking!', 18917, 1, 1, 0, 0, 0, 0, 'Do you really want to unlearn your leatherworking specialty and lose all associated recipes?', 18969, 0), +(3077, 1, 0, 'I wish to unlearn tribal leatherworking!', 18975, 1, 1, 0, 0, 0, 0, 'Do you really want to unlearn your leatherworking specialty and lose all associated recipes?', 18969, 0); + + +UPDATE `creature_template` SET `gossip_menu_id` = 3068, `npcflag` = `npcflag`|1 WHERE `entry` = 7867; -- Thorkaf Dragoneye +UPDATE `creature_template` SET `gossip_menu_id` = 3069, `npcflag` = `npcflag`|1 WHERE `entry` = 7869; -- Mrumn Winterhoof +UPDATE `creature_template` SET `gossip_menu_id` = 3073, `npcflag` = `npcflag`|1 WHERE `entry` = 7871; -- Se'Jib + +-- +-- END UPDATING QUERIES +-- +UPDATE version_db_world SET date = '2022_03_14_00' WHERE sql_rev = '1643424341492582600'; +COMMIT; +END // +DELIMITER ; +CALL updateDb(); +DROP PROCEDURE IF EXISTS `updateDb`; diff --git a/data/sql/updates/db_world/2022_03_14_01.sql b/data/sql/updates/db_world/2022_03_14_01.sql new file mode 100644 index 000000000..cf23527e8 --- /dev/null +++ b/data/sql/updates/db_world/2022_03_14_01.sql @@ -0,0 +1,299 @@ +-- DB update 2022_03_14_00 -> 2022_03_14_01 +DROP PROCEDURE IF EXISTS `updateDb`; +DELIMITER // +CREATE PROCEDURE updateDb () +proc:BEGIN DECLARE OK VARCHAR(100) DEFAULT 'FALSE'; +SELECT COUNT(*) INTO @COLEXISTS +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'version_db_world' AND COLUMN_NAME = '2022_03_14_00'; +IF @COLEXISTS = 0 THEN LEAVE proc; END IF; +START TRANSACTION; +ALTER TABLE version_db_world CHANGE COLUMN 2022_03_14_00 2022_03_14_01 bit; +SELECT sql_rev INTO OK FROM version_db_world WHERE sql_rev = '1646835034551886180'; IF OK <> 'FALSE' THEN LEAVE proc; END IF; +-- +-- START UPDATING QUERIES +-- + +INSERT INTO `version_db_world` (`sql_rev`) VALUES ('1646835034551886180'); + +REPLACE INTO `quest_template_locale` (ID, locale, Title, Details, Objectives, EndText, CompletedText, ObjectiveText1, ObjectiveText2, ObjectiveText3, ObjectiveText4, VerifiedBuild) +VALUES +(13040,'zhTW','死馬當活馬醫','受傷的士兵數目與日俱增。他們正被某種前所未見的毒物侵襲。不論成份是什麼,這些奈幽蟲族施放出來的毒液,都是致命而無藥可救的!目前為止,我們僅能減緩毒質在病人體內擴散的速度。我們必須製出解藥!$B$B朝西北方去,到先鋒駐地外的山谷中,殺死一些遺忘深淵奈幽蟲族。摘下他們的毒腺,帶回來給我。','寒冰皇冠中,銀白先鋒駐地的賈斯塔夫神父,要求你帶回10個遺忘深淵毒囊。','','到寒冰皇冠的銀白先鋒駐地找賈斯塔夫神父。','','','','',0), +(13041,'zhTW','使命必達','很高興你出現了!我現在就得交出幽坑城訂購的發光象牙刻像,但我還缺一個玉髓才能替這套珠寶畫龍點睛。$B$B替我帶一個玉髓來,你就可以得到一個達拉然珠寶匠徽章。','把一個玉髓交給達拉然的提摩西‧瓊斯。','','到達拉然的卡地亞珠寶公司找提摩西·瓊斯。','','','','',0), +(13042,'zhTW','深入地下大廳','在東北方,地下大廳的深處,有個名為恐怖博士的瘋狂科學家,他正準備做些名副其實的恐怖壞事。$B$B在巫妖王的特許下,他打算在瓦苟身上做些實驗 - 這個維酷人一文不名地橫屍沙場,然後成了天譴軍團的一員。$B$B我們必須阻止他,但是我的占兆術無法找到他。一定有某人在幫助他。$B$B向他的學徒探明這件事,然後,別讓他看見明天的太陽!','尼約達村的骸骨女巫要你拷問學徒歐斯特基爾格,問出有關恐怖博士所從事工作的情報。$B$B然後殺人滅口。','','到寒冰皇冠的尼約達村找骸骨女巫。','從學徒歐斯特基爾格那探聽到情報','','','',0), +(13043,'zhTW','聚沙成塔','打開這本書,你發現上面充斥著噁心的圖解,說明如何建造博士稱之為「終極武器」的東西!$B$B顯然試作的原型產物,奈傑德,就是驚怖大廳盡頭矗立的那個巨型血肉巨人。$B$B如果他們建立了整支像這樣的軍隊,就意味著末日的到來。唯一能夠避免這件事的方法就只有殺了恐怖博士!$B$B要是你能夠控制這個傀儡,也許博士就會現身?','乘上奈傑德並控制它。擊退地下大廳的居民,直到恐怖博士現身。$B$B用奈傑德殺死博士,然後回去找尼約達村的骸骨女巫。','','到寒冰皇冠的尼約達村找骸骨女巫。','','','','',0), +(13044,'zhTW','如果有生還者...','越過西方的回聲山谷,穿過止境,就能看見第一個寒冰皇冠的天譴營地-天譴岸地。我們在天譴岸地打了一場漂亮的仗,殺了不少天譴軍,但是最後仍不得不撤退。許多士兵走散了-是生是死仍是未知數。$B$B如果有生還者的話,我們必須救出他們。沿著這條路往東南方走,就可以找到潘農布理斯,青銅龍軍團的盟友與元龍雛龍的飼養者。找到他,並尋求他的幫助。','和潘農布理斯對話,他在寒冰皇冠的銀白先鋒駐地。','','','','','','',0), +(13045,'zhTW','駭速快手','我們將盡我們所能地協助你,$n。元龍雛龍自願載你飛越止境,進入天譴岸地。盡量救出任何被俘虜的銀白十字軍。將他們帶回這裡,安置在基地中央的先鋒醫護站。$B$B在我後面的圍欄裡挑一頭銀白天爪,然後去吧!天譴岸地在西北方遠處,你得先越過回聲山谷,並穿越遼闊的止境。','在天譴岸地救出3個被俘虜的銀白十字軍,將他們安置在先鋒駐地中心的先鋒醫護站。並向大領主提里奧‧弗丁呈報你的捷報。','','到寒冰皇冠的銀白先鋒駐地找大領主提里奧·弗丁。','解救被俘虜的十字軍','','','',0), +(13066,'zhTW','由羅克長者','','','','','','','','',0), +(13067,'zhTW','修干加達長者','','','','','','','','',0), +(13070,'zhTW','冷鋒逼近','死亡之寒在西方肆虐,威脅著要將我們的身心都冰封起來,並將我們擄給他在荒地上的黑暗主人。我們將堅守此地,並奮戰到底!$B$B我們已開始備戰。向攻城大師費茲克報到,他就在前門。','在寒冰皇冠的先鋒駐地,向攻城大師費茲克報到。','','','','','','',0), +(13100,'zhTW','灌能蘑菇肉糕','我們有張訂單得馬上出貨!『不只是配件』的歐爾頓‧班尼特需要一份蘑菇肉糕。$B$B弄來一些長在達拉然下水道的灌能蘑菇,還有冷肉塊,在任何一個火堆處放進鍋子裡燉煮,煮好後送到歐爾頓那兒去。','使用你的肉糕鍋,用4朵長在達拉然下水道的灌能蘑菇和2份冷肉塊煮出灌能蘑菇肉糕,然後送去給『不只是配件』的歐爾頓‧班尼特,就在達拉然的織符者廣場。','','到達拉然的『不只是配件』找歐爾頓·班尼特交談。','','','','',0), +(13101,'zhTW','戲法旅舍的會議','戲法旅舍的艾立爾‧蒼凝要舉辦一場會議,而且他得盡快籌備盛宴!$B$B你要提供他一些北地燉肉,還有一壺葡萄酒。你可以在達拉然中央一帶的『勸君更進一杯酒』起司店弄到酒。起司店的老闆欠我酒,你應該可以在店附近找到一壺。$B$B把酒和燉肉帶給艾立爾。','以你的烹飪技能作出4份北地燉肉,並從達拉然的起司店取走一壺葡萄酒,將其帶給戲法旅舍的艾立爾‧蒼凝,就在達拉然的織符者廣場。','','到達拉然的戲法旅舍找艾立爾·蒼凝交談。','','','','',0), +(13102,'zhTW','下水道燉菜','雅杰‧格林,地下道的旅店老闆,現在需要為在他那落腳的顧客張羅一份燉菜。$B$B在水晶之歌森林裡,摘取4根水晶之歌胡蘿蔔,和4份冷肉塊一起烹調。$B$B要去水晶之歌森林的話,可以透過織符者廣場東角的傳送門大廳。$B$B燉菜準備好後,送去給雅杰。','在烹調鍋中,將產自水晶之歌森林的4支水晶之歌胡蘿蔔,與4份冷肉塊一起烹調。$B$B燉菜備齊以後,送去給達拉然下水道的旅店老闆,雅杰‧格林。','','到達拉然的城底區的咒語與烏鴉找雅杰·格林交談。','','','','',0), +(13103,'zhTW','給怒金的起司','達拉然獸皮工店的拉尼德‧怒金現在急需葡萄酒和起司。$B$B從達拉然中心的『勸君更進一杯酒』貨架上取得起司,並在全市各處的桌上,收集半杯滿的葡萄酒。把收集來的酒倒在同一個瓶子中,和起司一起送去給拉尼德。$B$B我不覺得他分得出其中的差別。','在空的起司盤上,放上『勸君更進一杯酒』取來的陳年達拉然濃起司,並從達拉然市中各處的桌上,收集6杯半滿的達拉然葡萄酒杯。$B$B將起司和葡萄酒帶給達拉然獸皮工店的拉尼德‧怒金,店的位置在魔導師貿易區。','','到達拉然找獸皮工店的拉尼德‧怒金。','','','','',0), +(13104,'zhTW','重登止境吧,英雄','先鋒駐地已經鞏固了,吾友。我們重新奪回了回聲山谷,也拿下了封阻止境入口的網牆。現在正是在天譴岸地發動反攻,並在寒冰皇冠內建立據點的好時機。戰線就要動起來了!$B$B我已經向黯黑看守者提出請求,請他在我們對天譴岸地發動攻擊時提供協助。他對天譴軍團的了解,將為我們的努力帶來無比貢獻。$B$B現在就動身,去向他報到吧,他的帳篷就在止境之外,這裡的西北方。','向回聲山谷裡的黯黑看守者報到,就在止境之外。','','','','','','',0), +(13105,'zhTW','重登止境吧,英雄','先鋒駐地已經鞏固了,吾友。我們重新奪回了回聲山谷,也拿下了封阻止境入口的網牆。現在正是在天譴岸地發動反攻,並在寒冰皇冠內建立據點的好時機。戰線就要動起來了!$B$B我已經向黯黑看守者提出請求,請他在我們對天譴岸地發動攻擊時提供協助。他對天譴軍團的了解,將為我們的努力帶來無比貢獻。$B$B現在就動身,去向他報到吧,他的帳篷就在止境之外,這裡的西北方。','向回聲山谷裡的黯黑看守者報到,就在止境之外。','','','','','','',0), +(13107,'zhTW','芥末熱狗!','大法師潘塔魯斯偶爾喜歡看鬥球比賽,而他現在就要出發去看一場,但他需要帶些食物。$B$B你必須在他的野餐籃中,放入4份你做的犀牛熱狗,還有4份自市中綠地採來的野生芥末。$B$B把野餐籃送去給他,使他準備齊全地出發。','以烹飪技能調理4份犀牛熱狗,並自達拉然市內的綠地採來4份野生芥末,都放進空的野餐籃。$B$B然後將野餐籃送去給達拉然市內,卡薩斯平臺的大法師潘塔魯斯。','','到達拉然的卡薩斯平臺找大法師潘塔魯斯。','','','','',0), +(13110,'zhTW','死不瞑目','並非所有我們的士兵都能自天譴岸地平安歸來。許多人被殺害後,被轉化成醜惡而扭曲的另一種人格。這就是成為天譴軍團之途。對這些枉死的靈魂而言,並不能得到永恆的寧靜。我們應該要讓他們安息。$B$B越過止境到天譴岸地去吧,殺掉那些被轉化為骷髏的再活化十字軍。將聖水灑在他們的骸骨上,解救他們的靈魂。','在回聲山谷的賈斯塔夫神父要求你,在再活化的十字軍屍體上使用聖水,以解救10個不能安息的靈魂。','','到寒冰皇冠的回聲山谷找賈斯塔夫神父。','解救不能安息的靈魂','','','',0), +(13112,'zhTW','灌能蘑菇肉糕','『不只是配件』的歐爾頓‧班尼特需要一份蘑菇肉糕。$B$B弄來一些長在達拉然下水道的灌能蘑菇,還有份冷肉塊,在任何一個火堆處放進鍋子裡燉煮,烹調好後送到歐爾頓那兒去。$B$B只要他們願意嚐嚐地精的美味,我會讓他們一試成主顧的!','使用你的肉糕鍋,用4朵長在達拉然下水道的灌能蘑菇和2份冷肉塊煮出灌能蘑菇肉糕,然後送去給『不只是配件』的歐爾頓‧班尼特,就在達拉然的織符者廣場。','','到達拉然的『不只是配件』找歐爾頓·班尼特交談。','','','','',0), +(13113,'zhTW','戲法旅舍的會議','你相信嗎?部落竟然禁止了吃人的行為…我告訴你這是不對的!我還是專心完成我的訂單好了。$B$B戲法旅舍的艾立爾‧蒼凝有許多訪客,你得提供他一些北地燉肉,還有一壺葡萄酒,酒可以從達拉然市中心的起司店弄來。$B$B去吧,把酒和燉肉送去給艾立爾。','將4份北地燉肉與從達拉然的起司店取走的一壺葡萄酒,帶給戲法旅舍的艾立爾‧蒼凝,就在達拉然的織符者廣場。','','到達拉然的戲法旅舍找艾立爾·蒼凝交談。','','','','',0), +(13114,'zhTW','下水道燉菜','我們又有活得幹了。地下道旅店的老闆,現在要為他那餓死鬼般的顧客張羅一份燉菜。$B$B不過看來他不怎麼欣賞,我上回在燉菜裡放地精的廚藝。在水晶之歌森林裡,摘取4根水晶之歌胡蘿蔔,和4份冷肉塊一起烹調。然後送去給雅杰。$B$B要去水晶之歌森林的話,可以透過織符者廣場東角的傳送門大廳。','在烹調鍋中,將產自水晶之歌森林的4根水晶之歌胡蘿蔔,與4份冷肉塊一起烹調。$B$B燉菜煮好以後,送去給達拉然下水道的旅店老闆,雅杰‧格林。','','到達拉然的城底區的咒語與烏鴉找雅杰·格林交談。','','','','',0), +(13115,'zhTW','給怒金的起司','獸皮工店的拉尼德‧怒金,現在需要葡萄酒和起司。為什麼他不想嚐嚐我美味的地精呢?$B$B從達拉然中心的起司店弄點起司,然後從城市各處的桌子上,收集幾杯半滿的葡萄酒。把酒和起司一起送去給拉尼德。$B$B也許我們可以將其命名為雜燴美酒,是吧。','在空的起司盤上,放上『勸君更進一杯酒』取來的陳年達拉然濃起司,並從達拉然市中各處的桌上,收集6杯半滿的達拉然葡萄酒杯。$B$B將起司和葡萄酒帶給達拉然獸皮工店的拉尼德‧怒金,店的位置在魔導師貿易區。','','到達拉然的傳奇皮革找拉尼德·怒金交談。','','','','',0), +(13116,'zhTW','芥末熱狗!','大法師潘塔魯斯喜歡看激烈的鬥球比賽,但他需要帶些食物。$B$B你必須在他的野餐籃中,放入4份你做的犀牛熱狗,還有4份自市中綠地採來的野生芥末。$B$B用芥末熱狗塞滿他的野餐籃吧,小子。','以烹飪技能調理4份犀牛熱狗,並自達拉然市內的綠地採來4份野生芥末,都放進空的野餐籃。$B$B然後將野餐籃送去給達拉然市內,卡薩斯平臺的大法師潘塔魯斯。','','到達拉然的卡薩斯平臺找大法師潘塔魯斯交談。','','','','',0), +(13118,'zhTW','對天譴岸地的淨化','天譴軍團派遣大軍進犯銀白十字軍,但是被擊退。現在他們到處燒殺擄掠。他們在天譴岸地的所有俘虜,都被巫妖王操控,成了他的奴隸。$B$B無疑地,銀白十字軍一定會請你幫忙,讓他們的士兵能有尊嚴地死去。我對你的要求不多,為了成功,我們必須對天譴軍團進行無差別掃蕩。$B$B去天譴岸地吧,朝西北方穿過止境,讓所有阻擋你路途的人都倒下。','在寒冰皇冠,回聲山谷的黯黑看守者要求你,殺掉3名遺忘深淵高階祭師、3名遺忘深淵地底王、還有8名再活化的十字軍。','','到寒冰皇冠的回聲山谷找黯黑看守者。','','','','',0), +(13122,'zhTW','天譴石','天譴石,是每個天譴軍在誕生或被創造時,伴隨產生的一項物品。它對擁有者而言,有某些益處,然而更重要的,它能讓天譴軍團的領導者,隨時掌握每個單位的位置與動向。$B$B只要有足夠的天譴石,我就能追蹤其他天譴軍團成員,在寒冰皇冠上的行動。$B$B當你在天譴岸地上,對天譴軍團大開殺戒時,收集所有你能找到的天譴石。當你的背包再也裝不下,把它們帶回來給我。','在寒冰皇冠,回聲山谷裡的黯黑看守者要你帶回15個天譴石給他。','','到寒冰皇冠的回聲山谷找黯黑看守者。','','','','',0), +(13125,'zhTW','空氣也為之凍結','你必須擊敗天譴岸地的領主們,若要讓此地自天譴軍團的魔掌中解放出來。$B$B『血肉撕裂者』索拉納斯掌控了天譴岸地第一階的神殿。在索拉納斯附近,你可以找到高階祭師亞薩蒙,他在第二階的靈魂尖塔上宣道。再往上走,地底王塔婁諾斯則據有魔鬼之淵的庭院。$B$B他們都是可怕而不容小覷的敵手。需要幫助時,使用這把戰爭號角。亞榭洛的死亡騎士將會出現。','在寒冰皇冠,回聲山谷的黯黑看守者要求你,殺掉『血肉撕裂者』索拉納斯、高階祭師亞薩蒙、和地底王塔婁諾斯。','','到寒冰皇冠的回聲山谷找黯黑看守者。','','','','',0), +(13130,'zhTW','革命的基石','你可以看見天譴岸地,有個令人作嘔的腐朽坑洞,然而有天它將成為尊貴與榮耀之地。大領主弗丁將之稱為「正義要塞」。$B$B嗯,固若金湯如冰冠城塞,也是從一顆石頭開始建造的。所以,現在開始尋找革命的第一顆石頭吧。$B$B往南方的水晶之歌去,找到在其東邊的無縛灌木林。在灌木叢的底層收集結晶心材,並從珊達拉遺跡取得精靈石藝。','回聲山谷的十字軍建築師席拉斯要求你,採集10株結晶心材的樹幹,以及10份上古精靈石藝。','','到寒冰皇冠的回聲山谷找十字軍建築師席拉斯。','','','','',0), +(13135,'zhTW','它會殺了我們','此處的東南方,在水晶之歌森林的東界,有個被稱為無縛灌木林的地方。在那裡,存在各種生命的形式。灌木林中的生物有種共通的特點,就是他們對魔法能量起的共鳴。他們都擁有純淨的晶化能量。當其不穩定時,足以殺死我們每個人!$B$B前往無縛灌木林,殺掉其中的生物,奪取他們的晶化能量。我們需要補充燃料,而他們需要永遠的安息。我們稱之為「雙贏」。','在寒冰皇冠,回聲山谷的十字軍工程師史匹派崔要求你,收集並帶回8份晶化能量。','','到寒冰皇冠的回聲山谷找十字軍工程師史匹派崔。','','','','',0), +(13139,'zhTW','深入北裂境的冰凍心臟','席拉斯和史匹派崔點收了建造正義要塞所需的建材與燃料。多虧你和黯刃騎士團,天譴岸地裡的天譴軍團已潰不成軍,他們的軍官也都被擊敗。$B$B我將會挑選動工的地點,但是,在此之前,你得先將我的報告上呈給大領主提里奧‧弗丁。在他的領導下,大軍將開入寒冰皇冠。','將賈斯塔夫神父的報告呈給大領主提里奧‧弗丁,他在寒冰皇冠的銀白先鋒駐地。','','到寒冰皇冠的銀白先鋒駐地找大領主提里奧·弗丁。','','','','',0), +(13141,'zhTW','十字軍之巔保衛戰','<大領主弗丁握著一支銀白十字軍的旌旗。>$B$B這支旌旗是獨一無二的,$n。它帶著強力的祝福,可以使大片荒蕪的土地變得聖潔不可侵犯。$B$B你必須帶著它前往西北方,穿過止境,到天譴軍團嘲弄地稱為十字軍之巔的地方。將旌旗豎立在死去弟兄的顱骨堆頂端,然後防禦任何攻擊。聖滌的過程結束後,去找賈斯塔夫神父。$B$B現在去吧,$n。願你心中無畏無懼。','前往十字軍之巔,在十字軍顱骨堆上使用十字軍祝福旌旗。防禦天譴軍團發動的攻擊,直到十字軍之巔頂端一帶的土地被聖滌。最後回到寒冰皇冠的回聲山谷,找到賈斯塔夫神父。','北伐軍之峰的戰鬥','到寒冰皇冠的回聲山谷找賈斯塔夫神父。','','','','',0), +(13148,'zhTW','修理項鍊','這條損壞的項鍊附著一張發票,上面寫著「提摩西‧瓊斯財產 - 卡地亞珠寶公司 - 魔導師貿易區 - 達拉然」。$B$B也許你該修復它並物歸原主。','以一枚玉髓修復這條項鍊,並送至提摩西‧瓊斯的珠寶工藝店,店在達拉然的魔導師貿易區。','','到達拉然的卡地亞珠寶公司找提摩西·瓊斯。','','','','',0), +(13157,'zhTW','十字軍之巔','很榮幸地知道,你協助銀白十字軍,在寒冰皇冠城牆內站穩腳跟,$n。十字軍之巔,現在與巫妖王及他跳梁小丑般的爪牙正面對峙。$B$B大領主弗丁和存活下來的十字軍,都等著你造訪十字軍之巔。再次動身,穿過止境去找到大領主吧!','在寒冰皇冠的十字軍之巔上,找到大領主提里奧‧弗丁。','','','','','','',0), +(13203,'zhTW','冬幕節禮物','','','','','','','','',0), +(13221,'zhTW','我還沒死!','聖光對我微笑。我還沒死於這活生生的夢魘中,但若我不能逃脫,天譴軍團將殺了我,並肢解我的軀體以作為他們『工作』的原料。$B$B我無法獨力做到這件事,我的孩子,唯有藉助你的幫忙,我才能活下來再次對抗天譴軍團。$B$B在天譴軍團的劊子手了結我以前,你能幫助我幫逃出去嗎?','護送卡瑪洛斯神父至安全之處,並向破天者號上的虔誠的亞柏薩倫回報。','護送卡瑪洛斯神父逃離險境','到寒冰皇冠的破天者號找虔誠的亞柏薩倫。','','','','',0), +(13224,'zhTW','奧格林之錘','銀白十字軍將我飽經蹂躪的身軀,從破碎前線上拉了回來。多麼榮耀的一場戰役,$r。當它砍下聯盟齷齪小賊和天譴軍團的頭顱時,我的斧頭,它高歌著。$B$B我的時代結束了。對我而言,這將是善終。幫我帶封口信,大兵。$B$B我們的基地遠在天譴軍所及之外-默德雷薩和奧多薩之間的上空。它被稱為『奧格林之錘』。$B$B<賀克徒勞無功地試圖敬禮。>$B$B飛高些,小心避開聯盟的毀滅武器,破天者號。告訴寇姆...告訴他我不行了...奮鬥...為了部落。','找出搭乘著奧格林之錘,在寒冰皇冠上空盤旋的空奪者寇姆‧黑疤。','','','','','','',0), +(13226,'zhTW','審判日將臨!','我們無法獨力擊敗巫妖王。分裂的我們唯有敗亡一途!$B$B我懇求你回應我們的呼喚,$g兄弟:姐妹;。銀白十字軍接受所有願意為戰爭奉獻心力的志士。大領主提里奧‧弗丁在銀白先鋒駐地親自等著你!坐上你的坐騎,飛往寒冰皇冠的東南邊境。你可以在那裡找到大領主弗丁以及銀白十字軍的士兵。$B$B去吧,$c,為了審判日將臨!','在寒冰皇冠的銀白先鋒駐地,向大領主提里奧‧弗丁報到。','','','','','','',0), +(13227,'zhTW','審判日將臨!','我們無法獨力擊敗巫妖王。分裂的我們唯有敗亡一途!$B$B我懇求你回應我們的呼喚,$g兄弟:姐妹;。銀白十字軍接受所有願意為戰爭奉獻心力的志士。大領主提里奧‧弗丁在銀白先鋒駐地親自等著你!坐上你的坐騎,飛往寒冰皇冠的東南邊境。你可以在那裡找到大領主弗丁以及銀白十字軍的士兵。$B$B去吧,$c,為了審判日將臨!','到寒冰皇冠的銀白先鋒駐地向大領主提里奧‧弗丁報到。','','','','','','',0), +(13240,'zhTW','泰彌亞在你的未來預見了離心傀儡!','汝應前往漂浮於凜懼島上的奧核之眼,$n。$B$B據余所見,汝應當於該地毀去離心傀儡。余斷言,若汝不能當機立斷地摧毀這些怪力亂神之物,則汝輩必致敗亡。$B$B吾既視之,汝遂應行之。','達拉然的大法師泰彌亞預見你必須摧毀10個離心傀儡。','','到達拉然找大法師泰彌亞。','','','','',0), +(13244,'zhTW','提邁爾的預言:泰坦神鐵先鋒!','整個艾澤拉斯都處在危險之中。瘋狂的神靈洛肯已經具備了毀滅世界的能力!$B$B在閃電大廳的大地瞭望塔里,那些泰坦神鐵先鋒正在有條不紊地執行著洛肯的瘋狂計畫,無數生靈即將成為他們的犧牲品。$B$B我已經預見到了,$N。你就是阻止這個瘋狂計畫的人!','根據達拉然的大法師提邁爾的預言,你必須去殺死7個泰坦神鐵先鋒。','','到達拉然找大法師提邁爾。','','','','',0), +(13246,'zhTW','死亡證明:克莉斯塔薩','你聽說克莉斯塔薩的遭遇了嗎?$B$B她幫助我們消滅了瑪裡苟斯的配偶,莎拉苟薩。為了報復,瑪裡苟斯抓住了她,讓她成為自己的配偶,還將她凍在一塊冰裡。$B$B我們最近發現,瑪裡苟斯正在用魔法消磨她的意志,她已經快被逼瘋了。$B$B$N,你必須趁她還沒有完全轉變,趕快轉至考達拉的魔樞,去結束她的生命。$B$B雖然我感到非常難過,但你必須把她的心臟帶回來作為憑證。','達拉然的大法師朗達拉克要你將克莉斯塔薩的破碎之心交給他。$B$B該任務必須在英雄難度下完成。','','到達拉然找大法師朗達拉克。','','','','',0), +(13247,'zhTW','死亡證明:魔網守護者埃雷苟斯','魔樞之戰愈演愈烈,我們必須想辦法扭轉局勢!你願意飛往戰鬥最激烈的地區嗎,$N?$B$B魔網守護者埃雷苟斯守護著一條管道,瑪裡苟斯正是通過這條管道源源不斷地汲取魔網中的能量。如果我們能殺了魔網守護者,就可以對敵人造成重創。$B$B請把他的魔網調諧器帶回來給我,讓我們好好研究一下。','達拉然的大法師朗達拉克要你將魔網調諧器交給他。$B$B該任務必須在英雄難度下完成。','','到達拉然找大法師朗達拉克。','','','','',0), +(13250,'zhTW','死亡證明:蓋爾達拉','祖爾德拉克的德拉克瑞預言者,做了件駭人聽聞的事:他們殺死了他們大多數的神衹,並吸收了祂們的力量!其中最危險的,就是阿卡利高階預言者,蓋爾達拉。$B$B隱身於剛德拉克深處,蓋爾達拉幾乎吸收了阿卡利所有的魔精,現在,他和其追隨者的勢力,已自該地冒出頭來。如果我們現在不儘快清除掉他,我們將會被瘋狂與駭人力量的浪潮沖走!$B$B快點,$n,將阿卡利剩下的魔精帶回來給我。','達拉然的大法師朗達拉克要求你,帶回阿卡利的魔精殘餘。$B$B這個任務只能在英雄難度中完成。','','到達拉然找大法師朗達拉克。','','','','',0), +(13251,'zhTW','死亡證明:瑪爾加尼斯','$N,一位青銅龍軍團的朋友向我傳達了一個很有價值的情報。$B$B被燃燒軍團稱為恐懼魔王的那種神秘生物,也就是魔族納斯雷茲姆,關於他們生存的那個世界呢,人們知之甚少,而且可以說幾乎都是無憑無據的傳言。$B$B但瑪爾加尼斯曾犯過一個致命的錯誤:他留下了一件來自他們世界的聖物!$B$B你要進入時光之穴,返回阿爾薩斯王子淨化斯坦索姆的那個時刻,拿到那件聖物。','達拉然的大法師朗達拉克要你將納斯雷茲姆家園的聖物交給他。$B$B該任務必須在英雄難度下完成。','','到達拉然找大法師朗達拉克。','','','','',0), +(13252,'zhTW','死亡證明:『塑鐵者』斯雍尼爾','我們對血肉詛咒所知甚少。然而,我們可以在石之大廳裡弄到些情報。$B$B『塑鐵者』斯雍尼爾似乎了解不少有關詛咒的事。據可靠的消息來源,他有一只載有許多資訊的泰坦圓盤,可以解答我們所有的疑問。$B$B取回這只圓盤給我,$n,好讓我們解開謎團。','達拉然的大法師朗達拉克要求你,帶回血肉詛咒圓盤。$B$B這個任務只能在英雄難度中完成。','','到達拉然找大法師朗達拉克。','','','','',0), +(13253,'zhTW','死亡證明:洛肯','洛肯屹立於雷光大廳之中,策劃著終結我們世界的計畫。需要我複述一次嗎,$n?$B$B他安坐在那裡,聆聽著禁錮於北裂境之底古神的瘋狂囈語。你和你的朋友必須立刻前往風暴群山!$B$B在地疆瞭望塔中挺身面對洛肯,在事情還來得及挽回之前!把他的紅寶石戒指帶回來,好讓我知道我們再次安全了。','達拉然的大法師朗達拉克要求你,帶回天界紅寶石戒指。$B$B這個任務只能在英雄難度中完成。','','到達拉然找大法師朗達拉克。','','','','',0), +(13255,'zhTW','死亡證明:信使沃菈齊','安卡罕特的奈幽蟲族,在試圖逃離天譴軍團的侵略時掘得太深了。他們與北裂境底下的古神近在咫尺。$B$B引起了他的警覺,他派出他的瘋狂生物以對付牠們,而奈幽蟲族便陷入了兩難的境地。$B$B上古諸神軍隊的首領,信使沃菈齊,是名為無面者的可怕族類其中一員。做掉他,並帶回他萎縮的腦部。$B$B不過拜託,保持你的理智。','達拉然的大法師朗達拉克要求你,帶回無面者的萎縮腦部。$B$B這個任務只能在英雄難度中完成。','','到達拉然找大法師朗達拉克。','','','','',0), +(13256,'zhTW','死亡證明:塞安妮苟薩','我們遭到了入侵!藍龍軍團已經傳送進了紫羅蘭監獄,正計畫從那裡沖進達拉然!$B$B有報告說,他們已經不小心釋放了一些囚犯。不過就算他們是故意放出那些囚犯,我也不會感到意外的。$B$B$N,你能否迅速召集一支隊伍趕到那裡去?塞安妮苟薩是這些入侵者的首領,把她的徽記給我帶回來!','達拉然的大法師朗達拉克要你將塞安妮苟薩的徽記交給他。$B$B該任務必須在英雄難度下完成。','','到達拉然找大法師朗達拉克。','','','','',0), +(13257,'zhTW','戰爭的使者','酋長需要你,勇士。奧格瑞瑪進入了全城戒嚴狀態,一場不可避免的衝突迫在眉睫。酋長已經下令關閉所有的商店和服務設施,全面備戰。$B$B我不能透露更多的消息了——至少現在不能。在這樣的場合跟你談論局勢也不合適。$B$B酋長傳下令來,部落最強大的勇士必須立即趕往王座廳向他報到。去吧,$N!穿過傳送門返回奧格瑞瑪,向薩爾報到。','轉至奧格瑞瑪的格羅瑪什堡壘,向薩爾報到。','','','','','','',0), +(13265,'zhTW','布料搜掠','','','','','','','','',0), +(13268,'zhTW','布料搜掠','','','','','','','','',0), +(13269,'zhTW','布料搜掠','','','','','','','','',0), +(13270,'zhTW','布料搜掠','','','','','','','','',0), +(13272,'zhTW','布料搜掠','','','','','','','','',0), +(13311,'zhTW','群魔套卡','你現在收集齊了整套的群魔套卡,另一張卡片出現在套卡的上面。上頭描繪著一個占卜師,似乎無論你用什麼角度拿著套卡,他都一直盯著你看。$b$b你覺得這張卡片似乎可以用來跟什麼人溝通。','使用群魔套卡召喚一位暗月占卜師,然後把套卡交給他。','','找暗月占卜師。','','','','',0), +(13350,'zhTW','別讓邪惡伺機喘息','是雅路麥斯...我能夠感應到,他的力量又增強了。顯然他的爪牙又重新開始對他灌注能量。如果他得到足夠的休養,很快地他就會變成一個可怕的威脅。$b$b回到他在奧多薩北部的處所。蒐集雅路麥斯的心臟、顱骨、權杖與長袍然後在雅路麥斯的殘骸結合。$b$b快點,$r! 在他尚未變得太過強大之前摧毀他!','破天者號上的薩沙理安要你在奧多薩擊敗飛升的雅路麥斯。','','到寒冰皇冠的破天者號找薩沙理安。','殺死飛昇的雅路麥斯','','','',0), +(13479,'zhTW','彩蛋大狩獵','貴族花園,生命與春天的慶典!希望你們喜歡彩蛋狩獵,朋友,說不定我用得上像你這樣卓越人士的協助。$b$b我不知道是什麼樣的魔法製造出這些彩蛋,德魯伊也不願意向我解釋。說不定你能在收集彩蛋的時候也替我找一些蛋殼碎片。你可以保留彩蛋裡面的東西,我只是想要一些蛋殼碎片的樣本。','春日蒐集者要你帶給他20個蛋殼碎片。','','找春日蒐集者。','','','','',0), +(13480,'zhTW','彩蛋大狩獵','貴族花園,生命與春天的慶典!希望你們喜歡彩蛋狩獵,朋友,說不定我用得上像你這樣卓越人士的協助。$b$b我不知道是什麼樣的魔法製造出這些彩蛋,德魯伊也不願意向我解釋。說不定你能在收集彩蛋的時候也替我找一些蛋殼碎片。你可以保留彩蛋裡面的東西,我只是想要一些蛋殼碎片的樣本。','春日收集者要你帶給他20個蛋殼碎片。','','找春日收集者。','','','','',0), +(13483,'zhTW','春日蒐集者','如果你想要找點事情做,我聽說有些法師在主城附近的營地 - 剃刀嶺、布瑞爾、血蹄村和獵鷹之翼廣場,正在徵召彩蛋獵人協助進行一些研究。我是不清楚詳情啦,不過我很肯定他們一定會告訴你。','跟剃刀嶺、布瑞爾、血蹄村或獵鷹之翼廣場的一位春日蒐集者交談。','','','','','','',0), +(13484,'zhTW','春日收集者','如果你想要找點事情做,我聽說有些法師在主城附近的營地 - 閃金鎮、卡拉諾斯、多蘭納爾和藍色守望,正在徵召彩蛋獵人協助進行一些研究。我是不清楚詳情啦,不過我很肯定他們一定會告訴你。','跟閃金鎮、卡拉諾斯、多蘭納爾或藍色守望的一位春日收集者交談。','','','','','','',0), +(13502,'zhTW','滴搭滴,貴族花園籃','來...拿著這個蛋籃去尋找明亮的彩蛋。等你找到十個貴族花園巧克力之後再帶著籃子回來找我。','貴族花園商販要你收集10個貴族花園巧克力,並帶著借來的蛋籃回去找他。','','找貴族花園商販。','','','','',0), +(13503,'zhTW','滴搭滴,貴族花園籃','來...拿著這個蛋籃去尋找明亮的彩蛋。等你找到十個貴族花園巧克力之後再帶著籃子回來找我。','貴族花園商人要你收集10個貴族花園巧克力,並帶著借來的蛋籃回去找他。','','找貴族花園商人。','','','','',0), +(13685,'zhTW','鐵爐堡驍士','你已經證明你有資格在鐵爐堡的旌旗下參與競賽,$n。現在,你應該去向菈娜‧頑錘自我介紹,她率領著鐵爐堡代表隊,同時也是鐵爐堡大勇士。她會讓你在主城的名義下參加聯賽,並檢視你作為一名驍士的訓練與測驗。$B$B很榮幸和你一同合作,$n,祝你在比賽時一切順利。','跟銀白聯賽場地的菈娜‧頑錘談談,以成為一名鐵爐堡驍士。','','','','','','',0), +(13688,'zhTW','諾姆瑞根驍士','你已經證明你有資格在諾姆瑞根的旌旗下參與競賽,$n。現在,你應該去向安布羅斯‧拴炫自我介紹,他率領著諾姆瑞根代表隊,同時也是諾姆瑞根大勇士。他會讓你在主城的名義下參加聯賽,並檢視你作為一名驍士的訓練與測驗。$B$B很榮幸和你一同合作,$n,祝你在比賽時一切順利。','跟銀白聯賽場地的安布羅斯‧拴炫談談,以成為一名諾姆瑞根驍士。','','','','','','',0), +(13689,'zhTW','達納蘇斯驍士','你已經證明你有資格在達納蘇斯的旌旗下參與競賽,$n。現在,你應該去向潔琳‧晚歌自我介紹,她率領著達納蘇斯代表隊,同時也是達納蘇斯大勇士。她會讓你在主城的名義下參加聯賽,並檢視你作為一名驍士的訓練與測驗。$B$B很榮幸和你一同合作,$n,祝你在比賽時一切順利。','跟銀白聯賽場地的潔琳‧晚歌談談,以成為一名達納蘇斯驍士。','','','','','','',0), +(13690,'zhTW','艾克索達驍士','你已經證明你有資格在艾克索達的旌旗下參與競賽,$n。現在,你應該去向克羅索斯自我介紹,他率領著艾克索達代表隊,同時也是艾克索達大勇士。他會讓你在主城的名義下參加聯賽,並檢視你作為一名驍士的訓練與測驗。$B$B很榮幸和你一同合作,$n,祝你在比賽時一切順利。','跟銀白聯賽場地的克羅索斯談談,以成為一名艾克索達驍士。','','','','','','',0), +(13691,'zhTW','奧格瑪驍士','你已經證明你有資格在奧格瑪的旌旗下參與競賽,$n。現在,你應該去向『碎顱者』莫克拉自我介紹,他率領著奧格瑪參賽者,同時也是奧格瑪大勇士。他會讓你在主城的名義下參加聯賽,並檢視你作為一名驍士的訓練與測驗。$B$B很榮幸和你一同合作,$n,祝你在比賽時一切順利。','跟銀白聯賽場地的『碎顱者』莫克拉談談,以成為一名奧格瑪驍士。','','','','','','',0), +(13692,'zhTW','劍與海','我的劍掉進海裡了。幫我找回來!','水晶之歌森林的老人巴洛要你幫他找回騎士位階之劍。','','到泰洛卡森林找老人巴洛。','','','','',0), +(13693,'zhTW','森金驍士','你已經證明你有資格在森金的旌旗下參與競賽,$n。現在,你應該去向祖爾拓自我介紹,他率領著森金的戰士,同時也是食人妖大勇士。他會讓你在主城的名義下參加聯賽,並檢視你作為一名驍士的訓練與測驗。$B$B很榮幸和你一同合作,$n,祝你在比賽時一切順利。','跟銀白聯賽場地的祖爾拓談談,以成為一名森金驍士。','','','','','','',0), +(13694,'zhTW','雷霆崖驍士','你已經證明你有資格在雷霆崖的旌旗下參與競賽,$n。現在,你應該去向魯諾克‧蠻鬃自我介紹,他率領著雷霆崖參賽者,同時也是雷霆崖大勇士。他會讓你在主城的名義下參加聯賽,並檢視你作為一名驍士的訓練與測驗。$B$B很榮幸和你一同合作,$n,祝你在比賽時一切順利。','跟銀白聯賽場地的魯諾克‧蠻鬃談談,以成為一名雷霆崖驍士。','','','','','','',0), +(13695,'zhTW','幽暗城驍士','你已經證明你有資格在幽暗城的旌旗下參與競賽,$n。現在,你應該去向亡靈哨兵威瑟瑞自我介紹,他率領著幽暗城代表隊,同時也是幽暗城大勇士。他會讓你在主城的名義下參加聯賽,並檢視你作為一名驍士的訓練與測驗。$B$B很榮幸和你一同合作,$n,祝你在比賽時一切順利。','跟銀白聯賽場地的亡靈哨兵威瑟瑞談談,以成為一名幽暗城驍士。','','','','','','',0), +(12420,'zhTW','每月啤酒俱樂部','會員享有優惠。成為「每月啤酒俱樂部」的會員,表示你有管道獲得到最新、最青的啤酒。$B$B帶著你的「每月啤酒」俱樂部會員表格給拉金‧雷酒,他就在鐵爐堡的石火旅店中。','把「每月啤酒」俱樂部會員表格帶給拉金‧雷酒,他就在鐵爐堡的石火旅店中。','','到鐵爐堡的石火旅店找拉金‧雷酒。','','','','',0), +(12421,'zhTW','每月啤酒俱樂部','$n,團結就是力量。成為「每月啤酒」俱樂部的一員,意味著你和你的同好隨時享用最青的啤酒。$b$b帶著你的「每月啤酒」俱樂部會員表格交給雷瑪,他就在奧格瑪裡。','帶著「每月啤酒」俱樂部會員表格交給奧格瑪裡的雷瑪。','','到奧格瑪找雷瑪。','','','','',0), +(12491,'zhTW','恐酒的恐酒','寇仁‧恐酒的屍體緊抓住這個小小的酒桶。這一定是他最惡名昭彰的恐酒!','將恐酒的恐酒帶到鐵爐堡附近的啤酒節營地交給易菲克佛‧鐵桶。','','到丹莫洛找易菲克佛·鐵桶。','','','','',0), +(12492,'zhTW','恐酒的恐酒','寇仁‧恐酒的屍體緊抓住這個小小的酒桶。這一定是他最惡名昭彰的恐酒!','將恐酒的恐酒帶到奧格瑪附近的啤酒節營地交給泰伯‧詐桶。','','到杜洛塔找泰伯·詐桶。','','','','',0), +(12564,'zhTW','食人妖巡邏:痛苦特效藥','我的手下雖然很勇敢,但還是凡人。$b$b創傷和疼痛會削弱他們在任務中的表現。不管做多少訓練都沒有辦法改變生命的本質。$b$b幸虧,我們有位鍊金師開發了一種軟膏,塗在繃帶上使用能夠大幅減輕士兵所承受的痛楚。$b$b這種軟膏需要用到食人妖種在西邊的一種特殊罌粟。前往德拉克索璀原野採收一些罌粟,避免我們的儲量用盡。','德拉克索璀的布蘭登上尉要你從德拉克索璀原野收集5個成熟的水罌粟。','','到祖爾德拉克的德拉克索璀找布蘭頓上尉。','','','','',0), +(12568,'zhTW','食人妖巡邏:徹底死亡','相信我,$r。我知道...$b$b燒焦血肉的氣味即使對我這樣一個被遺忘者來說都令人作嘔。然而,確保我們的同胞不會加入天譴軍團是一項嚴肅的責任。$b$b把這個油澆在我們的戰死者身上,確保我們永遠不會在戰場上以敵人的身分相見。','魯伯特上尉要你在德拉克索璀對5個戰敗的銀白步卒使用焚化之油。','','到祖爾德拉克的德拉克索璀找魯伯特上尉。','焚燒銀白步卒屍體','','','',0), +(12585,'zhTW','食人妖巡邏:物質享受','好吧,這不是什麼華麗的任務,但還是得做。$b$b我的部隊在外邊拋頭顱灑熱血,還得成天泡在及膝的髒水裡。這真的對身體很不好。$b$b他們很需要一個溫暖的營火讓他們烘乾身體,然後放鬆心靈。$b$b城市這個區域全沉在水裡,但是這些棘木的枯枝很能抵擋濕氣。$b$b替我們弄來一些生火的燃料,我們會萬分感激的。','德拉克亞苟的格隆戴爾上尉要你拿給他二十塊枯死的棘木。','','到祖爾德拉克的德拉克亞苟找格隆戴爾上尉。','','','','',0), +(12588,'zhTW','食人妖巡邏:你能挖出來嗎?','我們其中一支小隊在最後一波攻擊中碰巧發現了一張老舊的藏寶圖。如果上面寫的是真的,那麼這附近就藏有無價的食人妖神器。$b$b我的士兵都很忙,被天譴軍團和德拉克瑞兩方面壓迫。我要你去跑這條情報。$b$b帶著這個鏟子,然後在西南邊的鬆土堆使用它。當你認真的搜尋完之後再回來找我。','德拉克索璀的布蘭登上尉要你在附近的上古土堆使用鋼鏟來回收上古德拉克瑞聖物。','','到祖爾德拉克的德拉克索璀找布蘭頓上尉。','調查上古土堆','','','',0), +(12591,'zhTW','食人妖巡邏:投彈','聰明...$b$b當我們看起來好像控制住局勢的時候,他們把隧道挖過我們正下方,並且佔領了聖壇!$b$b現在我們被夾在兩支奈幽蟲族的部隊之間,我們的處境每分每秒都在惡化。$b$b首先,我們一定要阻止他們繼續增援。一定要把他們的隧道封閉起來。$b$b帶著這些手榴彈,把它們丟進西北方的蟲坑之中。','德拉克索璀的魯伯特上尉要你對5個奈幽蟲坑使用高衝擊手榴彈。','','到祖爾德拉克的德拉克索璀找魯伯特上尉。','奈幽隧道坍塌','','','',0), +(12594,'zhTW','食人妖巡邏:無法坐視','這麼多個哨站,他們卻把我丟來這裡。$b$b這個鬼地方快被那些惡臭停滯的死水給淹沒了,而現在還聚集了一堆污水元素!$b$b我並不知道這些傢伙是什麼,也不知道他們為什麼會在這裡,但現實就是我無法坐視不理。我只知道他們把我的轄區搞得一團亂!我怎麼可能在這種滿是爛泥髒污的地方帶領一支有尊嚴高貴的軍隊?$b$b快到外邊去,$c,在你沒有把我的戰場弄乾淨之前,不要回來!','格隆戴爾上尉要你在德拉克亞苟殺死7隻青苔狂暴者。','','到祖爾德拉克的德拉克亞苟找格隆戴爾上尉。','殺死青苔狂暴者','','','',0), +(12600,'zhTW','兌換商品–熊坐騎','','','','','','','','',0), +(12601,'zhTW','煉金師的幫手','哈,你一定就是他們提過的那個巡邏兵吧。你來得真是太及時了,我正急需幫手呢。$b$b我們抓到了一個達卡萊俘虜,正準備審問他。不過在審訊開始之前,我需要你幫忙製造一種真言藥水。$b$b製造真言藥水是非常複雜的活,需要保持敏銳的直覺、完美地把握時機,並在每一個製作環節上都有即興發揮的能力。$b$b你準備好以後就告訴我吧,$r。記住,對時機的掌握至關重要。','赫布瓦羅的煉金師菲肯斯坦要你幫他製作真言藥水。$b$b準備好以後與他交談,並按照他的指示行動。','','到祖爾德拉克赫布瓦羅找煉金師菲肯斯坦。','完成自白劑','','','',0), +(12602,'zhTW','煉金師的幫手','哈,你一定就是他們提過的那個巡邏兵吧。你來得真是太及時了,我正急需幫手呢。$b$b我們抓到了一個達卡萊俘虜,正準備審問他。不過在審訊開始之前,我需要你幫忙製造一種真言藥水。$b$b製造真言藥水是非常複雜的活,需要保持敏銳的直覺、完美地把握時機,並在每一個製作環節上都有即興發揮的能力。$b$b你準備好以後就告訴我吧,$r。記住,對時機的掌握至關重要。','赫布瓦羅的煉金師菲肯斯坦要你幫他製作真言藥水。$b$b準備好以後與他交談,並按照他的指示行動。','','到祖爾德拉克赫布瓦羅找煉金師菲肯斯坦。','完成自白劑','','','',0), +(12604,'zhTW','恭喜!','','','','','','','','',0), +(12616,'zhTW','滿載秘密的房間','太棒了,是個經驗老到的冒險者。這正是我們需要的人選。$B$B祈倫托那些負責防衛麥迪文之塔的守衛說卡拉贊似乎不大對勁。塔裡似乎潛伏著新的力量,既不屬於守護者,也不屬於燃燒軍團。他們偵察的結果顯示,那似乎是一個叫做坦瑞斯‧暗血的精靈王子,推測他服侍的是天譴軍團。$b$b去吧,我的朋友,調查暗血的房間,把你找到的情報都帶來給我。','銀色黎明大使要你到卡拉贊的佣人區去,搜索坦瑞斯·暗血的房間。','瞭解坦瑞斯目的。','找銀色黎明特使。','','','','',0), +(12626,'zhTW','詛咒連','<科弗斯抬手敬了一個禮。>$B$B我們已經準備了成堆的武器、盔甲和彈藥,幾乎要把倉庫堆滿了。戰爭準備已經就緒!$B$B<科弗斯將一份單據小心地卷了起來。>$B$B軍官辦公區在守備層的另一側,也就是這裡的西南方向。你去把這份裝備報告書交給天災指揮官薩拉諾爾,他是詛咒連的指揮官。','將科弗斯的報告交給黑鋒要塞守備層的天災指揮官薩拉諾爾。','','到東瘟疫之地:血色領地找天災指揮官薩拉諾爾。','','','','',0), +(12693,'zhTW','狼獾人陣營','','','','','','','','',0), +(12694,'zhTW','神諭者陣營','','','','','','','','',0), +(12745,'zhTW','特別驚喜','我們來到這裡,夷平了整個地方,然後前往牢房。我們倒是沒想到會看到那種景象,$n。看來十字軍真的很忙。整間牢房都塞滿了銀色黎明的囚犯。大多數在我們抵達的時候都已經死了,但還有一些人還有呼吸。$B$B我本來要進去把他們通通處決掉,但我認為這個榮耀應該歸於你手中。尤其是有個很吵鬧的地精,我想你一定會很樂於親手行刑的。','赤紅之焰禮拜堂的騎士指揮官瘟疫之拳命令你處決附近牢房裡關押的銀色黎明囚犯,高比‧布雷斯頓海默。','','到東瘟疫之地:血色領地找騎士指揮官瘟疫之拳。','','','','',0), +(12749,'zhTW','特別驚喜','我們來到這裡,夷平了整個地方,然後前往牢房。我們倒是沒想到會看到那種景象,$n。看來十字軍真的很忙。整間牢房都塞滿了銀色黎明的囚犯。大多數在我們抵達的時候都已經死了,但還有一些人還有呼吸。$B$B我本來要進去把他們通通處決掉,但我認為這個榮耀應該歸於你手中。尤其是有個很吵鬧的食人妖,我想你一定會很樂於親手行刑的。','赤紅之焰禮拜堂的騎士指揮官瘟疫之拳命令你處決附近牢房裡關押的銀色黎明囚犯,伊吉‧暗牙。','','到東瘟疫之地:血色領地找騎士指揮官瘟疫之拳。','','','','',0), +(12752,'zhTW','孤注一擲的研究','$n,巫妖王的侵略肆虐著我們。如果要把他趕回他的冰封王座,部落與聯盟必須同舟共濟。在撒塔斯的聖光露臺成立了一個新同盟。$B$B為了要援助和巫妖王的艱苦奮戰,前往聖光露臺的新藥劑師營地,和大藥劑師普崔司談談。他進行危急調查想辦法結束這場殭屍瘟疫。他的解藥是我們的救星。$B$B動作快,$n,每秒鐘都有一條無辜的生命喪失。','與大藥劑師普崔司交談,他就在撒塔斯聖光露臺的新藥劑師營地中。','','','','','','',0), +(12753,'zhTW','孤注一擲的聯盟','善良的$n,巫妖王以侵略和疾病毒害我們 -- 部落與聯盟都難逃一劫 。唯有結合我們兩大艾澤拉斯文明的力量才能對抗他的攻擊。$B$B在撒塔斯的聖光露臺,我們結成了新的聯盟。為了協助我們對抗巫妖王,前往聖光露臺的新聯盟駐紮地和主教拉札利爾談談。$B$B動作快,$n。','與撒塔斯的主教拉札利爾談談。他就在聖光露臺的新聯盟駐紮地中。','','','','','','',0), +(12764,'zhTW','康加露酒的秘密','','','','','','','','',0), +(12765,'zhTW','康加露酒','','','','','','','','',0), +(12766,'zhTW','與你的大使談談','<精靈形態的龍拿走你的介紹信,瀏覽一番後收進他的袖套內。>$B$B所有事務看起來都很規則。不過,如果你不在意的話,我想要你去與你在神殿這裡的大使談談。$B$B在我們派你去見女王前,她那裡有些極為重要的事需要你先去處理。$B$B<他輕拍了袖套裡的信件。>$B$B我會在你回來前替你保存這封信件的。','與龍眠神殿的洛瑞爾‧真刃談談。','','','','','','',0), +(12767,'zhTW','與你的大使談談','<精靈形態的龍拿走你的介紹信,瀏覽一番後收進他的袖套內。>$B$B所有事務看起來都很規則。不過,如果你不在意的話,我想要你去與你在神殿這裡的大使談談。$B$B在我們派你去見女王前,她那裡有些極為重要的事需要你先去處理。$B$B<他輕拍了袖套裡的信件。>$B$B我會在你回來前替你保存這封信件的。','與龍眠神殿的葛拉克‧岩拳談談。','','','','','','',0), +(12768,'zhTW','龍眠神殿管理者','你已經證明你值得信賴,甚至比信賴更進一步。$B$B你應該回到龍眠神殿並且和管理者,泰瑞歐斯塔茲再談一談,$n。','和龍眠神殿的泰瑞歐斯塔茲交談。','','','','','','',0), +(12771,'zhTW','鐵爐堡','聖光響應了我們的祈禱!依靠沙塔斯城的納魯所賜予的聖光精華,以及虔誠信徒的祈福,我們終於創造出了神聖而強大的聖物,這些聖物將成為我們對抗巫妖王的利器。$B$B帶上這個,$n。你要仔細保護它,把它安全地送到鐵爐堡。麥格尼·銅須急於轉至諾森德參戰,這件聖物可以説明他達成目標。','拉莎莉爾主教要你將光明聖物交給鐵爐堡的國王麥格尼·銅須。','','到鐵爐堡找國王麥格尼·銅須。','','','','',0), +(12772,'zhTW','孤注一擲的聯盟','善良的$n,巫妖王以侵略和疾病毒害我們 -- 部落與聯盟都難逃一劫 。唯有結合我們兩大艾澤拉斯文明的力量才能對抗他的攻擊。$B$B在撒塔斯的聖光露臺,我們結成了新的聯盟。為了協助我們對抗巫妖王,前往聖光露臺的新聯盟駐紮地和主教拉札利爾談談。$B$B動作快,$n。','與撒塔斯的主教拉札利爾談談。他就在聖光露臺的新聯盟駐紮地中。','','','','','','',0), +(12773,'zhTW','達納蘇斯','我們的祈禱得到了回應!我們利用來自撒塔斯那魯的聚合聖光,以及虔誠信徒所獻上的祈禱,創造了擁有深奧力量的神聖法器。這些法器將會成為我們對抗巫妖王陰謀的武器。$B$B帶著這個,$n。看緊它,將之確實送抵達納蘇斯。把它送去給泰蘭妲‧語風 -- 她的牧師們將會發現這是她們與來自北方的邪惡勢力交戰的關鍵力量來源。','主教拉札利爾要你把聖光灌注神器送去給達納蘇斯的泰蘭妲‧語風。','','到達納蘇斯找神殿花園的泰蘭妲‧語風。','','','','',0), +(12774,'zhTW','暴風城','我們的祈禱得到了回應!我們利用來自撒塔斯那魯的聚合聖光,以及虔誠信徒所獻上的祈禱,創造了擁有深奧力量的神聖法器。這些法器將會成為我們對抗巫妖王陰謀的武器。$B$B帶著這個,$n。看緊它,並安全地將它送達暴風城。暴風城之王已經回來了。我相信他會很樂於見到這個。','主教拉札利爾要你把聖光灌注神器送去給暴風城的瓦裡安‧烏瑞恩國王。','','到暴風城的暴風要塞找瓦裡安·烏瑞恩國王。','','','','',0), +(12775,'zhTW','孤注一擲的聯盟','善良的$n,巫妖王以侵略和疾病毒害我們 -- 部落與聯盟都難逃一劫 。唯有結合我們兩大艾澤拉斯文明的力量才能對抗他的攻擊。$B$B在撒塔斯的聖光露臺,我們結成了新的聯盟。為了協助我們對抗巫妖王,前往聖光露臺的新聯盟駐紮地和主教拉札利爾談談。$B$B動作快,$n。','與撒塔斯的主教拉札利爾談談。他就在聖光露臺的新聯盟駐紮地中。','','','','','','',0), +(12776,'zhTW','埃索達','聖光響應了我們的祈禱!依靠沙塔斯城的納魯所賜予的聖光精華,以及虔誠信徒的祈福,我們終於創造出了神聖而強大的聖物,這些聖物將成為我們對抗巫妖王的利器。$B$B帶上這個,$n。你要仔細保護它,把它安全地送到埃索達,交給聖光穹頂的先知維倫。他可以借用聖物的力量對抗阿爾薩斯的邪惡部隊。','拉莎莉爾主教要你將光明聖物交給埃索達的維倫。','','到埃索達找先知維倫。','','','','',0), +(12777,'zhTW','絕望的聯軍','親愛的$n,巫妖王在用疾病和侵略折磨著我們——部落和聯盟都深受其害。只有艾澤拉斯的這兩個偉大文明聯手,才有可能擊敗他。$B$B我們在沙塔斯城的聖光廣場結成了聯軍。$B$B為了幫助我們對抗巫妖王,請你到聖光廣場的新聯盟營地去,和拉莎莉爾主教談一談。$B$B願聖光保佑你,$n。','到沙塔斯城的聖光廣場去,與新聯盟營地中的拉莎莉爾主教談一談。','','','','','','',0), +(12781,'zhTW','歡迎!','歡迎來到魔獸世界!$B$B為了感謝您購買魔獸世界典藏版,請將這張禮券交給黑鋒要塞的女妖希奧克絲。您將得到一份特別的禮物:陪伴您踏上冒險旅途的小寵物!$B$B再次感謝您的支持,請盡情享受魔獸世界的樂趣吧!','將黑鋒要塞禮品券交給黑鋒要塞的女妖希奧克絲。','','到黑鋒要塞找女妖希奧克絲。','','','','',0), +(12782,'zhTW','孤注一擲的研究','$n,巫妖王的侵略肆虐著我們。如果要把他趕回他的冰封王座,部落與聯盟必須同舟共濟。在撒塔斯的聖光露臺成立了一個新同盟。$B$B為了要援助和巫妖王的艱苦奮戰,前往聖光露臺的新藥劑師營地,和大藥劑師普崔司談談。他進行危急調查想辦法結束這場殭屍瘟疫。他的解藥是我們的救星。$B$B動作快,$n,每秒鐘都有一條無辜的生命喪失。','與大藥劑師普崔司交談,他就在撒塔斯聖光露臺的新藥劑師營地中。','','','','','','',0), +(12783,'zhTW','孤注一擲的研究','$n,巫妖王的侵略肆虐著我們。如果要把他趕回他的冰封王座,部落與聯盟必須同舟共濟。在撒塔斯的聖光露臺成立了一個新同盟。$B$B為了要援助和巫妖王的艱苦奮戰,前往聖光露臺的新藥劑師營地,和大藥劑師普崔司談談。他進行危急調查想辦法結束這場殭屍瘟疫。他的解藥是我們的救星。$B$B動作快,$n,每秒鐘都有一條無辜的生命喪失。','與大藥劑師普崔司交談,他就在撒塔斯聖光露臺的新藥劑師營地中。','','','','','','',0), +(12784,'zhTW','孤注一擲的研究','$n,巫妖王的侵略肆虐著我們。如果要把他趕回他的冰封王座,部落與聯盟必須同舟共濟。在撒塔斯的聖光露臺成立了一個新同盟。$B$B為了要援助和巫妖王的艱苦奮戰,前往聖光露臺的新藥劑師營地,和大藥劑師普崔司談談。他進行危急調查想辦法結束這場殭屍瘟疫。他的解藥是我們的救星。$B$B動作快,$n,每秒鐘都有一條無辜的生命喪失。','與大藥劑師普崔司交談,他就在撒塔斯聖光露臺的新藥劑師營地中。','','','','','','',0), +(12785,'zhTW','奧格瑪','我們有了突破性的發現!我們的殭屍樣本讓我們對天譴瘟疫的本質有了新的認識。他們結出了腐爛,甜美的果實...$B$B把這個盒子帶往奧格瑪,盒子裡面是一顆被殭屍瘟疫感染的頭顱,當然還有一些其他我們發明出的東西 -- 一種反式瘟疫。這兩種瘟疫在殭屍的腦袋中不斷對抗,而這顆頭顱正是解藥的關鍵,這我很確定。$B$B把這個盒子帶給我的助手,藥劑師卡洛孚,他在奧格瑪替大酋長索爾服務。','大藥劑師普崔司要你把這顆雙重染疫的大腦交給奧格瑪的藥劑師卡洛孚。','','到奧格瑪找藥劑師卡洛孚。','','','','',0), +(12786,'zhTW','雷霆崖','我們有了突破性的發現!我們的殭屍樣本讓我們對天譴瘟疫的本質有了新的認識。他們結出了腐爛,甜美的果實...$B$B把這個盒子帶往雷霆崖,盒子裡面是一顆被殭屍瘟疫感染的頭顱,當然還有一些其他我們發明出的東西 -- 一種反式瘟疫。這兩種瘟疫在殭屍的腦袋中不斷對抗,而這顆頭顱正是解藥的關鍵,這我很確定。$B$B把這個盒子帶給我的助手,鍊金師夏睨,他在雷霆崖上替瑪加薩‧恐怖圖騰服務。','大藥劑師普崔司要你把這顆雙重染疫的大腦交給雷霆崖的鍊金師夏睨。','','到雷霆崖找鍊金師夏睨。','','','','',0), +(12787,'zhTW','幽暗城','殭屍瘟疫實際上是個隱藏的祝福。在我們尋找解藥的研究中,我們學習到許多與天譴軍與他們的傳染病相關的知識。我們的研究賦予了我們對付他們的武器。武器,還有工具...$B$B拿著這個 -- 這是一顆殭屍的腦袋,我們從近來的發現中取得的化合物裡面,注射進這顆頭顱中,使得它有一部分被治癒了。將這顆頭顱送給藥劑大師法拉尼爾;我有一整列張表的實驗等著他拿這個頭來做。','大藥劑師普崔司要你把這顆雙重染疫的大腦交給幽暗城的藥劑大師法拉尼爾。','','到幽暗城找藥劑大師法拉尼爾','','','','',0), +(12788,'zhTW','銀月城','我們有了突破性的發現!我們的殭屍樣本讓我們對天譴瘟疫的本質有了新的認識。他們結出了腐爛,甜美的果實...$B$B把這個盒子帶往銀月城,盒子裡面是一顆被殭屍瘟疫感染的頭顱,當然還有一種我們發明出的東西 -- 一種反式瘟疫。這兩種瘟疫在殭屍的腦袋中不斷對抗,而這顆頭顱正是解藥的關鍵,這我很確定。$B$B把這個盒子帶給我的助手,藥劑師提琵希。她在銀月城替洛索瑪‧塞隆服務。','大藥劑師普崔司要你把這顆雙重染疫的大腦交給銀月城的藥劑師提琵希。','','到銀月城找藥劑師提琵希。','','','','',0), +(12798,'zhTW','眾劍套卡','你現在集齊了整套的眾劍套卡,另一張卡片出現在套卡的上面。上頭描繪著一個占卜師,似乎無論你用什麼角度拿著套卡,他都一直盯著你看。$b$b你覺得這張卡片似乎可以用來跟什麼人溝通。','使用眾劍套卡召喚一位暗月占卜師,然後把套卡交給他。','','找暗月占卜師。','','','','',0), +(12808,'zhTW','孤注一擲的聯盟','善良的$n,巫妖王以侵略和疾病毒害我們 -- 部落與聯盟都難逃一劫 。唯有結合我們兩大艾澤拉斯文明的力量才能對抗他的攻擊。$B$B在撒塔斯的聖光露臺,我們結成了新的聯盟。為了協助我們對抗巫妖王,前往聖光露臺的新聯盟駐紮地和主教拉札利爾談談。$B$B動作快,$n。','與撒塔斯的主教拉札利爾談談。他就在聖光露臺的新聯盟駐紮地中。','','','','','','',0), +(12809,'zhTW','鐵爐堡','聖光響應了我們的祈禱!依靠沙塔斯城的納魯所賜予的聖光精華,以及虔誠信徒的祈福,我們終於創造出了神聖而強大的聖物,這些聖物將成為我們對抗巫妖王的利器。$B$B帶上這個,$n。你要仔細保護它,把它安全地送到鐵爐堡。侏儒區的大工匠梅卡托克正在期待著借助聖物的力量為聯盟效力。','拉莎莉爾主教要你將光明聖物交給鐵爐堡的大工匠梅卡托克。','','到鐵爐堡找大工匠梅卡托克。','','','','',0), +(12811,'zhTW','孤注一擲的研究','$n,巫妖王的侵略肆虐著我們。如果要把他趕回他的冰封王座,部落與聯盟必須同舟共濟。在撒塔斯的聖光露臺成立了一個新同盟。$B$B為了要援助和巫妖王的艱苦奮戰,前往聖光露臺的新藥劑師營地,和大藥劑師普崔司談談。他進行危急調查想辦法結束這場殭屍瘟疫。他的解藥是我們的救星。$B$B動作快,$n,每秒鐘都有一條無辜的生命喪失。','與大藥劑師普崔司交談,他就在撒塔斯聖光露臺的新藥劑師營地中。','','','','','','',0), +(12812,'zhTW','奧格瑪','終於有重大突破了!我們的僵屍樣本在天災的影響下產生了新的特性,並結出了豐碩的果實……$B$B把這只盒子帶到奧格瑞瑪去。裡面是一枚帶有僵屍疫病的徽記,上面還有我們添加的其它東西——一種反藥劑。這兩種將在徽記上相互作用,而我敢肯定,這枚徽記就是治癒疫病的關鍵。$B$B把這只盒子交給我的助手,藥劑師卡爾洛夫。他就在奧格瑞瑪的薩爾酋長身邊。','大藥劑師普特雷斯要求你將雙天災腦質交給奧格瑞瑪的藥劑師卡爾洛夫。','','到奧格瑪找藥劑師卡爾洛夫。','','','','',0), +(12816,'zhTW','調查銀月城的天譴軍團','巫妖王把他的不死生物派到我們的門口,我們不能坐視不管!你一定會站出來保衛銀月城不受可惡的入侵者騷擾吧?$b $b外面有些奇怪的符文法陣,散發著和周圍不死生物與亡域上同樣的能量。它們有一定的重要性,我肯定,而且我要你去調查出來。削減那些該死生物的數量,把它們死亡的證據和調查結果帶來給我,我就會獎勵你。去吧!','從銀月城外的天譴軍團身上,收集三個暗淡的亡域之石,並調查它們營地附近發光的符文法陣。','調查法陣','到永歌森林找朱雷克中尉。','','','','',0), +(12817,'zhTW','調查艾克索達的天譴軍團','巫妖王把他的不死生物派到我們的門口,我們不能坐視不管!你一定會站出來保衛艾克索達不受可惡的入侵者騷擾吧?$b $b外面有些奇怪的符文法陣,散發著和周圍不死生物與亡域上同樣的能量。它們有一定的重要性,我肯定,而且我要你去調查出來。削減那些該死生物的數量,把它們死亡的證據和調查結果帶來給我,我就會獎勵你。去吧!','從艾克索達外的天譴軍團身上,收集三個暗淡的亡域之石,並調查它們營地附近發光的符文法陣。然後回去找克瑞格中尉。','調查法陣','到埃索達找克雷格爾上尉。','','','','',0), +(12872,'zhTW','諾甘農之殼','發明者圓盤資料傳輸中。$B$B圓盤資料傳輸完畢。$B$B緊急伽瑪射線措施準備中。$B$B緊急伽瑪射線措施準備完成。請以充能圓盤啟動諾甘農之殼管理員梅查頓。一旦您的身分確認完成,管理員會立刻給您諾甘農之殼。$B$B祝您擁有愉快的千年。','從管理員梅查頓那取回諾甘農之殼。','','使用布萊恩的通訊器聯絡布萊恩·銅須。$B$B如果你弄丟了聯絡器,你可以與冰霜堡的考古學家安多林重新要個新的。','','','','',0), +(12873,'zhTW','霜誕國王','當然...我確實看得出核心會長什麼樣。如果有任何霜誕矮人看過它,他們聽到這描述一定會認出來的。$B$B沒有時間可以浪費了,讓我們直接進入重點吧。去找風暴之心國王談話,請求他的協助...他一定知道有誰可以幫我們找到核心。對了,記得客氣一點 - 他在這一帶很受敬重的。','與霜堡的約格‧風暴之心交談。','','','','','','',0), +(12880,'zhTW','探索大師','要組合拱心石,就需要諾甘農之縛...那是一具位於造物者動力核心的裝置,就是我們身旁的這個大坑。布萊恩現在應該帶著另一半的拱心石待在底下了。我已經教他如何使用裝置了。他在等待著你。$B$B我會盡可能的拖延鐵矮人,但是我沒有自信能夠抵擋太久。風暴之心國王帶著他的手下回頭防禦霜堡了。你得快點,$r。','將諾甘農之核帶去造物者動力核心給布萊恩‧銅鬚。','','到風暴群山的造物者動力核心找布萊恩‧銅鬚。','','','','',0), +(12890,'zhTW','如果尺寸重要','','','','','','','','',0), +(12918,'zhTW','完美寶石','看來刺骨的寒風已經吹到這裡來了?現在不僅僅是缺少欣賞我手藝的顧客,而且我也沒有庫存的寶石可以出售了!如果你能弄些未切割的寶石來補充我的庫存,我可以教你切割完美寶石的技術。切割完美寶石能讓你在切割諾森德地區出產的優秀品質寶石時,有機會切割出一顆具有更高屬性的完美寶石。','將茶晶石、黑玉和暗影水晶各兩顆交給瓦加德的恩霍羅。','','到凜風峽灣的瓦爾加德的恩霍羅。','','','','',0), +(12952,'zhTW','完美寶石','在這個鳥不生蛋的地方,我的手藝都會被荒廢的。我花了很多年來學習怎麼把寶石切割成完美的形態,現在卻英雄無用武之地!你看上去就像是能幫我找些寶石來的那類人,如果你願意幫忙的話,我可以教你切割完美寶石的技術。切割完美寶石能讓你在切割諾森德地區出產的優秀品質寶石時,有機會切割出一顆具有更高屬性的完美寶石。','將茶晶石、黑玉和暗影水晶各兩顆交給復仇港的卡特爾·迪芬斯。','','到凜風峽灣的復仇臺地找卡特爾·迪芬斯。','','','','',0), +(12958,'zhTW','貨品:血玉護符','一間有錢的貨運公司訂購了整套的血玉護符要賣到幽坑城去。如果你能夠給我一個護符,我可以給你一個達拉然珠寶匠徽章作為回報。','將一個維酷護符、一個暗玉以及一個血石合併,以製成一個血玉護符,將其交給達拉然的提摩西‧瓊斯。$B$B你可以從北裂境的任何一個維酷人身上找到維酷護符。','','到達拉然的卡地亞珠寶公司找提摩西·瓊斯。','','','','',0), +(12959,'zhTW','貨品:發光象牙刻像','一間有錢的貨運公司訂購了整套的發光象牙刻像要賣到幽坑城去。如果你能夠替我帶來一個刻像,我可以給你一個達拉然珠寶匠徽章作為回報。','將一個北地象牙、一個玉髓以及一個暗影水晶組合成發光象牙刻像,然後交給達拉然的提摩西‧瓊斯。$B$B你可以從北裂境的任何一個鍬牙或長毛象身上找到北地象牙。','','到達拉然的卡地亞珠寶公司找提摩西·瓊斯。','','','','',0), +(12960,'zhTW','貨品:邪惡烈日胸針','一間有錢的貨運公司訂購了整套的邪惡烈日胸針要賣到幽坑城去。如果你能為我找來一個,我可以給你個達拉然珠寶匠徽章作為回報。','把一個鐵矮人胸針、一個巨黃晶以及一個烈日水晶組合成邪惡烈日胸針,交給達拉然的提摩西‧瓊斯。$B$B你可以從北裂境的任何一個鐵矮人身上找到鐵矮人胸針。','','到達拉然的卡地亞珠寶公司找提摩西·瓊斯。','','','','',0), +(12961,'zhTW','貨品:錯綜龍骨刻像','一間有錢的貨運公司訂購了整套的錯綜龍骨刻像要賣到幽坑城去。如果你能夠給我一個,我可以給你個達拉然珠寶匠徽章作為回報。','把一個元龍骨、一個烈日水晶以及一個暗玉組合成錯綜龍骨刻像,並交給達拉然的提摩西‧瓊斯。$B$B你可以從北裂境的任何一頭元龍身上取得元龍骨。','','到達拉然的卡地亞珠寶公司找提摩西·瓊斯。','','','','',0), +(12962,'zhTW','貨品:光亮護甲聖物','一間有錢的貨運公司訂購了整套的光亮護甲聖物要賣到幽坑城去。如果你能夠給我一套,我可以給你一個達拉然珠寶匠徽章作為回報。','把一個元素護甲碎塊、一個血石以及一個巨黃晶組合成光亮護甲聖物,並交給達拉然的提摩西‧瓊斯。$B$B你可以從北裂境的任何一個亡魄身上取得元素護甲碎塊。','','到達拉然的卡地亞珠寶公司找提摩西·瓊斯。','','','','',0), +(12963,'zhTW','貨品:移形烈日珍品','一間有錢的貨運公司訂購了整套的移形烈日珍品要賣到幽坑城去。如果你能夠給我一份,我可以給你一個達拉然珠寶匠徽章作為回報。','把一個天譴珍品、一個烈日水晶以及一個暗影水晶組合成移形烈日珍品,並交給達拉然的提摩西‧瓊斯。$B$B你可以從北裂境的任何一個天譴軍身上取得天譴珍品。','','到達拉然的卡地亞珠寶公司找提摩西·瓊斯。','','','','',0), +(13002,'zhTW','寶石完美工法','歡迎來到冰天雪地,$g。這地方對於身子保暖來說不太好,但是我學會怎麼切割完美的寶石啦。我只需要有幾顆寶石讓我來活動一下手腳就行。$B$B替我帶來一些寶石,我就會教你寶石完美工法。它能讓你在切割北裂境出產的優秀寶石時,切出擁有額外加值的完美寶石。','將兩顆巨黃晶、兩顆暗玉和兩顆暗影水晶交給戰歌堡的賈巴利。','','到北風凍原的戰歌堡找賈巴利。','','','','',0), +(13004,'zhTW','完美寶石','戰爭一觸即發,我們都要竭盡所能!我的長處在於切割寶石,而且不是普通的寶石,是完美的寶石!如果你能弄些未切割的寶石來給我,我也可以教你學會這種技術。切割完美寶石能讓你在切割諾森德地區出產的優秀品質寶石時,有機會切割出一顆具有更高屬性的完美寶石。','將茶晶石、黑玉和暗影水晶各兩顆交給無畏要塞的奧雷斯托斯。','','到北風凍原的驍勇要塞找奧雷斯托斯。','','','','',0), +(13008,'zhTW','天譴戰術','我們碰上了意想不到的事情!我們在山側炸出了一個洞穴,然後衝進了天譴岸地。天譴軍的反擊非常強烈,我們撐沒多久就潰敗了。我們撤退到谷地,以彈雨伺候追上來的天譴軍。這抵擋了他們一陣子,直到...那些混蛋開始用十字軍當做活盾牌。我們因為擔心殺死自己的士兵而不得不停火。$B$B解救那些困在基地外面原野上的士兵吧,$c!','銀白先鋒駐地的十字軍指揮官恩塔利要你解救5位被網住的十字軍。','','到寒冰皇冠的銀白先鋒駐地找十字軍指揮官恩塔利。','釋放被網住的十字軍','','','',0), +(13012,'zhTW','沙迪斯長者','','','','','','','','',0), +(13013,'zhTW','貝爾達克長者','','','','','','','','',0), +(13014,'zhTW','莫爾希長者','','','','','','','','',0), +(13015,'zhTW','法爾高長者','','','','','','','','',0), +(13016,'zhTW','諾爾索長者','','','','','','','','',0), +(13017,'zhTW','加坦長者','','','','','','','','',0), +(13018,'zhTW','杉德林長者','','','','','','','','',0), +(13019,'zhTW','索依姆長者','','','','','','','','',0), +(13020,'zhTW','石鬚長者','','','','','','','','',0), +(13021,'zhTW','伊加修長者','','','','','','','','',0), +(13022,'zhTW','訥金長者','','','','','','','','',0), +(13023,'zhTW','奇里亞斯長者','','','','','','','','',0), +(13024,'zhTW','瓦尼卡雅長者','','','','','','','','',0), +(13025,'zhTW','魯納羅長者','','','','','','','','',0), +(13026,'zhTW','藍狼長者','','','','','','','','',0), +(13027,'zhTW','陶羅斯長者','','','','','','','','',0), +(13028,'zhTW','灰鬃長者','','','','','','','','',0), +(13029,'zhTW','帕姆亞長者','','','','','','','','',0), +(13030,'zhTW','胡瑞恩長者','','','','','','','','',0), +(13031,'zhTW','天衛長者','','','','','','','','',0), +(13032,'zhTW','慕拉可長者','','','','','','','','',0), +(13033,'zhTW','阿爾普長者','','','','','','','','',0), +(13036,'zhTW','榮譽至上','通往救贖之道漫長無盡啊,年輕的$c。我們打遍了各大陸,甚至穿越了世界,來到了此處。現在,我們要面臨最大的挑戰:毀滅巫妖王與他的天譴軍團。$B$B哎,我們卻是各自為政地在進行這場戰鬥。部落與聯盟彼此作戰,無法援助我們。我們一定得為了自己奮戰不懈!我們又有什麼別的選擇呢?$B$B你願意加入嗎?把你的力量借給我們,$c!我的指揮官,恩塔利,正在等待著你。','向銀白先鋒駐地的十字軍指揮官恩塔利報到。','','','','','','',0), +(13039,'zhTW','防守先鋒駐地','我們無法保持這個步調,$g小夥子:小姑娘;。當缺口崩毀的時候,閘門也被打開了。天譴軍團如潮水般湧入,他們滿山遍野,在視野可及之處如同沒有邊界般散佈!$B$B我們需要任何願意挺身而出的男女,協助我們制止這波邪惡的浪潮。到外面去盡你的本分,朋友。盡你所能地殺那些怪物!','在寒冰皇冠之銀白先鋒駐地的十字軍領主達佛斯要求你殺掉15個遺忘深淵奈幽蟲族。','','到寒冰皇冠的銀白先鋒駐地找十字軍領主達佛斯。','殺死遺忘深淵奈幽蟲族','','','',0), +(11532,'zhTW','分散死亡之痕的注意力','一旦軍團南下的部隊獲得抒解,他們就會立刻對付我們。天譴軍團也持續地聚集在死亡之痕。但這維持不了多久了。$B$B我要你騎著龍鷹前往死亡之痕,用這些秘法炸藥減少那些惡魔的數量。$B$B如果一切順利的話,他們甚至不會知道我們從凱爾的僕人那邊取得這些軍火。等你準備好就和埃倫談談,那個港口西邊的龍鷹管理者。','戰鬥法師艾里娜要你準備好飛越死亡之痕後,和埃倫‧破雲者談談。一旦抵達之後,用秘法炸藥殺死2隻深淵霸主、3隻埃雷達爾巫士和12隻憤怒執行者。','','到奎爾丹納斯島的陽灣聖殿找戰鬥法師艾爾娜。','','','','',0), +(11535,'zhTW','做好準備','物資正在運來,起碼我是這麼被告知的。$b$b但無論如何,我們無法繼續等待支援會在攻勢陷入困境前到來。我們必須要盡量利用手邊的資源。$b$b就我來說,我需要礦石來製造鐵砧,好讓我可以著手升級我們的鎧甲和武器。$b$b在東邊的海岸有一群納迦,他們在收集礦石並且裝箱要運送給燃燒軍團。$b$b偷走他們的礦石會對我們有很大的幫助...。','日境軍械庫的鐵匠荷莎要你殺死東部的暗脊部屬,用他們身上的鑰匙,然後從箱子中偷取三塊礦石。','','到奎爾丹納斯島的日境軍械庫找鐵匠霍爾薩。','','','','',0), +(11538,'zhTW','日境軍械庫的戰鬥','燃燒軍團控制著日境軍械庫,它在我們的行動中具有重要的戰略價值。只要他們的士氣高昂,我們就很難奪取建築物的控制權。$B$B憎恨密使是軍團的打擊部隊派來鞏固他們的戰術據點。敵人靠著他們的抵達正在重整。比起我們,他們更懼怕這些憎恨密使。$B$B今天,我們要改變這件事。拿起這個旌旗並且殺死那些軍團惡魔。等到憎恨密使出現,殺死他並且刺穿他。','先驅者因努羅要你殺在曙光廣場或日境軍械庫的6隻燃燒軍團惡魔以及憎恨密使。用破碎之日旌旗刺穿憎恨密使的屍體。','','到奎爾丹納斯島的陽灣聖殿找先驅者因努羅。','刺穿憎恨密使','','','',0), +(11539,'zhTW','奪下港口','凱爾最精英的部隊之一,曦刃軍團目前駐紮日境港和晨星村。$B$B港口在我們的計畫中佔有關鍵的位置,所以你對曦刃造成的每一分損傷都對我們來說是莫大的助力。凱爾薩斯會悔恨他為什麼要背棄他的人民,與燃燒軍團同流合污。','日境軍械庫的博學者伊拉斯塔要你去殺死6名曦刃召喚師、6名曦刃血騎士和3名曦刃神射手。','','到奎爾丹納斯島的日境軍械庫找博學者伊拉斯塔。','','','','',0), +(11542,'zhTW','攔截援軍','我們需要將戰線往前推進,來鞏固我們對日境的控制權,目標就是佔領港口。不幸的是,一支曦刃後備兵的艦隊正在支援凱爾部隊的路上。$B$B帶上這些火油,然後從埃倫‧破雲者那邊借調一隻龍鷹。你可以飛近那些船帆;一旦你靠得夠近,就把那些船帆全點燃。$B$B找一艘船降落,然後對付那些後備部隊。只要他們的船帆著火,他們就無法好好組織防禦。','日境軍械庫的復仇者卡藍要你和埃倫‧破雲者談談,並且飛往曦刃的支援艦隊。使用燃油點燃那些船帆,等你降落後,殺死6名曦刃後備兵。','','到奎爾丹納斯島的日境軍械庫找復仇者卡藍。','燒燬辛洛倫的船帆','燒燬血之誓約的船帆','燒燬曦逐者的船帆','',0), +(11545,'zhTW','仁慈的募捐','我們已經從敵人的手中徹底奪回了陽灣港,最終的勝利就在眼前。不過,我們也付出了非常沉重的代價,許多英勇的戰士倒在了敵人的刀下。$B$B我現在唯一想做的事情就是讓這些犧牲者永遠被人們銘記,永遠不被遺忘。現在我們打算在這裡建造一座紀念碑,在上面篆刻所有犧牲者的姓名。當然,如果你能捐一筆錢就再好不過了,希望你能考慮一下這件事。多餘的資金將會用來幫助英烈們留下來的孤兒與遺孀。','奎爾丹納斯島陽灣港的學者艾尤莉希望你捐贈10枚金幣。','','','','','','',0), +(11551,'zhTW','第一道屏障,阿加麥斯之門','','','','到奎爾丹納斯島找大法師奈蘇爾。','','','','',0), +(11552,'zhTW','第二道屏障,洛希多爾之門','','','','到奎爾丹納斯島找大法師奈蘇爾。','','','','',0), +(11553,'zhTW','最後的屏障,埃庫尼蘇之門','','','','到奎爾丹納斯島找大法師奈蘇爾。','','','','',0), +(11556,'zhTW','戰場上的崇敬','','','','','','','','',0), +(11557,'zhTW','所有戰士的崇拜','','','','','','','','',0), +(11558,'zhTW','危險的愛','這種像傳染病的愛有些不對勁。這情形很噁心,而且當它降低了我們的警戒心,對所有人來說都是個威脅。$b$b我們有這麼多人民都已經陷入如此荒繆的行為已經夠糟了。但我想這甚至還散播到我們的守衛那裡了,他們是應該對這種事免疫的人。$b$b找一名我們的守衛,看看他們是否已經得到了這愚蠢的病。','取得一張守衛發黴的卡片並帶給幽暗城的芬斯塔德‧阿吉歐。','','到奧特蘭克山脈的洛丹米爾湖找芬斯塔德·阿吉歐。','','','','',0), +(11578,'zhTW','The \'Chow\' Quest (123)aa COPY','$T蠢蛋;!殺死2個狗頭人惡黨。','殺死兩個狗頭人惡黨。','','找本尼任務給予者。','','','','',0), +(11579,'zhTW','「狗頭人」任務(123)aa COPY','$T蠢蛋;!殺死2個狗頭人惡黨。','殺死兩個狗頭人惡黨。','','找本尼任務給予者。','','','','',0), +(11621,'zhTW','萊維羅希的石板','你清理了石板上的刻痕,找尋任何你看得懂的內容。你終於找到了以下的片段:$B$B「迷霧之子將要收集一千個靈魂,以獻給居於晦暗深海中的,喚醒他的特使。」$B$B卡魯克或許會對這個消息感興趣。','把你在石板上找到的訊息告訴卡魯克。','','','','','','',0), +(11653,'zhTW','哈...你現在可沒那麼大囉!','這樣就可以應付她了,老大!接下來,我要你這麼做...$B$B帶著我的光束槍,朝著北邊前往瑪格默斯。如果我們想要有機會對抗那些怪物,我們必須減小他們的體型...就這麼辦。$B$B為了評估光束槍的效能,我要你在猛瑪象人身上的光束槍效果結束之前殺掉牠們。任何粉碎者或是配偶都可以,不過放過那些後代吧...他們還很小,不用擔心。','在5隻瑪格默斯粉碎者或瑪格默斯瑞卡的配偶身上檢測卡芙緹的超進階初體版微縮光束槍。在完成後,回到嘶軸簡易機場找她。','','','測試卡芙緹的光束槍','','','',0), +(11657,'zhTW','接住火炬','既然你已經學會投擲火炬了,讓我們看看你接不接得住它!$B$B拿著這堆未點燃的火炬。去篝火那邊然後把火炬高高地丟向空中!然後在火炬落地之前接住它…然後再把它丟回去!連續接住四個後,就回來找我。$B$B小心點!如果它們掉到地上可是很危險的!所以千萬別失誤!要是你真的失誤了,那你就得回到篝火那再重新開始一次。祝你好運!','連續接住4根火炬,然後跟吞火者大師談話。','連續接住火炬4次。','找吞火者大師。','','','','',0), +(11665,'zhTW','城裡的鱷魚','你好,$g小夥子:姑娘;。我很高興你願意停下來和這個老人聊天 -- 家裡最近出了一些狀況。$b$b一個旅行商人把一窩鱷魚寶寶賣給了容易受騙的孩子。鱷魚是狂野的獸類,許多已經逃走,潛伏在城裡的下水道中。$b$b拿出你最堅固的釣魚竿吧,在暴風城或是奧格瑪垂下魚標,把一隻小惡魔帶回來給我。我急著想看看這些鱷魚寶寶。','把一隻鱷魚寶寶帶給老人巴洛。你可以在撒塔斯城東北邊的泥濘湖畔找到他在那釣魚。','','到泰洛卡森林的泥濘湖畔找老人巴洛。','','','','',0), +(11666,'zhTW','魚餌強盜','你一定知道有關金色淡水小魚的事--牠們遍佈於所有泰洛卡森林的河中,對牧師來說是一道美味佳餚。不過這裡有個你從來都沒聽說過的:黑鰭小魚。$b$b這種魚是出名的狡詐且難以捕捉。你得在最佳的緊要關頭才能拉動你的釣線,因為牠們在咬住餌之前都很小心翼翼。$b$b將你的魚線丟入泰洛卡森林的河中,並將一尾黑鰭小魚帶來給我。別在湖中垂釣,黑鰭小魚只生長在流動的水中。','帶一尾黑鰭小魚給老人巴洛。你可以在撒塔斯城東北邊的泥濘湖畔找到他在那釣魚。','','到泰洛卡森林的泥濘湖畔找老人巴洛。','','','','',0), +(11667,'zhTW','逃脫的傢伙','微光泥鰍是一種在納葛蘭水域中非常靈活的底棲魚。他們一生都在不斷成長,並以戰士的別稱聞名。$b$b許多年前,我抓到一隻我認為是世界上最大的泥鰍...直到牠咬斷我的魚線並且掙脫。$b$b像你這種程度的漁夫是不需要什麼指點 -- 你的命運正等在納葛蘭的湖畔。','抓到世界最大的泥鰍,然後帶給老人巴洛。你可以在撒塔斯城東北邊的泥濘湖畔找到他在那釣魚。','','到泰洛卡森林的泥濘湖畔找老人巴洛。','','','','',0), +(11668,'zhTW','抓蝦子一點都不簡單','贊格沼澤的湖中擁有豐富的魚產。懷疑過他們以什麼為食嗎?$b$b就是蝦子。巨型清水蝦。洛克跟我說你可以用蝦子煮出無數的菜色。$b$b但是蝦子實在太小了,沒辦法用釣線來釣。所以這有個點子:把浮腫的倒刺鰓鱒魚剖開,夠幸運的話你會在當中找到一兩隻蝦子。','帶10隻巨型清水蝦給老人巴洛。你可以在撒塔斯城東北方找到他在泥濘湖畔垂釣。','','到泰洛卡森林的泥濘湖畔找老人巴洛。','','','','',0), +(11669,'zhTW','魔化之血肉片','我這個老人就快要去打獵了,打敗艾薩拉海岸外的一隻巨大鯊魚。$b$b事實上,我需要一種在水中也不會溶解的毒藥。想要製作出那樣的東西,我會需要一種腺體,來自於一種毒魚:魔化之血鯛魚。$b$b像這種邪惡的魚種只存活在地獄火半島和影月谷的惡水中。帶條大魚給我,我就會是個快樂的老人了。','帶一尾可怕的魔化之血鯛魚給老人巴洛。你可以在撒塔斯城東北邊的泥濘湖畔找到他在那釣魚。','','到泰洛卡森林的泥濘湖畔找老人巴洛。','','','','',0), +(11691,'zhTW','召喚艾胡恩','我和手下的薩滿已經聚集我們的力量,為了接下來和冰霜領主的戰鬥做好準備。$n,你將會是大地之母的拳頭。$B$B帶著這根熔岩圖騰。對東北方的冰石使用它。艾胡恩正在透過那個石塊引導他的力量;我們希望圖騰能夠打斷他並且切斷他和元素位面的連結。$B$B如果我們夠幸運的話,那或許我們就能打敗他。$B$B做好準備,$n。如果艾胡恩和我們正面衝突,他將會非常瘋狂與憤怒。','將陶土議會熔岩圖騰帶到冰石旁邊。','','到希爾斯布萊德丘陵的達隆山找冰石。','','','','',0), +(11696,'zhTW','艾胡恩就在這裡!','$n,你的出現真是個恩賜。艾胡恩就在那些洞穴之中,為他的攻擊積蓄力量。陶土議會已經派人出發了,但單靠他們是無法擊敗艾胡恩的。$B$B探索吧,$n。找到我們陶土議會的薩滿,和他們的領袖魯瑪‧天空之母談談。或許加上你的力量,我們能在一切太遲之前打倒艾胡恩。','在奴隸監獄中找到魯瑪‧天空之母。','','','','','','',0), +(11731,'zhTW','投擲火炬','你想當一名火焰雜耍師?你已經具備這個資格了嗎?你夠靈敏嗎?反應夠快嗎?你有勇氣嗎?假如你認為你已經具備以上的條件,那麼我們來做個小小的練習吧!$B$B拿著這些火炬,然後跑到篝火的旁邊。你會看到火盆上閃爍著標誌。對標示的火盆投擲一根火炬。動作要快!你得在標誌消失以前擲出火炬才算數!$B$B在時間結束之前擊中足夠數量的火盆,然後回到這裡…我們再來進行下一個課程。','在火炬投擲遊戲中獲勝,然後與吞火者大師談話。','擊中8個火盆。','找吞火者大師。','擊中火盆','','','',0), +(11790,'zhTW','教徒內奸','這個祭壇布滿各種詛咒神教的印記。看來本土上的那些狂信者也來到了北裂境。最好去通知這艘船的船長。$B$B你或許可以在甲板上找到他。','與『左撇子』縱帆船長談談,他就在驍勇要塞的風暴破碎者上。','','','','','','',0), +(11861,'zhTW','榮耀火焰','','','','','','','','',0), +(11862,'zhTW','榮耀火焰','','','','','','','','',0), +(11863,'zhTW','榮耀火焰','','','','','','','','',0), +(11882,'zhTW','玩火','你想學點熱騰騰的新技巧嗎?$b$b我不知道你有沒有辦法學會我這種技藝,不過如果你是個夠專心的學生的話,或許能學會一兩招。去跟任何一個聯盟主城的吞火者大師談談。他們應該可以協助你。','跟任何一個聯盟主城的吞火者大師談談。','','','','','','',0), +(11883,'zhTW','火舞?','你想要學習火舞的藝術?$b$b很好,那去跟任何一個部落主城的火舞者大師談談。他們應該可以協助你。','跟任一個何部落主城的火舞者大師談談。','','','','','','',0), +(11886,'zhTW','不尋常的活動','當慶典舉行時,$c,我們當中的某些人就需要留意那些較嚴重的問題。$b$b在節慶開始時,我們收到了來自佐拉姆海岸的報告,有關於暮光教派的活動明顯增加。這個節日的起源就是緬懷那些瘋子所崇敬的強大力量;這不可能是巧合。前往黑暗深淵入口南邊的營地去,看看那些暮光教徒在搞什麼鬼 - 必要的時候使用武力也沒關係。等你找到的時候,使用這個信號來召喚一個嚮導。','前往位於梣谷,佐拉姆海岸的黑暗深淵入口南邊新成立的暮光營地。為陶土議會找出答案,然後使用圖騰信號召喚陶土議會嚮導。','','找陶土議會嚮導。','','','','',0), +(11891,'zhTW','無辜的偽裝','根據這個看來,在佐拉姆海岸的大集會西北邊有另一個教徒營地,而且他們正在與...客人說話?$b$b親耳聽聽會知道的更多,$r。拿著這個寶珠;它可讓你以本地海灘爬行物的模樣出現。去刺探那些教徒在西北邊營地中的「客人」談話,成功後回報給我。','使用爬行者寶珠去刺探位於梣谷,佐拉姆海岸的黑暗深淵西邊新成立的暮光營地。然後使用圖騰信號召喚陶土議會嚮導。','偷聽暮光教徒的陰謀。','找陶土議會嚮導。','','','','',0), +(11915,'zhTW','玩火','你想學點熱騰騰的新技巧嗎?$b$b我不知道你有沒有辦法學會我這種技藝,不過如果你是個夠專心的學生的話,或許能學會一兩招。去跟任何一個部落主城的吞焰者大師談談。他們應該可以協助你。','跟任一個何部落主城的吞焰者大師談談。','','','','','','',0), +(11917,'zhTW','反攻','我們必須以行動來維持平衡!時間對我們相當不利。$b$b回到佐拉姆海岸矗立著大型冰石的營地。他們可以利用這些東西來和元素溝通,就像是希利蘇斯那樣。雖然我不喜歡這樣,那些精靈會瞭解的 - 你必須要召喚艾胡恩手下的一個副官,並且摧毀它。或許這樣先發制人的行動會有效...','把冰石召喚出來的霜浪副官殺死,冰石就在梣谷的佐拉姆海岸。然後向主城中的陶土議會長老回報。','','找陶土議會長者。','','','','',0), +(11921,'zhTW','投擲更多的火炬','哈囉,火焰雜耍師!你玩得開心或是燒掉了什麼嗎?那就是我想聽到的事!現在,你覺得自己是個火炬投擲的專家嗎?既然這樣的話我有個測試給你...$B$B你知道規則:將火炬投擲到標記的火盆裡。不過,這一次你擁有的時間變少,你得投中的火盆更多了。$B$B準備好了嗎?','在火炬投擲遊戲中獲勝,然後與吞火者大師談話。','擊中20個火盆。','找吞火者大師。','擊中火盆','','','',0), +(11922,'zhTW','投擲火炬','你想當一名火焰雜耍師?你已經具備這個資格了嗎?你夠靈敏嗎?反應夠快嗎?你有勇氣嗎?假如你認為你已經具備以上的條件,那麼我們來做個小小的練習吧!$B$B拿著這些火炬,然後跑到篝火的旁邊。你會看到火盆上閃爍著標誌。對標示的火盆投擲一根火炬。動作要快!你得在標誌消失以前擲出火炬才算數!$B$B在時間結束之前擊中足夠數量的火盆,然後回到這裡…我們再來進行下一個課程。','在火炬投擲遊戲中獲勝,然後跟吞焰者大師談話。','擊中8個火盆。','找吞焰者大師。','擊中火盆','','','',0), +(11923,'zhTW','接住火炬','既然你已經學會投擲火炬了,讓我們看看你接不接得住它!$B$B拿著這堆未點燃的火炬。去篝火那邊然後把火炬高高地丟向空中!然後在火炬落地之前接住它…然後再把它丟回去!連續接住四個後,就回來找我。$B$B小心點!如果它們掉到地上可是很危險的!所以千萬別失誤!要是你真的失誤了,那你就得回到篝火那再重新開始一次。祝你好運!','連續接住4根火炬,然後跟吞焰者大師談話。','連續接住火炬4次。','找吞焰者大師。','','','','',0), +(11924,'zhTW','接住更多的火炬','接火炬當然可以讓你保持清醒。你以為你可以持續很久嗎?$B$B走著瞧吧...','連續接住10根火炬,然後跟吞火者大師談話。','連續接住火炬10次。','找吞火者大師。','','','','',0), +(11925,'zhTW','接住更多的火炬','接火炬當然可以讓你保持清醒。你以為你可以持續很久嗎?$B$B走著瞧吧…','連續接住10根火炬,然後跟吞焰者大師談話。','連續接住火炬10次。','找吞焰者大師。','','','','',0), +(11926,'zhTW','投擲更多的火炬','哈囉,火焰雜耍師!你燒得開心嗎?那就是我想聽到的事!現在,你覺得自己是個火炬投擲的專家嗎?既然這樣的話我有個測試給你…$B$B你知道規則:將火炬投擲到標記的火盆裡。不過,這一次你擁有的時間變少,你得投中的火盆更多了。$B$B準備好了嗎?','在火炬投擲遊戲中獲勝,然後跟吞焰者大師談話。','擊中20個火盆。','找吞焰者大師。','擊中火盆','','','',0), +(11933,'zhTW','偷取艾克索達之焰','這個從艾克索達核心偷出來的火焰,讓你感到很溫暖。這股新的力量對你而言很陌生,但故事使者或許知道更多...','將艾克索達之焰交給節慶故事使者。','','找節慶故事使者。','','','','',0), +(11935,'zhTW','偷取銀月之焰','這個從銀月城偷出來的火焰,讓你感到很溫暖。這股新的力量對你而言很陌生,但博學大師或許知道更多...','將銀月之焰交給節慶博學大師。','','找節慶博學大師。','','','','',0), +(11941,'zhTW','謎題...','這個金屬碎片有某種力量阻礙了你施法。$b$b或許瑞洛拉茲可以壓抑一些它的效果。','將閃爍碎片交給隘境之盾的瑞洛拉茲。','','到北風凍原的隘境之盾找瑞洛拉茲。','','','','',0), +(11943,'zhTW','囚籠','你發現了一個特殊監牢的碎片,$n。$b$b瑪里苟斯的手下使用類似的監牢囚禁被抓到的法師以及其他秘法實體,但這個監牢是製造來囚禁某種...更強大的...$b$b希望我們敵人的敵人能夠成為一個朋友,你要去找到這個監牢的其他碎片。$b$b它們可能由瑪里苟斯最信賴的部下所保管。','隘境之盾的瑞洛拉茲要你從天藍將軍身上取得監禁外殼,以及從戰爭使者寇瑞卓克身上取得能量之核。','','到北風凍原的隘境之盾找瑞洛拉茲。','','','','',0), +(11947,'zhTW','反攻','我們必須以行動來維持平衡!時間對我們相當不利。$b$b淒涼之地西北邊的艾瑟雷索,有個營地藏在它的陰影之下。他們使用冰石和元素溝通,就像是希利蘇斯那樣。雖然我不喜歡這樣,那些精靈會瞭解的 - 你必須要召喚艾胡恩手下的一個副官,並且摧毀它。或許這樣先發制人的行動會有效...','把冰石召喚出來的冰雹副官殺死,冰石就在淒涼之地的艾瑟雷索。然後向主城中的陶土議會長老回報。','','找陶土議會長老。','','','','',0), +(11948,'zhTW','反攻','我們必須以行動來維持平衡!時間對我們相當不利。$b$b奈普圖隆一直以來都在荊棘谷西岸小島上有個基地。暮光教派在此設立了冰石,來和獵潮者的國度溝通,就像是希利蘇斯那樣。雖然我不喜歡這樣,那些精靈會瞭解的 - 你必須要召喚艾胡恩手下的一個副官,並且摧毀它。或許這樣先發制人的行動會有效...','把冰石召喚出來的冰風副官殺死,冰石就在荊棘谷西邊的水元素小島。然後向主城中的陶土議會長老回報。','','找陶土議會長老。','','','','',0), +(11952,'zhTW','反攻','我們必須以行動來維持平衡!時間對我們相當不利。$b$b灼熱峽谷的觀火嶺是暮光教派在此地的活動基地。最近他們透過冰石來和獵潮者的國度溝通,就像是希利蘇斯那樣。雖然我不喜歡這樣,那些精靈會瞭解的 - 你必須要召喚艾胡恩手下的一個副官,並且摧毀它。或許這樣先發制人的行動會有效...','把冰石召喚出來的嚴寒副官殺死,冰石就在灼熱峽谷的西北部,觀火嶺底下的洞穴。然後向主城中的陶土議會長老回報。','','找陶土議會長老。','','','','',0), +(11953,'zhTW','反攻','我們必須以行動來維持平衡!時間對我們相當不利。$b$b希利蘇斯是暮光教派的艾澤拉斯基地。他們在北部透過新的冰石來和獵潮者的國度溝通。雖然我不喜歡這樣,那些精靈會瞭解的 - 你必須要召喚艾胡恩手下的一個副官,並且摧毀它。或許這樣先發制人的行動會有效...','把冰石召喚出來的冰川副官殺死,冰石就在希利蘇斯北部的暮光小徑。然後向主城中的陶土議會長老回報。','','找陶土議會長老。','','','','',0), +(11954,'zhTW','反攻','我們必須以行動來維持平衡!時間對我們相當不利。$b$b為了確保艾胡恩能夠成功,暮光教派在德拉諾靠近黑暗之門的地方建立了一個基地。他們透過冰石來協助冰霜領主的抵達。雖然我不喜歡這樣,那些精靈會瞭解的 - 你必須要召喚艾胡恩手下的一個聖殿騎士,並且摧毀它。或許這樣先發制人的行動會有效...','把冰石召喚出來的冰川聖殿騎士殺死,冰石就在地獄火半島靠近黑暗之門的地方。然後向主城中的陶土議會長老回報。','','找陶土議會長老。','','','','',0), +(11955,'zhTW','艾胡恩,冰霜領主','我相信你有足夠的力量能承擔我們眼前的責任。$b$b我們不能容許暮光教派和奈普圖隆的納迦異端實現他們的計畫。為了艾澤拉斯以及這塊土地上所有的生命,我們一定要行動。我們的目標十分明確,你必須前往盤牙蓄湖中的奴隸監獄,去找我們在那邊的薩滿。去吧,要快!','前往贊格沼澤的盤牙蓄湖,進入其中的奴隸監獄,並且和紐瑪‧雲女交談。','','','','','','',0), +(11964,'zhTW','夏日小焦焰的薰香','你知道節慶火焰在每個聯盟營地都有嗎?火焰看守者在每個城鎮之外照顧它們,確保篝火明亮和慶典的維持。$b$b每位火焰看守者都有一個夏日小焦焰陪伴 -- 火焰精靈的使者。$B$B你想要向小焦焰獻上敬意嗎,$n?帶著這個夏日薰香,把它交給任何一個夏日小焦焰。取悅那些火焰小精靈,它或許會回報你。','節慶博學大師要你把夏日薰香帶給任何一座聯盟據點外的夏日小焦焰。','','找夏日小焦焰。','','','','',0), +(11966,'zhTW','節慶小焦焰薰香','你知道節慶火焰在每個部落營地都有嗎?火焰守護者在每個城鎮之外照顧它們,確保篝火明亮和慶典的維持。$b$b每位火焰看守者都有一個節慶小焦焰陪伴 -- 火焰精靈的使者。$B$B你想要向小焦焰獻上敬意嗎,$n?帶著這個夏日薰香,把它交給任何一個節慶小焦焰。取悅那些火焰小精靈,它或許會回報你。','節慶故事使者要你把夏日薰香帶給任何一座部落據點外的節慶小焦焰。','','找節慶小焦焰。','','','','',0), +(11970,'zhTW','夏日傳說大師','節慶固然是個歡樂與笑語的時刻,然而讓你知道這個節日的由來也很重要。教育是一種需要追求並且細細品味的東西。$b$b聯盟主城的節慶博學大師能夠引領你。去好好學習吧。','和聯盟主城的任何一位節慶博學大師談談。','','','','','','',0), +(11971,'zhTW','夏日故事使者','節慶固然是個歡樂與笑語的時刻,然而讓你知道這個節日的由來也很重要。教育是一種需要追求並且細細品味的東西。$b$b部落主城的節慶故事使者能夠引領你。去好好學習吧。','和部落主城的任何一位節慶故事使者談談。','','','','','','',0), +(11972,'zhTW','艾胡恩裂片','這些裂片不祥地震動著。它們蘊含著冰霜領主的最後精華嗎?','將寒冰裂片交給魯瑪‧天空之母。','','回奴隸監獄找魯瑪·天空之母。','','','','',0), +(11974,'zhTW','[ph]等我長大了……','你在我這麼大的時候,你就知道你長大後會想當個$c嗎?$B$B我還不確定我想做什麼呢。我以前想當個血騎士,但現在不會了。現在我想當個搖滾巨星!像是那個70級牛頭大佬裡面的血精靈!$B$B沒在巡迴的時候,他們常在銀月城裡閒逛耶!你可以帶我去那裡嗎?他們常在長者步道的一個陽台…大概是不想讓歌迷太靠近吧。但我們總可是試著靠近,對吧?$B$B拜託帶我去,拜託!','帶你的孤兒,瑟蘭德雅去看70級牛頭大佬,他們在銀月城的長者步道。當你抵達時,如果她不附近記得要叫她。','瑟蘭德雅在銀月城拜訪了70級牛頭大佬','找血精靈孤兒。','','','','',0), +(11975,'zhTW','現在,等我長大...','也許我已經問過你了,你在我這麼大的時候,你就知道你長大後會想當個$c嗎?$B$B我還不確定我想做什麼呢。也許是法師、術士或是個血騎士,就像我以前說的?說不定…一個搖滾巨星!!像席格‧尼修斯,那個精英牛頭大佬裡面的血精靈!$B$B我聽說沒在巡迴的時候,他們常在銀月城裡閒逛耶!你可以帶我去那裡嗎?他們就坐在長者步道的一個陽台,等別人來拜訪他們。$B$B拜託帶我去,拜託!','帶你的孤兒,瑟蘭德雅去看精英牛頭大佬,他們在銀月城的長者步道。當你抵達時,如果她不附近記得要叫她。','','找血精靈孤兒。','','','','',0), +(11976,'zhTW','冰片','碎片跳動著,透露出不祥的徵兆。難道其中蘊含著冰霜領主的精華?','將冰片交給魯瑪·天母。','','到希爾斯布萊德丘陵的達隆山找魯瑪·天空之母。','','','','',0), +(11987,'zhTW','幸運卡片:白銀','','','','','','','','',0), +(11992,'zhTW','海圖','你在龍皮上發現了海圖的一部分。如果能拼湊出完整的海圖,或許可以瞭解克瓦迪爾未來的攻擊計畫。$B$B裂鞭海岸的水手身上說不定就攜帶著海圖的缺失部分。卡魯克應該會對上面的內容感興趣吧。','將斯卡迪爾海圖的缺失部分交給裂鞭海岸的卡魯克。','','到北風凍原找卡魯克。','','','','',0), +(11995,'zhTW','繁星之眠需要你','$C,我知道那些巨牙海民會要求你幫很多忙,但你必須要前往繁星之眠,而且要盡快!$B$B今天早上的劇變一定有很廣泛的影響,絕對不止於原住民的不便。$B$B港口北面出去的十字路口的西邊就是繁星之眠。那邊有個大法師叫做莫德菈。她會在那邊等你。','到繁星之眠和大法師莫德菈的影像談談。','','','','','','',0), +(11996,'zhTW','阿格瑪之錘需要你','$C,我知道那些巨牙海民會要求你幫很多忙,但你必須要前往阿格瑪之錘,而且要盡快!$B$B今天早上的劇變一定有很廣泛的影響,絕對不止於原住民的不便。$B$B港口北面出去的十字路口的西邊就是阿格瑪之錘。大法師埃薩‧奪日者會在那邊等你!','和位於阿格瑪之錘的大法師埃薩‧奪日者的影像談談。','','','','','','',0), +(11997,'zhTW','再次向格裡安·斯托曼報到','月溪旅奉命轉至灰熊丘陵東北部,確保那裡的通行安全。他們在灰喉堡東北方、索爾莫丹西南方創建了一處營地。$B$B你應該認識月溪旅的領袖格裡安·斯托曼上尉吧?在對抗洛丹倫天災軍團的戰役中,格裡安立下了赫赫戰功。但他卻毅然決定離開戰場,返回西部荒野對抗迪菲亞兄弟會。','與月溪旅營地的格裡安·斯托曼上尉談一談。','','','','','','',0), +(11999,'zhTW','搜索屍體','西南邊的月眠花園是一群精靈貴族的最後安息地。同時也是一個地脈核心。$B$B藍龍軍團在使用一種叫做極濤磁針的魔法裝置要摧毀核心,將能量釋出好讓他們可以隨意重新導向。$B$B我需要更多的訊息,而你要幫我取那些訊息!$B$B在那個地方被摧毀成遺跡之後,精靈貴族的鬼魂重新甦醒,殺死了那邊大半的狩法獵人。去搜索他們的屍體尋找線索。','找出月眠花園計畫書,並交到阿格瑪之錘給大法師埃薩‧奪日者的影像。','','到龍骨荒野的阿格瑪之錘找大法師埃薩·奪日者的影像。','','','','',0), +(12000,'zhTW','搜索屍體','西南邊的月眠花園是一群精靈貴族的最後安息地。同時也是一個強大的地脈核心。$B$B藍龍軍團在使用一種叫做極濤磁針的魔法裝置要摧毀核心,將能量釋出好讓他們可以隨意重新導向。$B$B我需要更多的訊息,而我希望你能幫我取得那些訊息。$B$B在那個地方發生劇變而變成遺跡之後,精靈貴族的鬼魂重新甦醒,殺死了那邊大半的狩法獵人。去搜索他們的屍體尋找線索。','找出月眠花園計畫書,並帶去繁星之眠給大法師莫德菈的影像。','','到龍骨荒野的繁星之眠找大法師莫德菈的影像。','','','','',0), +(12006,'zhTW','為暴行復仇!','這些被稱為狩法獵人的生物闖入並毀掉了我們的居地。$B$B為我們復仇,否則我會確保你的軍事基地被亡靈大軍給輾過!','殺死總計15名的龍骨荒野狩法獵人、月眠巡者、極濤磁針巫士或節點巫師。完成之後向月眠花園的伊希尼歐‧月影回報。','','到龍骨荒野的月眠花園找伊希尼歐·月影。','殺死月眠花園的藍龍軍團部隊','','','',0), +(12009,'zhTW','突凱亞的螃蟹陷阱','因為這些麻煩事,我失去了助手。如果可以的話你能不能幫幫我?$B$B我在港口的海面下設置了許多補蟹陷阱。你能幫我拿回來嗎?我太老了,動不了了,大海對我來說又太危險。$B$B求求你,拿著這顆氣囊,你在水面下的時候它會幫助你呼吸。','收集8個突凱亞的螃蟹陷阱,交還給默亞基港的突凱亞。','','到龍骨荒野的默亞基港找突凱亞。','','','','',0), +(12012,'zhTW','通知長老','這個消息必須要通知整個陶土議會。我不確定該如何進行,但把任務交付給你的長老或許知道。拜託,回去向長老報告,並且採取下一步行動 - 我們一定得做點什麼!','回到任何一座主城,並和陶土議會長老交談。','','','','','','',0), +(12020,'zhTW','就這麼一次,在我酒醉的時候...','黑鐵矮人被痛扁了一頓!就在他們夾著尾巴逃回黑鐵酒吧的時候,他們還掉了一些貨物下來。最重要的是,你保住了營地...至少今天保住了。$b$b去找博克西,然後向他說明你擊敗黑鐵矮人的英勇事蹟。','與啤酒節營地的博克西‧栓旋者交談。','','','','','','',0), +(12022,'zhTW','乾了再砸!','嘿!看來你酒量不錯嘛。\n\n但你駕馭得了酒杯嗎?\n\n我們拿那些把營地搞得天翻地覆的麥芽酒沒轍。我們發現如果要重新把這些活生生的麥芽酒給抓住,最好的辦法就是朝它們丟酒杯!\n\n看來你需要練習練習。如果你能丟中S.T.O.U.T.,那下次麥芽酒又活過來時你應該可以幫得上忙。拿一杯樣品酒來,乾了,然後把酒杯丟出去!','朝S.T.O.U.T.丟無酒精的啤酒節樣品並丟中5次。','','到丹莫洛找博克西·栓旋者。','命中S.T.O.U.T.','','','',0), +(12027,'zhTW','軟綿綿先生的冒險之旅','抱歉打擾你,$g先生:女士;,你知道回去軍營的路嗎?我和軟綿綿先生本來決定在樹林裡走走,而我的兄弟沃特也要跟來的,可是他有侍從工作要做只得作罷。$B$B我和軟綿綿先生自個兒來,但是迷路了,森林裡到處都是飢餓的狼群,不過,我們找到了這座塔。$B$B狼走了,可是我們還是走不出去。$B$B我問過軟綿綿先生,他也不清楚回去的路,可以幫幫我們嗎?行行好?','幫助愛蜜莉和軟綿綿先生回到西部荒野民兵團駐營,然後再向侍從沃特交談。','幫助艾米莉和小毛球逃回營地','到灰白之丘的西部荒野民兵團駐營找侍從沃特。','','','','',0), +(12028,'zhTW','靈魂洞察','$R,自從地震分隔之後,我再也沒有聽過因度雷村族人的消息了。$B$B你有經歷過幻象任務嗎?你打敗過洛根,你一定能夠勝任的。$B$B我會給你一些特製的薰香灑在我的火盆中。把它的煙霧用力吸進去,你的靈魂就會飄升到空中,可以看見因度雷的情況。$B$B隨著你的靈魂升起,我就可以透過你的眼睛觀察。','完成靈魂洞察之後和默亞基港的『秘法師』托阿魯交談。','通過靈魂視界觀察因度雷村的情況。','到龍骨荒野的默亞基港找『秘法師』托阿魯。','','','','',0), +(12030,'zhTW','長者馬納洛亞','我的人民...$B$B<秘法師瀕臨崩潰邊緣。>$B$B當你的靈魂飛越這座村子的時候,我感受到長者馬納洛亞注意到你。$n,他希望和你聊聊。我們將這些岩石長者尊崇為遠古智慧的明燈。$B$B長老一定是想要幫忙,到村子的西北邊找到他。','和長者馬納洛亞交談,他就是因度雷村中的雕像。','','','','','','',0), +(12031,'zhTW','解放徘徊者','精靈在西方的聖地被蹂躪得支離破碎。如今,魔法在土地間流竄,所經之處飽受摧殘。$B$B當藍龍試著要控制這股法力的同時,卡魯耶克的靈魂陷入了危機。$B$B這股力量自湖泊竄出,瞬間就殺死了絕大多數的村民,而倖存者都陷入了瘋狂。$B$B$n,你一定要馬上釋放這些靈魂,他們才能超生。放過那些瘋狂的生物;他們還有機會能獲得理智。','讓總計15名的因度雷漁夫、因度雷秘法師或因度雷戰士獲得安息,並向因度雷村的長者馬納洛亞回報。','','到龍骨荒野的因度雷村找長者馬納洛亞。','讓因度雷靈魂安息','','','',0), +(12123,'zhTW','警告龍后','<巨龍管理者將介紹信從你手中取走。>$B$B是...是呀,我想你已經準備好覲見女王。$B$B準備好就告訴我吧,我會命這些飛龍安全地引領你到神殿的頂端。你得親自將這封信呈給雅立史卓莎女王。','將介紹信送交到龍眠神殿的『生命守縛者』龍后雅立史卓莎手上。','','到龍骨荒野的龍眠神殿找『生命守縛者』龍后雅立史卓莎。','','','','',0), +(12133,'zhTW','砸碎南瓜','這個大型南瓜燈籠放在村莊的中央。那些看著它眼睛的人,都會受到當中黑暗威脅的引誘而誤入歧途。$B$B砸碎南瓜之後露出一個老舊、燒焦的聖徽。','將燒焦的聖徽交給變裝的孤兒監護員。','','找變裝的孤兒監護員。','','','','',0), +(12135,'zhTW','「讓火焰來吧!」','你一定得幫幫我們!無頭騎士可能會在任何時候攻擊這個村子!火焰將會到處都是!孩子們並不安全!$B$B拜託,$n,你願意面對無頭騎士的火焰嗎?等他來的時候,你一定要加入滅火的行列。拿起水桶灑向火源,或是交給靠近火勢的朋友。如果你能熄滅那些火焰,我們或許還有救!','變裝的孤兒監護員希望你在無頭騎士縱火之後,幫助撲滅村莊中的火勢。等火完全撲滅之後,再和變裝的孤兒監護員交談。','撲滅火焰','找變裝的孤兒監護員。','','','','',0), +(12139,'zhTW','「讓火焰來吧!」','無頭騎士可能會在任何時候攻擊這個村子!火焰將會到處都是!$B$B$n,你願意面對無頭騎士的火焰嗎?等他來的時候,你一定要加入滅火的行列。拿起水桶灑向火源,或是交給靠近火勢的朋友。如果你能熄滅那些火焰,我們或許還有救!','戴面具的孤兒監護員要你幫忙撲滅村莊中的火勢。等火完全撲滅之後,再和鎮上的戴面具的孤兒監護員交談。','撲滅火焰','找戴面具的孤兒監護員。','','','','',0), +(12153,'zhTW','鐵族長與他的鐵砧','既然你已經將你的戰爭魔像充能完成,接下來就是要好好利用能源了。到丹亞戈的內部啟動魔像然後控制它。$B$B就我們所收集到的資料來看,丹亞戈的領袖,鐵族長怒錘,他身邊的私人保鏢,推測能夠保護他不受一切的傷害。$B$B那個保鏢就是和他一道旅行的構裝生物,大家都叫它鐵砧。癱瘓那尊構裝生物,那麼他的保護也就同時消失。$B$B族長常常待在主要工廠裡面,位在丹亞戈的頂端,在那裡監督下層產品進度。','利用戰爭魔像的能力,擊敗鐵族長怒錘,接著向勘察員崗哨的瑞加爾‧斷眉回報。','','到灰白之丘的勘察員崗哨找瑞加爾‧斷眉。','','','','',0), +(12154,'zhTW','熄燈號','丹亞戈的頂端大廳儲存了整個區域的能量來源。如果你能將動力核心摧毀,這可以癱瘓整個丹亞戈好一會兒。$B$B沒有能量就沒有魔像、沒有哨兵更沒有閃電武器。$B$B當我們被送上去以後,他們說這些炸藥是用來逃脫的,但是都是因為鐵族長怒錘,這些探查都報銷了。不過我們還是可以利用這些炸藥就是,畢竟,這些是我們唯一可以用來炸毀動力核心的東西。','利用瑞加爾的爆裂物摧毀丹亞戈的符能核心,接著向勘察員崗哨的瑞加爾‧斷眉回報。','','到灰白之丘的勘察員崗哨找瑞加爾‧斷眉。','摧毀丹亞戈能量核心','','','',0), +(12155,'zhTW','砸碎南瓜','這個大型南瓜燈籠放在村莊的中央。那些看著它眼睛的人,都會受到當中黑暗威脅的引誘而誤入歧途。$B$B砸碎南瓜之後露出一個老舊、燒焦的聖徽。','將燒焦的聖徽交給戴面具的孤兒監護員。','','找戴面具的孤兒監護員。','','','','',0), +(12180,'zhTW','被俘的勘察員','為了探查神器和當地歷史的資訊,瑞加爾派了一隊的勘察員進丹亞戈。瑞加爾的小隊卻從未料想到丹亞戈是如此的人口眾多。$B$B當鐵矮人備戰的同時,他們被俘,但是我們不夠人手救出他們,而瑞加爾又忙著幻想著如何打敗鐵矮人而分身乏術。$B$B任務落在你肩上了,$n,任何鐵矮人和他們僕役的身上,都有可能帶著關有勘查員牢房的鑰匙。','救出勘察員加恩、勘察員拓剛和勘察員韋拉那,再向勘察員崗哨的巡山人基立安回報。','','到灰白之丘的勘察員崗哨找巡山人基立安。','救出勘察員加恩','救出勘察員拓剛','救出勘察員韋拉那','',0), +(12185,'zhTW','為洛肯笑一個','我已經利用你捕捉的影像,把監督者偽裝所需的一切給準備妥當了。不是我要自誇,你偽裝的鐵矮人真不賴。$B$B把偽裝工具帶去丹亞戈,偽裝你自己,前去城市寬廣的中央區最東邊建築物裡面。$B$B在那裡,你應該可以看見一個平臺,用來讓族長和監督者聽取洛肯的命令。披著偽裝啟動它,盡你所能的蒐集情報。','把你自己偽裝成鐵符文監督者,攔截洛肯的訊息,然後回報給勘察員崗哨的巡山人基立安。','','到灰白之丘的勘察員崗哨找巡山人基立安。','收到洛肯的訊息','','','',0), +(12191,'zhTW','乾了再砸!','嘿,看來你的手臂挺有力的!\n\n我就開門見山地說了。有時候麥芽酒會在營地附近大鬧。我們花了大把的錢才把那些商人和他們的酒給請到這裡來!一丁點酒都不能浪費掉,更別說是讓它們活過來了!\n\n讓麥芽酒回到原始型態最好的方法就是朝它丟酒杯。把樣品酒喝光,然後朝這邊的S.T.O.U.T.丟酒杯。證明你有能力保護我們的財產!','朝S.T.O.U.T.丟無酒精的啤酒節樣品並丟中5次。','','到杜洛塔找比索·迅提。','命中S.T.O.U.T.','','','',0), +(12192,'zhTW','就這麼一次,在我酒醉的時候...','黑鐵矮人被痛扁了一頓!就在他們夾著尾巴逃回黑鐵酒吧的時候,他們還掉了一些貨物下來。最重要的是,你保住了營地...至少今天保住了。$b$b去找比索,然後向他說明你擊敗黑鐵矮人的英勇事蹟。','與啤酒節營地的比索‧迅提交談。','','','','','','',0), +(12194,'zhTW','喂,聽說今年沒有紀念品,是不是真的啊?','我們不但供應每個人啤酒,同時還贈送酒杯!只要用合法的方式就可以換取漂亮的紀念杯!這張兌換卷給你。$b$b跟啤酒節營地後方的布力克斯談話。他是個怪傢伙,甚至現在也還戴著那副護目鏡。他會幫你處理那張兌換券,而所有啤酒節的交易也都可以找他。','把啤酒節酒杯兌換券交給啤酒節營地後方的布力克斯‧修械。','','到杜洛塔找布力克斯‧修械。','','','','',0), +(12228,'zhTW','重獲法術','','','','','','','','',0), +(12233,'zhTW','處理種子','不,不……還是你拿著吧。$B$B你不會真的想讓我拿著它們吧?$B$B接下來你該這麼做……','','','','','','','',0), +(12245,'zhTW','格殺俘虜','我們準備了更多的蠢蛋供你宰殺。$B$B我聽我的探員說一些模範市民落入了血色突襲軍的手中。$B$B這可不行。$B$B回去新壁爐谷,殺掉亡靈衛兵施耐德,資深書記金尼迪斯,工程師波奇,以及大法官亞邁。$B$B據我所知,他們散佈在該區域的各處,但毫無疑問的,他們都待在籠子裡等著被折磨。$B$B和他們說話,宰掉他們!','女間諜瑞派恩要求你除去亡靈衛兵施耐德,資深書記金尼迪斯,工程師波奇,以及大法官亞邁。$B$B任務完成之後,與毒怨之地的高階執行官羅思回報你完成的消息。','','到龍骨荒野的毒怨之地找高階執行官羅思。','','','','',0), +(12252,'zhTW','折磨拷問者','夠了!我要你終止我的子民所受的折磨!$B$B突襲軍的拷問者阿爾馮斯,從每個落在那些混蛋手上的人身上搾取情報!我得知道他們到底得知了什麼情報。$B$B我的線人告訴我他的拷問室位於軍營的地下室,就在鐵匠舖的旁邊。我借給你我個人的烙鐵,確保這任務正確地執行。$B$B找到他,然後問出情報。','毒怨之地的高階執行官羅思要你在拷問者阿爾馮斯身上使用5次烙鐵來拷問出情報。然後殺了他。','','到龍骨荒野的毒怨之地找高階執行官羅思。','徹底拷問拷問者樂卡夫特','','','',0), +(12254,'zhTW','除去祈禱','這些計畫透露了某種暗地裡的陰謀。如果我們要搞清楚究竟是怎麼一回事,我們就得趕快行動。$B$B但首先,我們得先處理突襲軍身上的奇異保護,如果毒怨之地要一勞永逸的對付他們,我們得找出他們背後的傢伙並消滅他們。$B$B我注意到新壁爐谷遠處那間教堂裡的主教會祝福那些部隊。殺了他,竊取他的祈禱書。$B$B也許這本書可以給我們一點線索。','新壁爐谷的史考莉探員要你取得主教史曲特的祈禱書。','','到龍骨荒野的新壁爐谷找史考莉探員。','','','','',0), +(12272,'zhTW','流血的礦石','在納克薩瑪斯進攻的幾小時之前,我們有個礦工碰巧發現了一種暗色礦石,會不斷的流出黏濁的液體。在疏散礦坑的混亂之中,我們失去了那個陌生礦脈的樣本。現在天譴軍團正在為了製造他們的死亡機械探礦。$B$B你必須回去在此處東北方的腐屍農地裡的溫特加德礦坑,並回收這種奇怪的礦石。對我們的任務來說,這是至關緊要的一件事!','龍骨荒野上溫特加德要塞的攻城工程師刻閃要你從溫特加德礦坑回收10個奇怪的礦石的樣本。','','到龍骨荒野的溫特加德要塞找攻城工程師刻閃。','','','','',0), +(12278,'zhTW','每月啤酒俱樂部','會員享有優惠。成為「每月啤酒俱樂部」的會員,表示你有管道獲得到最新、最青的啤酒。$B$B帶著你的「每月啤酒」俱樂部會員表格給拉金‧雷酒,他就在鐵爐堡的石火旅店中。','把「每月啤酒」俱樂部會員表格帶給拉金‧雷酒,他就在鐵爐堡的石火旅店中。','','到鐵爐堡的石火旅店找丹莫羅的拉金‧雷酒。','','','','',0), +(12281,'zhTW','瞭解天譴戰爭機器','我們必須將史霖金的死視為一種安慰,化悲憤為力量。他的發現,以及你在礦坑中英勇的表現,與你所帶回來給我們的樣本,結合起來,讓我們對於天譴軍的戰爭機器有了更進一步的認識。$B$B將這個包裹帶給高階指揮官海弗德‧龍禍,裡面有那個奇異礦石的樣本和我的分析資料。','將刻閃的包裹遞送給位於溫特加德要塞的高階指揮官海弗德‧龍禍。','','到龍骨荒野的溫特加德要塞找高階指揮官海弗德·龍禍。','','','','',0), +(12282,'zhTW','往日的傷痕','$n,過去的記憶困擾著這片大地。我聽見橫貫整個北裂境墮落回音的哭號聲–對我哭喊。$B$B阿薩斯對每一個觸碰過的東西留下了駭人的傷痕。只有凋零與衰敗在他的視野中存留。對我們來說,研究我們敵人的過去非常重要。也許,不為人道的真相才能在這場戰役中突顯出幫助。$B$B首先,你必須在市鎮大廳中找回我的占卜寶珠,$n。離開這座塔,向左轉直走到底,前往市鎮廣場。','位於龍骨荒野,溫特加德要塞的『遠識』齊利格需要你找回他的占卜寶珠。','','到龍骨荒野的溫特加德要塞找『遠識』齊利格。','','','','',0), +(12283,'zhTW','真相大白','我們得搞清楚這裡發生了什麼事,所以需要將軍的日記!$B$B我很確定你可以在她宅邸二樓的床邊小桌上發現那本日記。宅邸就在教堂隔壁,獸欄和伐木場的對面。$B$B在你拿到日記以後,交給毒怨之地的高階執行官。$B$B你必須用老方法來辦這件事。鏡子的力量已經低到只夠讓我使用了。$B$B<探員奸笑著。>','史考莉探員要求你拿取大將軍阿比迪斯的日記,並將其交給毒怨之地的高階執行官羅思。','','到龍骨荒野的毒怨之地找高階執行官羅思。','','','','',0), +(12287,'zhTW','歐利克‧真心與遺民之濱','我凝視著寶珠、看見了你必經的道路,$n。$B$B多年以前,龍骨荒野海岸邊發生了一件慘劇。事件的本身依舊晦暗難明,但是我相信你的宿命就在南方,在遺民之濱的諸多廢墟之中。$B$B沿著東南方的路走下去,經過被蹂躪的城鎮廣場,順著路走,直到黎明之境。你會遇到我的一個老朋友,名叫歐利克‧真心。他在那兒等著你。','在龍骨荒野的黎明之境上,尋求歐利克‧真心的協助。','','','','','','',0), +(12297,'zhTW','叛徒與通敵','我手上的這份名單一定得要送到高階指揮官海弗德‧龍禍手中。他負責指揮第七軍團,駐紮在龍骨荒野東境的溫特加德要塞。$B$B如果這份名單上的任何一個叛徒駐紮在溫特加德...那就大事不妙了。$B$B快點,$n。把這份文件拿給格銳爾‧礦錘看,就在這鎮西要塞裡,然後向他解釋你的任務。他會讓你搭乘最快的獅鷲獸前往溫特加德!','亞當斯隊長要你把聯盟公文拿給鎮西要塞的格銳爾‧礦錘看。','','到凜風峽灣的鎮西要塞找格銳爾‧礦錘。','','','','',0), +(12306,'zhTW','每月啤酒俱樂部','$n,團結就是力量。成為「每月啤酒」俱樂部的一員,意味著你和你的同好隨時享用最青的啤酒。$b$b帶著你的「每月啤酒」俱樂部會員表格交給雷瑪,他就在奧格瑪裡。','帶著「每月啤酒」俱樂部會員表格交給奧格瑪裡的雷瑪。','','到奧格瑪找雷瑪。','','','','',0), +(12313,'zhTW','挽救啤酒節!','拯救美酒節!進入黑石深淵,與黑鐵酒吧的美酒節間諜談一談。','進入黑石深淵,與黑鐵酒吧的美酒節間諜談一談。','','','','','','',0), +(12318,'zhTW','拯救美酒節!','$n,我們的間諜發現了黑鐵矮人惡毒的陰謀!他們想破壞我們的美酒節慶典!$B$B科林·烈酒,這個釀酒者變節系列的頭領,糾集了黑石深淵的黑鐵矮人準備對付我們。他們的挖掘機經常朝我們發動攻擊。$B$B美酒節間諜已經潛入了位於黑石深淵深處的黑鐵酒吧。他應該知道關於科林的陰謀的更多資訊。$B$B找到這名間諜,拯救美酒節!','進入黑石深淵,與黑鐵酒吧的美酒節間諜談一談。','','','','','','',0), +(12416,'zhTW','戰火升溫','龍殿被天譴軍破壞殆盡,而龍殿的守護者,妲莉雅‧日觸,下落不明。在神殿中殞命的紅龍,如今成了扭曲的骸骨餘燼龍,將我們團團圍住,情勢危急,$r。$B$B伊斯肯達隊長帶領了一群汝等族類的小隊,在龍殿之外截斷天譴軍團的攻勢。$B$B前去幫助他...但請待在他的小隊後方。天譴軍團的攻勢源源不絕,我可不希望在那些滿是創痕的山谷中失去你。','幫助伊斯肯達隊長守護山谷,擊敗12隻嚴寒食屍鬼攻擊者、8隻嚴寒魂屍攻擊者,以及1隻嚴寒憎惡體攻擊者。事成以後,向賽利斯塔茲回報。','','到龍骨荒野的龍墳荒原找賽利斯塔茲。','','','','',0), +(12417,'zhTW','回歸大地','我還有另外一項需要注意的任務。我的兄弟不時在戰鬥中殞歿,如果我們不快點行動,他們只會助長餘燼龍的事例。$B$B從北方或南方的通道進入龍殿...這兩處的天譴軍應該比較薄弱。$B$B尋找從樹上掉落的晶紅橡實,並且將之種在我殞落的兄弟身旁。藉此,他們能夠帶來新生...這是吾族皆為之驕傲的命運。','從北方或南方的通道進入晶紅龍殿,尋找晶紅橡實。對戰死的紅龍使用晶紅橡實,使他們的軀體回歸大地。任務完成以後,向賽利斯塔茲回報。','','到龍骨荒野的龍墳荒原找賽利斯塔茲。','晶紅管理者回歸大地','','','',0), +(11486,'zhTW','美酒中的美酒','我知道像你這樣的$G小子:姑娘;最喜歡阻止衝突了,對吧?你想要讓你們的美酒節平平安安地辦完,不要出亂子。那麼我有一個提議……$B$B在這只酒杯中裝滿我們最棒的烈酒,然後把它交給鐵爐堡外的美酒節組織者,這樣我們黑鐵矮人也算是參加了這個節日!全艾澤拉斯最棒的烈酒來自這裡!黑石山的深處!不要忘了這一點!','將黑鐵啤酒杯交給丹莫羅的埃菲庫格·鐵桶。','','到丹莫洛找埃菲庫格·鐵桶。','','','','',0), +(11487,'zhTW','美酒中的美酒','我知道像你這樣的$G小子:姑娘;最喜歡阻止衝突了,對吧?你想要讓你們的美酒節平平安安地辦完,不要出亂子。那麼我有一個提議……$B$B在這只酒杯中裝滿我們最棒的烈酒,然後把它交給奧格瑞瑪外的美酒節組織者,這樣我們黑鐵矮人也算是參加了這個節日!全艾澤拉斯最棒的烈酒來自這裡!黑石山的深處!不要忘了這一點!','將黑鐵啤酒杯交給杜隆塔爾的塔波爾·斯威雷格。','','到杜洛塔找塔波爾·斯威雷格。','','','','',0), +(11490,'zhTW','占卜者的占卜','到殿堂去...使用寶珠。$b$b<提里斯喘息著。>$b$b快一點...一定要...阻止他們...在他們能夠...$b$b<提里斯在你面前死去。>','提里斯要你在博學者殿堂的陽臺使用寶珠。','','到博學者殿堂找卡雷苟斯。','啟動占卜寶珠','','','',0), +(11491,'zhTW','鐵符文傀儡和你:虛張聲勢','你隨時要有預備計畫,以免你的主要計畫發生意外!那就是為何我會將反智慧系統內建於傀儡中的原因。$B$B當你駕駛著閃亮新坐騎在巴爾古恩挖掘場時,可能會招致一些懷疑。這就是你的虛張聲勢技能存在的理由!你只要在被質問時,使用這個技能就可保你安然無恙。$B$B踩在雷布隆斯基的毯子上試試這個能力,一旦他開始咆哮生氣時,就使用虛張聲勢!$B$B等你準備好了,就爬上工作臺吧。','凜風峽灣,探險者協會前哨的渥特要你在踏上雷布隆斯基的地毯後,對他使用鐵符文傀儡的虛張聲勢技能。$B$B一旦完成任務後,你可以點選寵物視窗選擇解散來取消傀儡。','唬騙雷波斯基','到凜風峽灣的探險者協會前哨找渥特。','','','','',0), +(11513,'zhTW','攔截法力電池','我構思了一個計畫來取得足夠的能量來源,好製造我們的傳送門。$B$B劍刃山脈西北邊的以太族據點,貝許爾平臺,一直從風暴要塞走私法力電池到太陽之井,提供給凱爾薩斯。$B$B他們把法力電池都藏了起來,但有個方法可以讓你發現它們;某些以太族身上有相位裝置可以用來發覺這些走私貨物。$B$B使用相位裝置來找到這些法力電池,但小心他們的守衛。','找到10個走私的法力電池,然後把他們交給撒塔斯城,聖光露臺的主教納蘇安。','','到撒塔斯城的聖光露臺找主教納蘇安','','','','',0), +(11517,'zhTW','向納蘇安回報','我非常高興你在這裡,$c!現在我們拿下了聖所,我們得盡快建立一座魔法傳送門,從這裡直通到外域的撒塔斯城。$B$B我可敬的主上,主教納蘇安正在指揮進攻的事宜。任何能協助取得更多能量供給傳送門的人,他都希望我能派去給他。$B$B拜託,請前往撒塔斯城和他談談,他就在聖光露臺的阿達歐的房間。','商人波塔努斯請你和主教納蘇安談談,他就在撒塔斯城的聖光露臺。','','','','','','',0), +(11520,'zhTW','尋根','歡迎,$r。$b$b我怕這裡沒什麼能提供的,我得等我的試劑到貨。$b$b在我要求的補給中,其中有一種特別的試劑,刺棘根莖,特別難以取得。$b$b所幸,我知道一個秘密...$b$b使用刺棘撕掠者的腺體,可以控制劫毀者來挖掘那些根莖。$b$b要找到那些根莖,你要旅行到外域,從撒塔斯往東北飛,直到刺棘高地。','日境港的瑪納要你去外域的刺棘高地收集5個刺棘根莖,然後再回來找她。','','到奎爾丹納斯島的的日境港找瑪納。','','','','',0), +(11524,'zhTW','不穩定的運轉','我們缺乏人手來佔領日境聖所,但我有個計畫能夠把局面導向我們的目標。$B$B從太陽之井散溢出來的能量造成巡邏這座島嶼的哨兵幾乎發揮不了作用。控制他們機器運轉的水晶核已經受損到無法修復。$B$B我已經造出了一些新的水晶,能夠把控制權轉到我們手上,把水晶放入那些被擊敗的哨兵體內。我們需要所有可用的支援。','破碎之日會所的復仇者薩楊希望你將調諧水晶核放入5隻被擊倒的不穩定哨兵體內,將他們轉化為友軍。','','到奎爾丹納斯島的的破碎之日會所找復仇者薩楊。','部署轉化的哨衛','','','',0); + +-- +-- END UPDATING QUERIES +-- +UPDATE version_db_world SET date = '2022_03_14_01' WHERE sql_rev = '1646835034551886180'; +COMMIT; +END // +DELIMITER ; +CALL updateDb(); +DROP PROCEDURE IF EXISTS `updateDb`; diff --git a/data/sql/updates/db_world/2022_03_15_00.sql b/data/sql/updates/db_world/2022_03_15_00.sql new file mode 100644 index 000000000..77bd38bb9 --- /dev/null +++ b/data/sql/updates/db_world/2022_03_15_00.sql @@ -0,0 +1,31 @@ +-- DB update 2022_03_14_01 -> 2022_03_15_00 +DROP PROCEDURE IF EXISTS `updateDb`; +DELIMITER // +CREATE PROCEDURE updateDb () +proc:BEGIN DECLARE OK VARCHAR(100) DEFAULT 'FALSE'; +SELECT COUNT(*) INTO @COLEXISTS +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'version_db_world' AND COLUMN_NAME = '2022_03_14_01'; +IF @COLEXISTS = 0 THEN LEAVE proc; END IF; +START TRANSACTION; +ALTER TABLE version_db_world CHANGE COLUMN 2022_03_14_01 2022_03_15_00 bit; +SELECT sql_rev INTO OK FROM version_db_world WHERE sql_rev = '1645819759880724600'; IF OK <> 'FALSE' THEN LEAVE proc; END IF; +-- +-- START UPDATING QUERIES +-- + +INSERT INTO `version_db_world` (`sql_rev`) VALUES ('1645819759880724600'); + +DELETE FROM `acore_string` WHERE `entry` = 726; +INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES +(726, '|cffff0000[Arena Queue]:|r %s (skirmish %s) -- [%u-%u] [%u/%u]|r', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +-- +-- END UPDATING QUERIES +-- +UPDATE version_db_world SET date = '2022_03_15_00' WHERE sql_rev = '1645819759880724600'; +COMMIT; +END // +DELIMITER ; +CALL updateDb(); +DROP PROCEDURE IF EXISTS `updateDb`; diff --git a/data/sql/updates/db_world/2022_03_16_00.sql b/data/sql/updates/db_world/2022_03_16_00.sql new file mode 100644 index 000000000..d1fd8f06d --- /dev/null +++ b/data/sql/updates/db_world/2022_03_16_00.sql @@ -0,0 +1,31 @@ +-- DB update 2022_03_15_00 -> 2022_03_16_00 +DROP PROCEDURE IF EXISTS `updateDb`; +DELIMITER // +CREATE PROCEDURE updateDb () +proc:BEGIN DECLARE OK VARCHAR(100) DEFAULT 'FALSE'; +SELECT COUNT(*) INTO @COLEXISTS +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'version_db_world' AND COLUMN_NAME = '2022_03_15_00'; +IF @COLEXISTS = 0 THEN LEAVE proc; END IF; +START TRANSACTION; +ALTER TABLE version_db_world CHANGE COLUMN 2022_03_15_00 2022_03_16_00 bit; +SELECT sql_rev INTO OK FROM version_db_world WHERE sql_rev = '1647185908361212000'; IF OK <> 'FALSE' THEN LEAVE proc; END IF; +-- +-- START UPDATING QUERIES +-- + +INSERT INTO `version_db_world` (`sql_rev`) VALUES ('1647185908361212000'); + +DELETE FROM `spell_script_names` WHERE `spell_id`=45831; +INSERT INTO `spell_script_names` VALUES +(45831,'spell_gen_av_drekthar_presence'); + +-- +-- END UPDATING QUERIES +-- +UPDATE version_db_world SET date = '2022_03_16_00' WHERE sql_rev = '1647185908361212000'; +COMMIT; +END // +DELIMITER ; +CALL updateDb(); +DROP PROCEDURE IF EXISTS `updateDb`; diff --git a/data/sql/updates/db_world/2022_03_16_01.sql b/data/sql/updates/db_world/2022_03_16_01.sql new file mode 100644 index 000000000..e7911e42a --- /dev/null +++ b/data/sql/updates/db_world/2022_03_16_01.sql @@ -0,0 +1,29 @@ +-- DB update 2022_03_16_00 -> 2022_03_16_01 +DROP PROCEDURE IF EXISTS `updateDb`; +DELIMITER // +CREATE PROCEDURE updateDb () +proc:BEGIN DECLARE OK VARCHAR(100) DEFAULT 'FALSE'; +SELECT COUNT(*) INTO @COLEXISTS +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'version_db_world' AND COLUMN_NAME = '2022_03_16_00'; +IF @COLEXISTS = 0 THEN LEAVE proc; END IF; +START TRANSACTION; +ALTER TABLE version_db_world CHANGE COLUMN 2022_03_16_00 2022_03_16_01 bit; +SELECT sql_rev INTO OK FROM version_db_world WHERE sql_rev = '1647107378248226200'; IF OK <> 'FALSE' THEN LEAVE proc; END IF; +-- +-- START UPDATING QUERIES +-- + +INSERT INTO `version_db_world` (`sql_rev`) VALUES ('1647107378248226200'); + +UPDATE `smart_scripts` SET `target_type`=1 WHERE `entryorguid`=30154 AND `source_type`=0 AND `id` IN (1,4); + +-- +-- END UPDATING QUERIES +-- +UPDATE version_db_world SET date = '2022_03_16_01' WHERE sql_rev = '1647107378248226200'; +COMMIT; +END // +DELIMITER ; +CALL updateDb(); +DROP PROCEDURE IF EXISTS `updateDb`; diff --git a/src/common/Debugging/Errors.h b/src/common/Debugging/Errors.h index fec93713d..0155d0ddb 100644 --- a/src/common/Debugging/Errors.h +++ b/src/common/Debugging/Errors.h @@ -56,7 +56,7 @@ AC_COMMON_API std::string GetDebugInfo(); #define WPAssert(cond, ...) do { if (!(cond)) Acore::Assert(__FILE__, __LINE__, __FUNCTION__, GetDebugInfo(), #cond, ##__VA_ARGS__); } while(0) #define WPAssert_NODEBUGINFO(cond) do { if (!(cond)) Acore::Assert(__FILE__, __LINE__, __FUNCTION__, "", #cond); } while(0) -#define WPFatal(cond, ...) do { if (!(cond)) Acore::Fatal(__FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__); } while(0) +#define WPFatal(cond, ...) do { if (!(cond)) Acore::Fatal(__FILE__, __LINE__, __FUNCTION__, #cond, ##__VA_ARGS__); } while(0) #define WPError(cond, msg) do { if (!(cond)) Acore::Error(__FILE__, __LINE__, __FUNCTION__, (msg)); } while(0) #define WPWarning(cond, msg) do { if (!(cond)) Acore::Warning(__FILE__, __LINE__, __FUNCTION__, (msg)); } while(0) #define WPAbort(...) do { Acore::Abort(__FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__); } while(0) diff --git a/src/server/database/Database/Implementation/LoginDatabase.cpp b/src/server/database/Database/Implementation/LoginDatabase.cpp index 924a0b495..726895276 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.cpp +++ b/src/server/database/Database/Implementation/LoginDatabase.cpp @@ -75,9 +75,8 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_DEL_IP_NOT_BANNED, "DELETE FROM ip_banned WHERE ip = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_INS_ACCOUNT_BANNED, "INSERT INTO account_banned VALUES (?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()+?, ?, ?, 1)", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_ACCOUNT_NOT_BANNED, "UPDATE account_banned SET active = 0 WHERE id = ? AND active != 0", CONNECTION_ASYNC); - PrepareStatement(LOGIN_DEL_REALM_CHARACTERS_BY_REALM, "DELETE FROM realmcharacters WHERE acctid = ? AND realmid = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_DEL_REALM_CHARACTERS, "DELETE FROM realmcharacters WHERE acctid = ?", CONNECTION_ASYNC); - PrepareStatement(LOGIN_INS_REALM_CHARACTERS, "INSERT INTO realmcharacters (numchars, acctid, realmid) VALUES (?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(LOGIN_REP_REALM_CHARACTERS, "REPLACE INTO realmcharacters (numchars, acctid, realmid) VALUES (?, ?, ?)", CONNECTION_ASYNC); PrepareStatement(LOGIN_SEL_SUM_REALM_CHARACTERS, "SELECT SUM(numchars) FROM realmcharacters WHERE acctid = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_INS_ACCOUNT, "INSERT INTO account(username, salt, verifier, expansion, joindate) VALUES(?, ?, ?, ?, NOW())", CONNECTION_ASYNC); PrepareStatement(LOGIN_INS_REALM_CHARACTERS_INIT, "INSERT INTO realmcharacters (realmid, acctid, numchars) SELECT realmlist.id, account.id, 0 FROM realmlist, account LEFT JOIN realmcharacters ON acctid=account.id WHERE acctid IS NULL", CONNECTION_ASYNC); diff --git a/src/server/database/Database/Implementation/LoginDatabase.h b/src/server/database/Database/Implementation/LoginDatabase.h index 85fada57c..3a9bb4a82 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.h +++ b/src/server/database/Database/Implementation/LoginDatabase.h @@ -59,9 +59,8 @@ enum LoginDatabaseStatements : uint32 LOGIN_SEL_ACCOUNT_BY_ID, LOGIN_INS_ACCOUNT_BANNED, LOGIN_UPD_ACCOUNT_NOT_BANNED, - LOGIN_DEL_REALM_CHARACTERS_BY_REALM, LOGIN_DEL_REALM_CHARACTERS, - LOGIN_INS_REALM_CHARACTERS, + LOGIN_REP_REALM_CHARACTERS, LOGIN_SEL_SUM_REALM_CHARACTERS, LOGIN_INS_ACCOUNT, LOGIN_INS_REALM_CHARACTERS_INIT, diff --git a/src/server/game/AI/SmartScripts/SmartScript.cpp b/src/server/game/AI/SmartScripts/SmartScript.cpp index 2746e7a74..76c6d2edd 100644 --- a/src/server/game/AI/SmartScripts/SmartScript.cpp +++ b/src/server/game/AI/SmartScripts/SmartScript.cpp @@ -3844,6 +3844,10 @@ ObjectList* SmartScript::GetTargets(SmartScriptHolder const& e, Unit* invoker /* { l->push_back(owner); } + else if (me->IsSummon() && me->ToTempSummon()->GetSummonerUnit()) + { + l->push_back(me->ToTempSummon()->GetSummonerUnit()); + } } else if (go) { diff --git a/src/server/game/ArenaSpectator/ArenaSpectator.cpp b/src/server/game/ArenaSpectator/ArenaSpectator.cpp index cc31c7d8a..6048b7636 100644 --- a/src/server/game/ArenaSpectator/ArenaSpectator.cpp +++ b/src/server/game/ArenaSpectator/ArenaSpectator.cpp @@ -137,7 +137,7 @@ bool ArenaSpectator::HandleSpectatorSpectateCommand(ChatHandler* handler, std::s if (uint32 inviteInstanceId = player->GetPendingSpectatorInviteInstanceId()) { - if (Battleground* tbg = sBattlegroundMgr->GetBattleground(inviteInstanceId)) + if (Battleground* tbg = sBattlegroundMgr->GetBattleground(inviteInstanceId, BATTLEGROUND_TYPE_NONE)) tbg->RemoveToBeTeleported(player->GetGUID()); player->SetPendingSpectatorInviteInstanceId(0); } diff --git a/src/server/game/Battlegrounds/Arena.cpp b/src/server/game/Battlegrounds/Arena.cpp index fe6e7a439..dfb84271d 100644 --- a/src/server/game/Battlegrounds/Arena.cpp +++ b/src/server/game/Battlegrounds/Arena.cpp @@ -312,6 +312,10 @@ void Arena::EndBattleground(TeamId winnerTeamId) } } + // update previous opponents for arena queue + winnerArenaTeam->SetPreviousOpponents(loserArenaTeam->GetId()); + loserArenaTeam->SetPreviousOpponents(winnerArenaTeam->GetId()); + // save the stat changes if (bValidArena) { diff --git a/src/server/game/Battlegrounds/ArenaScore.h b/src/server/game/Battlegrounds/ArenaScore.h index 0528d4830..b51dacf95 100644 --- a/src/server/game/Battlegrounds/ArenaScore.h +++ b/src/server/game/Battlegrounds/ArenaScore.h @@ -61,7 +61,7 @@ protected: { RatingChange = ratingChange; MatchmakerRating = matchMakerRating; - TeamName = teamName; + TeamName = std::string(teamName); } void BuildRatingInfoBlock(WorldPacket& data); diff --git a/src/server/game/Battlegrounds/ArenaTeam.cpp b/src/server/game/Battlegrounds/ArenaTeam.cpp index c710c9cc9..7b67b68f8 100644 --- a/src/server/game/Battlegrounds/ArenaTeam.cpp +++ b/src/server/game/Battlegrounds/ArenaTeam.cpp @@ -345,7 +345,7 @@ void ArenaTeam::DelMember(ObjectGuid guid, bool cleanDb) WorldPacket data; playerMember->RemoveBattlegroundQueueId(bgQueue); sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, nullptr, playerMember->GetBattlegroundQueueIndex(bgQueue), STATUS_NONE, 0, 0, 0, TEAM_NEUTRAL); - queue.RemovePlayer(playerMember->GetGUID(), true, 0); + queue.RemovePlayer(playerMember->GetGUID(), true); playerMember->GetSession()->SendPacket(&data); } } diff --git a/src/server/game/Battlegrounds/ArenaTeam.h b/src/server/game/Battlegrounds/ArenaTeam.h index 316737727..fdad00fd5 100644 --- a/src/server/game/Battlegrounds/ArenaTeam.h +++ b/src/server/game/Battlegrounds/ArenaTeam.h @@ -209,6 +209,9 @@ public: void FinishWeek(); void FinishGame(int32 mod, const Map* bgMap); + void SetPreviousOpponents(uint32 arenaTeamId) { PreviousOpponents = arenaTeamId; } + uint32 GetPreviousOpponents() { return PreviousOpponents; } + void CreateTempArenaTeam(std::vector playerList, uint8 type, std::string const& teamName); // Containers @@ -229,5 +232,7 @@ protected: MemberList Members; ArenaTeamStats Stats; + + uint32 PreviousOpponents = 0; }; #endif diff --git a/src/server/game/Battlegrounds/Battleground.cpp b/src/server/game/Battlegrounds/Battleground.cpp index 35c65b278..3ac6b1922 100644 --- a/src/server/game/Battlegrounds/Battleground.cpp +++ b/src/server/game/Battlegrounds/Battleground.cpp @@ -232,6 +232,9 @@ Battleground::~Battleground() m_Map = nullptr; } + // remove from bg free slot queue + RemoveFromBGFreeSlotQueue(); + for (auto const& itr : PlayerScores) delete itr.second; } @@ -251,8 +254,21 @@ void Battleground::Update(uint32 diff) if (!GetPlayersSize()) { + //BG is empty + // if there are no players invited, delete BG + // this will delete arena or bg object, where any player entered + // [[ but if you use battleground object again (more battles possible to be played on 1 instance) + // then this condition should be removed and code: + // if (!GetInvitedCount(TEAM_HORDE) && !GetInvitedCount(TEAM_ALLIANCE)) + // AddToFreeBGObjectsQueue(); // not yet implemented + // should be used instead of current + // ]] + // Battleground Template instance cannot be updated, because it would be deleted if (!GetInvitedCount(TEAM_HORDE) && !GetInvitedCount(TEAM_ALLIANCE)) + { m_SetDeleteThis = true; + } + return; } @@ -768,6 +784,7 @@ void Battleground::EndBattleground(PvPTeamId winnerTeamId) if (GetStatus() == STATUS_WAIT_LEAVE) return; + RemoveFromBGFreeSlotQueue(); SetStatus(STATUS_WAIT_LEAVE); SetWinner(winnerTeamId); @@ -956,13 +973,17 @@ void Battleground::RemovePlayerAtLeave(Player* player) // if the player was a match participant if (participant) { - WorldPacket data; - player->ClearAfkReports(); + WorldPacket data; sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, this, player->GetCurrentBattlegroundQueueSlot(), STATUS_NONE, 0, 0, 0, TEAM_NEUTRAL); player->GetSession()->SendPacket(&data); + BattlegroundQueueTypeId bgQueueTypeId = BattlegroundMgr::BGQueueTypeId(GetBgTypeID(), GetArenaType()); + + // this call is important, because player, when joins to battleground, this method is not called, so it must be called when leaving bg + player->RemoveBattlegroundQueueId(bgQueueTypeId); + // remove from raid group if player is member if (Group* group = GetBgRaid(teamId)) if (group->IsMember(player->GetGUID())) @@ -977,6 +998,19 @@ void Battleground::RemovePlayerAtLeave(Player* player) if (isBattleground() && !player->IsGameMaster() && sWorld->getBoolConfig(CONFIG_BATTLEGROUND_CAST_DESERTER)) if (status == STATUS_IN_PROGRESS || status == STATUS_WAIT_JOIN) player->ScheduleDelayedOperation(DELAYED_SPELL_CAST_DESERTER); + + DecreaseInvitedCount(teamId); + + //we should update battleground queue, but only if bg isn't ending + if (isBattleground() && GetStatus() < STATUS_WAIT_LEAVE) + { + BattlegroundTypeId bgTypeId = GetBgTypeID(); + BattlegroundQueueTypeId bgQueueTypeId = BattlegroundMgr::BGQueueTypeId(GetBgTypeID(), GetArenaType()); + + // a player has left the battleground, so there are free slots -> add to queue + AddToBGFreeSlotQueue(); + sBattlegroundMgr->ScheduleQueueUpdate(0, 0, bgQueueTypeId, bgTypeId, GetBracketId()); + } } // Remove shapeshift auras @@ -1009,6 +1043,7 @@ void Battleground::Init() m_BgInvitedPlayers[TEAM_ALLIANCE] = 0; m_BgInvitedPlayers[TEAM_HORDE] = 0; + _InBGFreeSlotQueue = false; m_Players.clear(); @@ -1028,9 +1063,15 @@ void Battleground::StartBattleground() SetStartTime(0); SetLastResurrectTime(0); + // add BG to free slot queue + AddToBGFreeSlotQueue(); + // add bg to update list // this must be done here, because we need to have already invited some players when first Battleground::Update() method is executed sBattlegroundMgr->AddBattleground(this); + + if (m_IsRated) + LOG_DEBUG("bg.arena", "Arena match type: {} for Team1Id: {} - Team2Id: {} started.", m_ArenaType, m_ArenaTeamIds[TEAM_ALLIANCE], m_ArenaTeamIds[TEAM_HORDE]); } void Battleground::AddPlayer(Player* player) @@ -1129,6 +1170,26 @@ void Battleground::AddOrSetPlayerToCorrectBgGroup(Player* player, TeamId teamId) } } +// This method should be called only once ... it adds pointer to queue +void Battleground::AddToBGFreeSlotQueue() +{ + if (!_InBGFreeSlotQueue && isBattleground()) + { + sBattlegroundMgr->AddToBGFreeSlotQueue(m_RealTypeID, this); + _InBGFreeSlotQueue = true; + } +} + +// This method removes this battleground from free queue - it must be called when deleting battleground +void Battleground::RemoveFromBGFreeSlotQueue() +{ + if (_InBGFreeSlotQueue) + { + sBattlegroundMgr->RemoveFromBGFreeSlotQueue(m_RealTypeID, m_InstanceID); + _InBGFreeSlotQueue = false; + } +} + uint32 Battleground::GetFreeSlotsForTeam(TeamId teamId) const { if (!(GetStatus() == STATUS_IN_PROGRESS || GetStatus() == STATUS_WAIT_JOIN)) @@ -1611,6 +1672,7 @@ void Battleground::SendMessage2ToAll(uint32 entry, ChatMsg type, Player const* s void Battleground::EndNow() { + RemoveFromBGFreeSlotQueue(); SetStatus(STATUS_WAIT_LEAVE); SetEndTime(0); } @@ -1777,6 +1839,12 @@ GraveyardStruct const* Battleground::GetClosestGraveyard(Player* player) return sGraveyard->GetClosestGraveyard(player, player->GetBgTeamId()); } +void Battleground::SetBracket(PvPDifficultyEntry const* bracketEntry) +{ + m_BracketId = bracketEntry->GetBracketId(); + SetLevelRange(bracketEntry->minLevel, bracketEntry->maxLevel); +} + void Battleground::StartTimedAchievement(AchievementCriteriaTimedTypes type, uint32 entry) { for (BattlegroundPlayerMap::const_iterator itr = GetPlayers().begin(); itr != GetPlayers().end(); ++itr) diff --git a/src/server/game/Battlegrounds/Battleground.h b/src/server/game/Battlegrounds/Battleground.h index 5f4b2709a..1777699d4 100644 --- a/src/server/game/Battlegrounds/Battleground.h +++ b/src/server/game/Battlegrounds/Battleground.h @@ -316,6 +316,7 @@ public: // Get methods: [[nodiscard]] std::string GetName() const { return m_Name; } [[nodiscard]] BattlegroundTypeId GetBgTypeID(bool GetRandom = false) const { return GetRandom ? m_RandomTypeID : m_RealTypeID; } + [[nodiscard]] BattlegroundBracketId GetBracketId() const { return m_BracketId; } [[nodiscard]] uint32 GetInstanceID() const { return m_InstanceID; } [[nodiscard]] BattlegroundStatus GetStatus() const { return m_Status; } [[nodiscard]] uint32 GetClientInstanceID() const { return m_ClientInstanceID; } @@ -341,6 +342,7 @@ public: void SetName(std::string_view name) { m_Name = std::string(name); } void SetBgTypeID(BattlegroundTypeId TypeID) { m_RealTypeID = TypeID; } void SetRandomTypeID(BattlegroundTypeId TypeID) { m_RandomTypeID = TypeID; } + void SetBracket(PvPDifficultyEntry const* bracketEntry); void SetInstanceID(uint32 InstanceID) { m_InstanceID = InstanceID; } void SetStatus(BattlegroundStatus Status) { m_Status = Status; } void SetClientInstanceID(uint32 InstanceID) { m_ClientInstanceID = InstanceID; } @@ -361,6 +363,9 @@ public: void SetMaxPlayersPerTeam(uint32 MaxPlayers) { m_MaxPlayersPerTeam = MaxPlayers; } void SetMinPlayersPerTeam(uint32 MinPlayers) { m_MinPlayersPerTeam = MinPlayers; } + void AddToBGFreeSlotQueue(); // this queue will be useful when more battlegrounds instances will be available + void RemoveFromBGFreeSlotQueue(); // this method could delete whole BG instance, if another free is available + void DecreaseInvitedCount(TeamId teamId) { if (m_BgInvitedPlayers[teamId]) --m_BgInvitedPlayers[teamId]; } void IncreaseInvitedCount(TeamId teamId) { ++m_BgInvitedPlayers[teamId]; } [[nodiscard]] uint32 GetInvitedCount(TeamId teamId) const { return m_BgInvitedPlayers[teamId]; } @@ -642,7 +647,9 @@ private: uint32 m_ValidStartPositionTimer; int32 m_EndTime; // it is set to 120000 when bg is ending and it decreases itself uint32 m_LastResurrectTime; + BattlegroundBracketId m_BracketId{ BG_BRACKET_ID_FIRST }; uint8 m_ArenaType; // 2=2v2, 3=3v3, 5=5v5 + bool _InBGFreeSlotQueue{ false }; // used to make sure that BG is only once inserted into the BattlegroundMgr.BGFreeSlotQueue[bgTypeId] deque bool m_SetDeleteThis; // used for safe deletion of the bg after end / all players leave bool m_IsArena; PvPTeamId m_WinnerId; diff --git a/src/server/game/Battlegrounds/BattlegroundMgr.cpp b/src/server/game/Battlegrounds/BattlegroundMgr.cpp index fbb612410..84caf57b1 100644 --- a/src/server/game/Battlegrounds/BattlegroundMgr.cpp +++ b/src/server/game/Battlegrounds/BattlegroundMgr.cpp @@ -48,15 +48,22 @@ #include "WorldPacket.h" #include +bool BattlegroundTemplate::IsArena() const +{ + return BattlemasterEntry->type == MAP_ARENA; +} + /*********************************************************/ /*** BATTLEGROUND MANAGER ***/ /*********************************************************/ -BattlegroundMgr::BattlegroundMgr() : m_ArenaTesting(false), m_Testing(false), - m_lastClientVisibleInstanceId(0), m_NextAutoDistributionTime(0), m_AutoDistributionTimeChecker(0), m_NextPeriodicQueueUpdateTime(5 * IN_MILLISECONDS) +BattlegroundMgr::BattlegroundMgr() : + m_ArenaTesting(false), + m_Testing(false), + m_NextAutoDistributionTime(0), + m_AutoDistributionTimeChecker(0), + m_NextPeriodicQueueUpdateTime(5 * IN_MILLISECONDS) { - for (uint32 qtype = BATTLEGROUND_QUEUE_NONE; qtype < MAX_BATTLEGROUND_QUEUE_TYPES; ++qtype) - m_BattlegroundQueues[qtype].SetBgTypeIdAndArenaType(BGTemplateId(BattlegroundQueueTypeId(qtype)), BGArenaType(BattlegroundQueueTypeId(qtype))); } BattlegroundMgr::~BattlegroundMgr() @@ -72,47 +79,68 @@ BattlegroundMgr* BattlegroundMgr::instance() void BattlegroundMgr::DeleteAllBattlegrounds() { - while (!m_Battlegrounds.empty()) - delete m_Battlegrounds.begin()->second; - m_Battlegrounds.clear(); + for (auto& [_, data] : bgDataStore) + { + while (!data._Battlegrounds.empty()) + delete data._Battlegrounds.begin()->second; - while (!m_BattlegroundTemplates.empty()) - delete m_BattlegroundTemplates.begin()->second; - m_BattlegroundTemplates.clear(); + data._Battlegrounds.clear(); + + while (!data.BGFreeSlotQueue.empty()) + delete data.BGFreeSlotQueue.front(); + } + + bgDataStore.clear(); } // used to update running battlegrounds, and delete finished ones void BattlegroundMgr::Update(uint32 diff) { // update all battlegrounds and delete if needed - for (BattlegroundContainer::iterator itr = m_Battlegrounds.begin(), itrDelete; itr != m_Battlegrounds.end(); ) + for (auto& [_, bgData] : bgDataStore) { - itrDelete = itr++; - Battleground* bg = itrDelete->second; - bg->Update(diff); - if (bg->ToBeDeleted()) + auto& bgList = bgData._Battlegrounds; + auto itrDelete = bgList.begin(); + + // first one is template and should not be deleted + for (BattlegroundContainer::iterator itr = ++itrDelete; itr != bgList.end();) { - itrDelete->second = nullptr; - m_Battlegrounds.erase(itrDelete); - delete bg; + itrDelete = itr++; + Battleground* bg = itrDelete->second; + + bg->Update(diff); + if (bg->ToBeDeleted()) + { + itrDelete->second = nullptr; + bgList.erase(itrDelete); + + BattlegroundClientIdsContainer& clients = bgData._ClientBattlegroundIds[bg->GetBracketId()]; + if (!clients.empty()) + clients.erase(bg->GetClientInstanceID()); + + delete bg; + } } } // update events - for (int qtype = BATTLEGROUND_QUEUE_NONE; qtype < MAX_BATTLEGROUND_QUEUE_TYPES; ++qtype) + for (uint8 qtype = BATTLEGROUND_QUEUE_NONE; qtype < MAX_BATTLEGROUND_QUEUE_TYPES; ++qtype) m_BattlegroundQueues[qtype].UpdateEvents(diff); // update using scheduled tasks (used only for rated arenas, initial opponent search works differently than periodic queue update) - if (!m_ArenaQueueUpdateScheduler.empty()) + if (!m_QueueUpdateScheduler.empty()) { std::vector scheduled; - std::swap(scheduled, m_ArenaQueueUpdateScheduler); + std::swap(scheduled, m_QueueUpdateScheduler); + for (uint8 i = 0; i < scheduled.size(); i++) { - uint32 arenaRatedTeamId = scheduled[i] >> 32; + uint32 arenaMMRating = scheduled[i] >> 32; + uint8 arenaType = scheduled[i] >> 24 & 255; BattlegroundQueueTypeId bgQueueTypeId = BattlegroundQueueTypeId(scheduled[i] >> 16 & 255); + BattlegroundTypeId bgTypeId = BattlegroundTypeId((scheduled[i] >> 8) & 255); BattlegroundBracketId bracket_id = BattlegroundBracketId(scheduled[i] & 255); - m_BattlegroundQueues[bgQueueTypeId].BattlegroundQueueUpdate(diff, bracket_id, true, arenaRatedTeamId); // pussywizard: looking for opponents only for this team + m_BattlegroundQueues[bgQueueTypeId].BattlegroundQueueUpdate(diff, bgTypeId, bracket_id, arenaType, arenaMMRating > 0, arenaMMRating); } } @@ -121,16 +149,16 @@ void BattlegroundMgr::Update(uint32 diff) { m_NextPeriodicQueueUpdateTime = 5 * IN_MILLISECONDS; + LOG_TRACE("bg.arena", "BattlegroundMgr: UPDATING ARENA QUEUES"); + // for rated arenas for (uint32 qtype = BATTLEGROUND_QUEUE_2v2; qtype < MAX_BATTLEGROUND_QUEUE_TYPES; ++qtype) + { for (uint32 bracket = BG_BRACKET_ID_FIRST; bracket < MAX_BATTLEGROUND_BRACKETS; ++bracket) - m_BattlegroundQueues[qtype].BattlegroundQueueUpdate(m_NextPeriodicQueueUpdateTime, BattlegroundBracketId(bracket), true, 0); // pussywizard: 0 for rated means looking for opponents for every team - - // for battlegrounds and not rated arenas - // in first loop try to fill already running battlegrounds, then in a second loop try to create new battlegrounds - for (uint32 qtype = BATTLEGROUND_QUEUE_AV; qtype < MAX_BATTLEGROUND_QUEUE_TYPES; ++qtype) - for (uint32 bracket = BG_BRACKET_ID_FIRST; bracket < MAX_BATTLEGROUND_BRACKETS; ++bracket) - m_BattlegroundQueues[qtype].BattlegroundQueueUpdate(m_NextPeriodicQueueUpdateTime, BattlegroundBracketId(bracket), false, 0); + { + m_BattlegroundQueues[qtype].BattlegroundQueueUpdate(diff, BATTLEGROUND_AA, BattlegroundBracketId(bracket), BattlegroundMgr::BGArenaType(BattlegroundQueueTypeId(qtype)), true, 0); + } + } } else m_NextPeriodicQueueUpdateTime -= diff; @@ -225,36 +253,108 @@ void BattlegroundMgr::BuildPlayerJoinedBattlegroundPacket(WorldPacket* data, Pla *data << player->GetGUID(); } -Battleground* BattlegroundMgr::GetBattleground(uint32 instanceId) +Battleground* BattlegroundMgr::GetBattlegroundThroughClientInstance(uint32 instanceId, BattlegroundTypeId bgTypeId) +{ + //cause at HandleBattlegroundJoinOpcode the clients sends the instanceid he gets from + //SMSG_BATTLEFIELD_LIST we need to find the battleground with this clientinstance-id + Battleground* bg = GetBattlegroundTemplate(bgTypeId); + if (!bg) + return nullptr; + + if (bg->isArena()) + return GetBattleground(instanceId, bgTypeId); + + auto const& it = bgDataStore.find(bgTypeId); + if (it == bgDataStore.end()) + return nullptr; + + for (auto const& itr : it->second._Battlegrounds) + { + if (itr.second->GetClientInstanceID() == instanceId) + return itr.second; + } + + return nullptr; +} + +Battleground* BattlegroundMgr::GetBattleground(uint32 instanceId, BattlegroundTypeId bgTypeId) { if (!instanceId) return nullptr; - BattlegroundContainer::const_iterator itr = m_Battlegrounds.find(instanceId); - if (itr != m_Battlegrounds.end()) - return itr->second; + auto GetBgWithInstanceID = [instanceId](BattlegroundData const* bgData) -> Battleground* + { + auto const& itr = bgData->_Battlegrounds.find(instanceId); + if (itr != bgData->_Battlegrounds.end()) + return itr->second; + + return nullptr; + }; + + if (bgTypeId == BATTLEGROUND_TYPE_NONE) + { + for (auto const& [bgType, bgData] : bgDataStore) + { + if (auto bg = GetBgWithInstanceID(&bgData)) + return bg; + } + } + else + { + auto const& itr = bgDataStore.find(bgTypeId); + if (itr == bgDataStore.end()) + return nullptr; + + if (auto bg = GetBgWithInstanceID(&itr->second)) + return bg; + } return nullptr; } Battleground* BattlegroundMgr::GetBattlegroundTemplate(BattlegroundTypeId bgTypeId) { - BattlegroundTemplateContainer::const_iterator itr = m_BattlegroundTemplates.find(bgTypeId); - if (itr != m_BattlegroundTemplates.end()) - return itr->second; + BattlegroundDataContainer::const_iterator itr = bgDataStore.find(bgTypeId); + if (itr == bgDataStore.end()) + return nullptr; - return nullptr; + BattlegroundContainer const& bgs = itr->second._Battlegrounds; + + // map is sorted and we can be sure that lowest instance id has only BG template + return bgs.empty() ? nullptr : bgs.begin()->second; } -uint32 BattlegroundMgr::GetNextClientVisibleInstanceId() +uint32 BattlegroundMgr::CreateClientVisibleInstanceId(BattlegroundTypeId bgTypeId, BattlegroundBracketId bracket_id) { - return ++m_lastClientVisibleInstanceId; + if (IsArenaType(bgTypeId)) + return 0; // arenas don't have client-instanceids + + // we create here an instanceid, which is just for + // displaying this to the client and without any other use.. + // the client-instanceIds are unique for each battleground-type + // the instance-id just needs to be as low as possible, beginning with 1 + // the following works, because std::set is default ordered with "<" + // the optimalization would be to use as bitmask std::vector - but that would only make code unreadable + + BattlegroundClientIdsContainer& clientIds = bgDataStore[bgTypeId]._ClientBattlegroundIds[bracket_id]; + uint32 lastId = 0; + + for (BattlegroundClientIdsContainer::const_iterator itr = clientIds.begin(); itr != clientIds.end();) + { + if ((++lastId) != *itr) // if there is a gap between the ids, we will break.. + break; + + lastId = *itr; + } + + clientIds.emplace(++lastId); + return lastId; } // create a new battleground that will really be used to play -Battleground* BattlegroundMgr::CreateNewBattleground(BattlegroundTypeId originalBgTypeId, uint32 minLevel, uint32 maxLevel, uint8 arenaType, bool isRated) +Battleground* BattlegroundMgr::CreateNewBattleground(BattlegroundTypeId originalBgTypeId, PvPDifficultyEntry const* bracketEntry, uint8 arenaType, bool isRated) { - BattlegroundTypeId bgTypeId = GetRandomBG(originalBgTypeId, minLevel); + BattlegroundTypeId bgTypeId = GetRandomBG(originalBgTypeId, bracketEntry->minLevel); if (originalBgTypeId == BATTLEGROUND_AA) originalBgTypeId = bgTypeId; @@ -262,9 +362,13 @@ Battleground* BattlegroundMgr::CreateNewBattleground(BattlegroundTypeId original // get the template BG Battleground* bg_template = GetBattlegroundTemplate(bgTypeId); if (!bg_template) + { + LOG_ERROR("bg.battleground", "Battleground: CreateNewBattleground - bg template not found for {}", bgTypeId); return nullptr; + } Battleground* bg = nullptr; + // create a copy of the BG template if (BattlegroundMgr::bgTypeToTemplate.find(bgTypeId) == BattlegroundMgr::bgTypeToTemplate.end()) { @@ -275,9 +379,9 @@ Battleground* BattlegroundMgr::CreateNewBattleground(BattlegroundTypeId original bool isRandom = bgTypeId != originalBgTypeId && !bg->isArena(); - bg->SetLevelRange(minLevel, maxLevel); + bg->SetBracket(bracketEntry); bg->SetInstanceID(sMapMgr->GenerateInstanceId()); - bg->SetClientInstanceID(IsArenaType(originalBgTypeId) ? 0 : GetNextClientVisibleInstanceId()); + bg->SetClientInstanceID(CreateClientVisibleInstanceId(originalBgTypeId, bracketEntry->GetBracketId())); bg->Init(); bg->SetStatus(STATUS_WAIT_JOIN); // start the joining of the bg bg->SetArenaType(arenaType); @@ -298,37 +402,40 @@ Battleground* BattlegroundMgr::CreateNewBattleground(BattlegroundTypeId original } // used to create the BG templates -bool BattlegroundMgr::CreateBattleground(CreateBattlegroundData& data) +bool BattlegroundMgr::CreateBattleground(BattlegroundTemplate const* bgTemplate) { // Create the BG - Battleground* bg = nullptr; - bg = BattlegroundMgr::bgtypeToBattleground[data.bgTypeId]; + Battleground* bg = GetBattlegroundTemplate(bgTemplate->Id); - if (bg == nullptr) - return false; + if (!bg) + { + bg = BattlegroundMgr::bgtypeToBattleground[bgTemplate->Id]; - if (data.bgTypeId == BATTLEGROUND_RB) - bg->SetRandom(true); + ASSERT(bg); - bg->SetMapId(data.MapID); - bg->SetBgTypeID(data.bgTypeId); - bg->SetInstanceID(0); - bg->SetArenaorBGType(data.IsArena); - bg->SetMinPlayersPerTeam(data.MinPlayersPerTeam); - bg->SetMaxPlayersPerTeam(data.MaxPlayersPerTeam); - bg->SetName(data.BattlegroundName); - bg->SetTeamStartPosition(TEAM_ALLIANCE, data.StartLocation[TEAM_ALLIANCE]); - bg->SetTeamStartPosition(TEAM_HORDE, data.StartLocation[TEAM_HORDE]); - bg->SetStartMaxDist(data.StartMaxDist); - bg->SetLevelRange(data.LevelMin, data.LevelMax); - bg->SetScriptId(data.scriptId); + if (bgTemplate->Id == BATTLEGROUND_RB) + bg->SetRandom(true); - AddBattleground(bg); + bg->SetBgTypeID(bgTemplate->Id); + bg->SetInstanceID(0); + AddBattleground(bg); + } + + bg->SetMapId(bgTemplate->BattlemasterEntry->mapid[0]); + bg->SetName(bgTemplate->BattlemasterEntry->name[sWorld->GetDefaultDbcLocale()]); + bg->SetArenaorBGType(bgTemplate->IsArena()); + bg->SetMinPlayersPerTeam(bgTemplate->MinPlayersPerTeam); + bg->SetMaxPlayersPerTeam(bgTemplate->MaxPlayersPerTeam); + bg->SetTeamStartPosition(TEAM_ALLIANCE, bgTemplate->StartLocation[TEAM_ALLIANCE]); + bg->SetTeamStartPosition(TEAM_HORDE, bgTemplate->StartLocation[TEAM_HORDE]); + bg->SetStartMaxDist(bgTemplate->MaxStartDistSq); + bg->SetLevelRange(bgTemplate->MinLevel, bgTemplate->MaxLevel); + bg->SetScriptId(bgTemplate->ScriptId); return true; } -void BattlegroundMgr::CreateInitialBattlegrounds() +void BattlegroundMgr::LoadBattlegroundTemplates() { uint32 oldMSTime = getMSTime(); @@ -344,13 +451,11 @@ void BattlegroundMgr::CreateInitialBattlegrounds() return; } - uint32 count = 0; - do { Field* fields = result->Fetch(); - uint32 bgTypeId = fields[0].Get(); + BattlegroundTypeId bgTypeId = static_cast(fields[0].Get()); if (DisableMgr::IsDisabledFor(DISABLE_TYPE_BATTLEGROUND, bgTypeId, nullptr)) continue; @@ -363,72 +468,69 @@ void BattlegroundMgr::CreateInitialBattlegrounds() continue; } - CreateBattlegroundData data; - data.bgTypeId = BattlegroundTypeId(bgTypeId); - data.IsArena = (bl->type == TYPE_ARENA); - data.MinPlayersPerTeam = fields[1].Get(); - data.MaxPlayersPerTeam = fields[2].Get(); - data.LevelMin = fields[3].Get(); - data.LevelMax = fields[4].Get(); + BattlegroundTemplate bgTemplate; + bgTemplate.Id = bgTypeId; + bgTemplate.MinPlayersPerTeam = fields[1].Get(); + bgTemplate.MaxPlayersPerTeam = fields[2].Get(); + bgTemplate.MinLevel = fields[3].Get(); + bgTemplate.MaxLevel = fields[4].Get(); float dist = fields[9].Get(); - data.StartMaxDist = dist * dist; - data.Weight = fields[10].Get(); + bgTemplate.MaxStartDistSq = dist * dist; + bgTemplate.Weight = fields[10].Get(); + bgTemplate.ScriptId = sObjectMgr->GetScriptId(fields[11].Get()); + bgTemplate.BattlemasterEntry = bl; - data.scriptId = sObjectMgr->GetScriptId(fields[11].Get()); - data.BattlegroundName = bl->name[sWorld->GetDefaultDbcLocale()]; - data.MapID = bl->mapid[0]; - - if (data.MaxPlayersPerTeam == 0 || data.MinPlayersPerTeam > data.MaxPlayersPerTeam) + if (bgTemplate.MaxPlayersPerTeam == 0 || bgTemplate.MinPlayersPerTeam > bgTemplate.MaxPlayersPerTeam) { - LOG_ERROR("bg.battleground", "Table `battleground_template` for id {} has bad values for MinPlayersPerTeam ({}) and MaxPlayersPerTeam({})", - data.bgTypeId, data.MinPlayersPerTeam, data.MaxPlayersPerTeam); + LOG_ERROR("sql.sql", "Table `battleground_template` for id {} contains bad values for MinPlayersPerTeam ({}) and MaxPlayersPerTeam({}).", + bgTemplate.Id, bgTemplate.MinPlayersPerTeam, bgTemplate.MaxPlayersPerTeam); + continue; } - if (data.LevelMin == 0 || data.LevelMax == 0 || data.LevelMin > data.LevelMax) + if (bgTemplate.MinLevel == 0 || bgTemplate.MaxLevel == 0 || bgTemplate.MinLevel > bgTemplate.MaxLevel) { - LOG_ERROR("bg.battleground", "Table `battleground_template` for id {} has bad values for LevelMin ({}) and LevelMax({})", - data.bgTypeId, data.LevelMin, data.LevelMax); + LOG_ERROR("sql.sql", "Table `battleground_template` for id {} has bad values for LevelMin ({}) and LevelMax({})", + bgTemplate.Id, bgTemplate.MinLevel, bgTemplate.MaxLevel); continue; } - if (data.bgTypeId != BATTLEGROUND_AA && data.bgTypeId != BATTLEGROUND_RB) + if (bgTemplate.Id != BATTLEGROUND_AA && bgTemplate.Id != BATTLEGROUND_RB) { uint32 startId = fields[5].Get(); if (GraveyardStruct const* start = sGraveyard->GetGraveyard(startId)) { - data.StartLocation[TEAM_ALLIANCE].Relocate(start->x, start->y, start->z, fields[6].Get()); + bgTemplate.StartLocation[TEAM_ALLIANCE].Relocate(start->x, start->y, start->z, fields[6].Get()); } else { - LOG_ERROR("sql.sql", "Table `battleground_template` for id %u contains a non-existing WorldSafeLocs.dbc id %u in field `AllianceStartLoc`. BG not created.", data.bgTypeId, startId); + LOG_ERROR("sql.sql", "Table `battleground_template` for id {} contains a non-existing WorldSafeLocs.dbc id {} in field `AllianceStartLoc`. BG not created.", bgTemplate.Id, startId); continue; } startId = fields[7].Get(); if (GraveyardStruct const* start = sGraveyard->GetGraveyard(startId)) { - data.StartLocation[TEAM_HORDE].Relocate(start->x, start->y, start->z, fields[8].Get()); + bgTemplate.StartLocation[TEAM_HORDE].Relocate(start->x, start->y, start->z, fields[8].Get()); } else { - LOG_ERROR("sql.sql", "Table `battleground_template` for id %u contains a non-existing WorldSafeLocs.dbc id %u in field `HordeStartLoc`. BG not created.", data.bgTypeId, startId); + LOG_ERROR("sql.sql", "Table `battleground_template` for id {} contains a non-existing WorldSafeLocs.dbc id {} in field `HordeStartLoc`. BG not created.", bgTemplate.Id, startId); continue; } } - if (!CreateBattleground(data)) + if (!CreateBattleground(&bgTemplate)) continue; - _battlegroundTemplates[BattlegroundTypeId(bgTypeId)] = data; + _battlegroundTemplates[bgTypeId] = bgTemplate; - if (bl->mapid[1] == -1) // in this case we have only one mapId - _battlegroundMapTemplates[bl->mapid[0]] = &_battlegroundTemplates[BattlegroundTypeId(bgTypeId)]; + if (bgTemplate.BattlemasterEntry->mapid[1] == -1) // in this case we have only one mapId + _battlegroundMapTemplates[bgTemplate.BattlemasterEntry->mapid[0]] = &_battlegroundTemplates[bgTypeId]; - ++count; } while (result->NextRow()); - LOG_INFO("server.loading", ">> Loaded {} battlegrounds in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", ">> Loaded {} battlegrounds in {} ms", _battlegroundTemplates.size(), GetMSTimeDiffToNow(oldMSTime)); LOG_INFO("server.loading", " "); } @@ -499,23 +601,31 @@ void BattlegroundMgr::BuildBattlegroundListPacket(WorldPacket* data, ObjectGuid size_t count_pos = data->wpos(); *data << uint32(0); // number of bg instances - if (Battleground* bgt = GetBattlegroundTemplate(bgTypeId)) - if (GetBattlegroundBracketByLevel(bgt->GetMapId(), player->getLevel())) + auto const& it = bgDataStore.find(bgTypeId); + if (it != bgDataStore.end()) + { + // expected bracket entry + if (PvPDifficultyEntry const* bracketEntry = GetBattlegroundBracketByLevel(it->second._Battlegrounds.begin()->second->GetMapId(), player->getLevel())) { uint32 count = 0; - /*for (BattlegroundClientIdsContainer::const_iterator itr = clientIds.begin(); itr != clientIds.end(); ++itr) + BattlegroundBracketId bracketId = bracketEntry->GetBracketId(); + BattlegroundClientIdsContainer& clientIds = it->second._ClientBattlegroundIds[bracketId]; + + for (auto const& itr : clientIds) { - *data << uint32(*itr); + *data << uint32(itr); ++count; - }*/ + } + data->put(count_pos, count); } + } } } void BattlegroundMgr::SendToBattleground(Player* player, uint32 instanceId, BattlegroundTypeId bgTypeId) { - if (Battleground* bg = GetBattleground(instanceId)) + if (Battleground* bg = GetBattleground(instanceId, bgTypeId)) { uint32 mapid = bg->GetMapId(); Position const* pos = bg->GetTeamStartPosition(player->GetBgTeamId()); @@ -636,11 +746,25 @@ void BattlegroundMgr::SetHolidayWeekends(uint32 mask) } } -void BattlegroundMgr::ScheduleArenaQueueUpdate(uint32 arenaRatedTeamId, BattlegroundQueueTypeId bgQueueTypeId, BattlegroundBracketId bracket_id) +void BattlegroundMgr::ScheduleQueueUpdate(uint32 arenaMatchmakerRating, uint8 arenaType, BattlegroundQueueTypeId bgQueueTypeId, BattlegroundTypeId bgTypeId, BattlegroundBracketId bracket_id) { - uint64 const scheduleId = ((uint64)arenaRatedTeamId << 32) | (bgQueueTypeId << 16) | bracket_id; - if (std::find(m_ArenaQueueUpdateScheduler.begin(), m_ArenaQueueUpdateScheduler.end(), scheduleId) == m_ArenaQueueUpdateScheduler.end()) - m_ArenaQueueUpdateScheduler.push_back(scheduleId); + //This method must be atomic, @todo add mutex + //we will use only 1 number created of bgTypeId and bracket_id + uint64 const scheduleId = ((uint64)arenaMatchmakerRating << 32) | ((uint64)arenaType << 24) | ((uint64)bgQueueTypeId << 16) | ((uint64)bgTypeId << 8) | (uint64)bracket_id; + if (std::find(m_QueueUpdateScheduler.begin(), m_QueueUpdateScheduler.end(), scheduleId) == m_QueueUpdateScheduler.end()) + m_QueueUpdateScheduler.emplace_back(scheduleId); +} + +uint32 BattlegroundMgr::GetMaxRatingDifference() const +{ + uint32 diff = sWorld->getIntConfig(CONFIG_ARENA_MAX_RATING_DIFFERENCE); + + if (diff == 0) + { + diff = 5000; + } + + return diff; } uint32 BattlegroundMgr::GetRatingDiscardTimer() const @@ -766,24 +890,23 @@ bool BattlegroundMgr::IsBGWeekend(BattlegroundTypeId bgTypeId) BattlegroundTypeId BattlegroundMgr::GetRandomBG(BattlegroundTypeId bgTypeId, uint32 minLevel) { - if (GetBattlegroundTemplateByTypeId(bgTypeId)) + if (BattlegroundTemplate const* bgTemplate = GetBattlegroundTemplateByTypeId(bgTypeId)) { std::vector ids; ids.reserve(16); std::vector weights; weights.reserve(16); - BattlemasterListEntry const* bl = sBattlemasterListStore.LookupEntry(bgTypeId); - for (int32 mapId : bl->mapid) + for (int32 mapId : bgTemplate->BattlemasterEntry->mapid) { if (mapId == -1) break; - if (CreateBattlegroundData const* bg = GetBattlegroundTemplateByMapId(mapId)) + if (BattlegroundTemplate const* bg = GetBattlegroundTemplateByMapId(mapId)) { - if (bg->LevelMin <= minLevel) + if (bg->MinLevel <= minLevel) { - ids.push_back(bg->bgTypeId); + ids.push_back(bg->Id); weights.push_back(bg->Weight); } } @@ -795,30 +918,38 @@ BattlegroundTypeId BattlegroundMgr::GetRandomBG(BattlegroundTypeId bgTypeId, uin return BATTLEGROUND_TYPE_NONE; } +BGFreeSlotQueueContainer& BattlegroundMgr::GetBGFreeSlotQueueStore(BattlegroundTypeId bgTypeId) +{ + return bgDataStore[bgTypeId].BGFreeSlotQueue; +} + +void BattlegroundMgr::AddToBGFreeSlotQueue(BattlegroundTypeId bgTypeId, Battleground* bg) +{ + bgDataStore[bgTypeId].BGFreeSlotQueue.push_front(bg); +} + +void BattlegroundMgr::RemoveFromBGFreeSlotQueue(BattlegroundTypeId bgTypeId, uint32 instanceId) +{ + BGFreeSlotQueueContainer& queues = bgDataStore[bgTypeId].BGFreeSlotQueue; + for (BGFreeSlotQueueContainer::iterator itr = queues.begin(); itr != queues.end(); ++itr) + if ((*itr)->GetInstanceID() == instanceId) + { + queues.erase(itr); + return; + } +} + void BattlegroundMgr::AddBattleground(Battleground* bg) { - if (bg->GetInstanceID() == 0) - m_BattlegroundTemplates[bg->GetBgTypeID()] = bg; - else - m_Battlegrounds[bg->GetInstanceID()] = bg; + if (bg) + bgDataStore[bg->GetBgTypeID()]._Battlegrounds[bg->GetInstanceID()] = bg; sScriptMgr->OnBattlegroundCreate(bg); } void BattlegroundMgr::RemoveBattleground(BattlegroundTypeId bgTypeId, uint32 instanceId) { - if (instanceId == 0) - m_BattlegroundTemplates.erase(bgTypeId); - else - m_Battlegrounds.erase(instanceId); -} - -void BattlegroundMgr::DoForAllBattlegrounds(std::function const& worker) -{ - for (auto const& [_, bg] : m_Battlegrounds) - { - worker(bg); - } + bgDataStore[bgTypeId]._Battlegrounds.erase(instanceId); } // init/update unordered_map diff --git a/src/server/game/Battlegrounds/BattlegroundMgr.h b/src/server/game/Battlegrounds/BattlegroundMgr.h index ce5930ac6..bdb17651a 100644 --- a/src/server/game/Battlegrounds/BattlegroundMgr.h +++ b/src/server/game/Battlegrounds/BattlegroundMgr.h @@ -27,29 +27,38 @@ #include typedef std::map BattlegroundContainer; +typedef std::set BattlegroundClientIdsContainer; typedef std::unordered_map BattleMastersMap; typedef Battleground* (*bgRef)(Battleground*); typedef void(*bgMapRef)(WorldPacket*, Battleground::BattlegroundScoreMap::const_iterator); typedef void(*bgTypeRef)(WorldPacket*, Battleground::BattlegroundScoreMap::const_iterator, Battleground*); -struct CreateBattlegroundData +// this container can't be deque, because deque doesn't like removing the last element - if you remove it, it invalidates next iterator and crash appears +using BGFreeSlotQueueContainer = std::list; + +struct BattlegroundData { - BattlegroundTypeId bgTypeId; - bool IsArena; - uint32 MinPlayersPerTeam; - uint32 MaxPlayersPerTeam; - uint32 LevelMin; - uint32 LevelMax; - char const* BattlegroundName; - uint32 MapID; - float StartMaxDist; - std::array StartLocation; - uint32 scriptId; - uint8 Weight; + BattlegroundContainer _Battlegrounds; + std::array _ClientBattlegroundIds; + BGFreeSlotQueueContainer BGFreeSlotQueue; }; -struct GroupQueueInfo; +struct BattlegroundTemplate +{ + BattlegroundTypeId Id; + uint16 MinPlayersPerTeam; + uint16 MaxPlayersPerTeam; + uint8 MinLevel; + uint8 MaxLevel; + std::array StartLocation; + float MaxStartDistSq; + uint8 Weight; + uint32 ScriptId; + BattlemasterListEntry const* BattlemasterEntry; + + bool IsArena() const; +}; class BattlegroundMgr { @@ -71,21 +80,25 @@ public: void SendAreaSpiritHealerQueryOpcode(Player* player, Battleground* bg, ObjectGuid guid); /* Battlegrounds */ - Battleground* GetBattleground(uint32 InstanceID); + Battleground* GetBattlegroundThroughClientInstance(uint32 instanceId, BattlegroundTypeId bgTypeId); + Battleground* GetBattleground(uint32 instanceID, BattlegroundTypeId bgTypeId); Battleground* GetBattlegroundTemplate(BattlegroundTypeId bgTypeId); - Battleground* CreateNewBattleground(BattlegroundTypeId bgTypeId, uint32 minLevel, uint32 maxLevel, uint8 arenaType, bool isRated); + Battleground* CreateNewBattleground(BattlegroundTypeId bgTypeId, PvPDifficultyEntry const* bracketEntry, uint8 arenaType, bool isRated); void AddBattleground(Battleground* bg); void RemoveBattleground(BattlegroundTypeId bgTypeId, uint32 instanceId); + void AddToBGFreeSlotQueue(BattlegroundTypeId bgTypeId, Battleground* bg); + void RemoveFromBGFreeSlotQueue(BattlegroundTypeId bgTypeId, uint32 instanceId); + BGFreeSlotQueueContainer& GetBGFreeSlotQueueStore(BattlegroundTypeId bgTypeId); - void CreateInitialBattlegrounds(); + void LoadBattlegroundTemplates(); void DeleteAllBattlegrounds(); void SendToBattleground(Player* player, uint32 InstanceID, BattlegroundTypeId bgTypeId); /* Battleground queues */ BattlegroundQueue& GetBattlegroundQueue(BattlegroundQueueTypeId bgQueueTypeId) { return m_BattlegroundQueues[bgQueueTypeId]; } - void ScheduleArenaQueueUpdate(uint32 arenaRatedTeamId, BattlegroundQueueTypeId bgQueueTypeId, BattlegroundBracketId bracket_id); + void ScheduleQueueUpdate(uint32 arenaMatchmakerRating, uint8 arenaType, BattlegroundQueueTypeId bgQueueTypeId, BattlegroundTypeId bgTypeId, BattlegroundBracketId bracket_id); uint32 GetPrematureFinishTime() const; void ToggleArenaTesting(); @@ -105,10 +118,12 @@ public: static BattlegroundTypeId WeekendHolidayIdToBGType(HolidayIds holiday); static bool IsBGWeekend(BattlegroundTypeId bgTypeId); - uint32 GetRatingDiscardTimer() const; + uint32 GetMaxRatingDifference() const; + uint32 GetRatingDiscardTimer() const; void InitAutomaticArenaPointDistribution(); void LoadBattleMastersEntry(); void CheckBattleMasters(); + BattlegroundTypeId GetBattleMasterBG(uint32 entry) const { BattleMastersMap::const_iterator itr = mBattleMastersMap.find(entry); @@ -117,8 +132,6 @@ public: return BATTLEGROUND_TYPE_NONE; } - const BattlegroundContainer& GetBattlegroundList() { return m_Battlegrounds; } // pussywizard - static std::unordered_map bgToQueue; // BattlegroundTypeId -> BattlegroundQueueTypeId static std::unordered_map queueToBg; // BattlegroundQueueTypeId -> BattlegroundTypeId static std::unordered_map bgtypeToBattleground; // BattlegroundTypeId -> Battleground* @@ -128,48 +141,46 @@ public: static std::unordered_map ArenaTypeToQueue; // ArenaType -> BattlegroundQueueTypeId static std::unordered_map QueueToArenaType; // BattlegroundQueueTypeId -> ArenaType - void DoForAllBattlegrounds(std::function const& worker); - private: - bool CreateBattleground(CreateBattlegroundData& data); - uint32 GetNextClientVisibleInstanceId(); + bool CreateBattleground(BattlegroundTemplate const* bgTemplate); + uint32 CreateClientVisibleInstanceId(BattlegroundTypeId bgTypeId, BattlegroundBracketId bracket_id); BattlegroundTypeId GetRandomBG(BattlegroundTypeId id, uint32 minLevel); - typedef std::map BattlegroundTemplateContainer; - BattlegroundTemplateContainer m_BattlegroundTemplates; - BattlegroundContainer m_Battlegrounds; + typedef std::map BattlegroundDataContainer; + BattlegroundDataContainer bgDataStore; BattlegroundQueue m_BattlegroundQueues[MAX_BATTLEGROUND_QUEUE_TYPES]; - std::vector m_ArenaQueueUpdateScheduler; + std::vector m_QueueUpdateScheduler; bool m_ArenaTesting; bool m_Testing; - uint32 m_lastClientVisibleInstanceId; Seconds m_NextAutoDistributionTime; uint32 m_AutoDistributionTimeChecker; uint32 m_NextPeriodicQueueUpdateTime; BattleMastersMap mBattleMastersMap; - CreateBattlegroundData const* GetBattlegroundTemplateByTypeId(BattlegroundTypeId id) + BattlegroundTemplate const* GetBattlegroundTemplateByTypeId(BattlegroundTypeId id) { - BattlegroundTemplateMap::const_iterator itr = _battlegroundTemplates.find(id); + auto const& itr = _battlegroundTemplates.find(id); if (itr != _battlegroundTemplates.end()) return &itr->second; + return nullptr; } - CreateBattlegroundData const* GetBattlegroundTemplateByMapId(uint32 mapId) + BattlegroundTemplate const* GetBattlegroundTemplateByMapId(uint32 mapId) { - BattlegroundMapTemplateContainer::const_iterator itr = _battlegroundMapTemplates.find(mapId); + auto const& itr = _battlegroundMapTemplates.find(mapId); if (itr != _battlegroundMapTemplates.end()) return itr->second; + return nullptr; } typedef std::map BattlegroundSelectionWeightMap; - typedef std::map BattlegroundTemplateMap; - typedef std::map BattlegroundMapTemplateContainer; + typedef std::map BattlegroundTemplateMap; + typedef std::map BattlegroundMapTemplateContainer; BattlegroundTemplateMap _battlegroundTemplates; BattlegroundMapTemplateContainer _battlegroundMapTemplates; }; diff --git a/src/server/game/Battlegrounds/BattlegroundQueue.cpp b/src/server/game/Battlegrounds/BattlegroundQueue.cpp index d8bab21c6..2e1ea708f 100644 --- a/src/server/game/Battlegrounds/BattlegroundQueue.cpp +++ b/src/server/game/Battlegrounds/BattlegroundQueue.cpp @@ -35,7 +35,7 @@ /*** BATTLEGROUND QUEUE SYSTEM ***/ /*********************************************************/ -BattlegroundQueue::BattlegroundQueue() : m_bgTypeId(BATTLEGROUND_TYPE_NONE), m_arenaType(ArenaType(0)) +BattlegroundQueue::BattlegroundQueue() { for (uint32 i = 0; i < PVP_TEAMS_COUNT; ++i) { @@ -128,23 +128,25 @@ bool BattlegroundQueue::SelectionPool::AddGroup(GroupQueueInfo* ginfo, uint32 de /*********************************************************/ // add group or player (grp == nullptr) to bg queue with the given leader and bg specifications -GroupQueueInfo* BattlegroundQueue::AddGroup(Player* leader, Group* grp, PvPDifficultyEntry const* bracketEntry, bool isRated, bool isPremade, uint32 ArenaRating, uint32 MatchmakerRating, uint32 arenateamid) +GroupQueueInfo* BattlegroundQueue::AddGroup(Player* leader, Group* group, BattlegroundTypeId bgTypeId, PvPDifficultyEntry const* bracketEntry, uint8 arenaType, bool isRated, bool isPremade, + uint32 arenaRating, uint32 matchmakerRating, uint32 arenaTeamId /*= 0*/, uint32 opponentsArenaTeamId /*= 0*/) { BattlegroundBracketId bracketId = bracketEntry->GetBracketId(); // create new ginfo auto* ginfo = new GroupQueueInfo; - ginfo->BgTypeId = m_bgTypeId; - ginfo->ArenaType = m_arenaType; - ginfo->ArenaTeamId = arenateamid; + ginfo->BgTypeId = bgTypeId; + ginfo->ArenaType = arenaType; + ginfo->ArenaTeamId = arenaTeamId; ginfo->IsRated = isRated; ginfo->IsInvitedToBGInstanceGUID = 0; ginfo->JoinTime = GameTime::GetGameTimeMS().count(); ginfo->RemoveInviteTime = 0; ginfo->teamId = leader->GetTeamId(); ginfo->RealTeamID = leader->GetTeamId(true); - ginfo->ArenaTeamRating = ArenaRating; - ginfo->ArenaMatchmakerRating = MatchmakerRating; + ginfo->ArenaTeamRating = arenaRating; + ginfo->ArenaMatchmakerRating = matchmakerRating; + ginfo->PreviousOpponentsTeamId = opponentsArenaTeamId; ginfo->OpponentsTeamRating = 0; ginfo->OpponentsMatchmakerRating = 0; @@ -159,33 +161,30 @@ GroupQueueInfo* BattlegroundQueue::AddGroup(Player* leader, Group* grp, PvPDiffi if (ginfo->teamId == TEAM_HORDE) index++; - sScriptMgr->OnAddGroup(this, ginfo, index, leader, grp, bracketEntry, isPremade); + sScriptMgr->OnAddGroup(this, ginfo, index, leader, group, bgTypeId, bracketEntry, + arenaType, isRated, isPremade, arenaRating, matchmakerRating, arenaTeamId, opponentsArenaTeamId); - LOG_DEBUG("bg.battleground", "Adding Group to BattlegroundQueue bgTypeId: {}, bracket_id: {}, index: {}", m_bgTypeId, bracketId, index); + LOG_DEBUG("bg.battleground", "Adding Group to BattlegroundQueue bgTypeId: {}, bracket_id: {}, index: {}", bgTypeId, bracketId, index); // pussywizard: store indices at which GroupQueueInfo is in m_QueuedGroups - ginfo->_bracketId = bracketId; - ginfo->_groupType = index; + ginfo->BracketId = bracketId; + ginfo->GroupType = index; //add players from group to ginfo - if (grp) + if (group) { - for (GroupReference* itr = grp->GetFirstMember(); itr != nullptr; itr = itr->next()) + group->DoForAllMembers([this, ginfo](Player* member) { - Player* member = itr->GetSource(); - if (!member) - continue; - ASSERT(m_QueuedPlayers.count(member->GetGUID()) == 0); m_QueuedPlayers[member->GetGUID()] = ginfo; - ginfo->Players.insert(member->GetGUID()); - } + ginfo->Players.emplace(member->GetGUID()); + }); } else { ASSERT(m_QueuedPlayers.count(leader->GetGUID()) == 0); m_QueuedPlayers[leader->GetGUID()] = ginfo; - ginfo->Players.insert(leader->GetGUID()); + ginfo->Players.emplace(leader->GetGUID()); } //add GroupInfo to m_QueuedGroups @@ -220,10 +219,10 @@ void BattlegroundQueue::PlayerInvitedToBGUpdateAverageWaitTime(GroupQueueInfo* g return; // pointer to last index - uint32* lastIndex = &m_WaitTimeLastIndex[team_index][ginfo->_bracketId]; + uint32* lastIndex = &m_WaitTimeLastIndex[team_index][ginfo->BracketId]; // set time at index to new value - m_WaitTimes[team_index][ginfo->_bracketId][*lastIndex] = timeInQueue; + m_WaitTimes[team_index][ginfo->BracketId][*lastIndex] = timeInQueue; // set last index to next one (*lastIndex)++; @@ -244,11 +243,11 @@ uint32 BattlegroundQueue::GetAverageQueueWaitTime(GroupQueueInfo* ginfo) const return 0; // if there are enough values: - if (m_WaitTimes[team_index][ginfo->_bracketId][COUNT_OF_PLAYERS_TO_AVERAGE_WAIT_TIME - 1]) + if (m_WaitTimes[team_index][ginfo->BracketId][COUNT_OF_PLAYERS_TO_AVERAGE_WAIT_TIME - 1]) { uint32 sum = 0; for (uint32 i = 0; i < COUNT_OF_PLAYERS_TO_AVERAGE_WAIT_TIME; ++i) - sum += m_WaitTimes[team_index][ginfo->_bracketId][i]; + sum += m_WaitTimes[team_index][ginfo->BracketId][i]; return sum / COUNT_OF_PLAYERS_TO_AVERAGE_WAIT_TIME; } else @@ -256,56 +255,59 @@ uint32 BattlegroundQueue::GetAverageQueueWaitTime(GroupQueueInfo* ginfo) const } //remove player from queue and from group info, if group info is empty then remove it too -void BattlegroundQueue::RemovePlayer(ObjectGuid guid, bool sentToBg, uint32 playerQueueSlot) +void BattlegroundQueue::RemovePlayer(ObjectGuid guid, bool decreaseInvitedCount) { - // pussywizard: leave queue packet - if (playerQueueSlot < PLAYER_MAX_BATTLEGROUND_QUEUES) - if (Player* p = ObjectAccessor::FindConnectedPlayer(guid)) - { - WorldPacket data; - sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, nullptr, playerQueueSlot, STATUS_NONE, 0, 0, 0, TEAM_NEUTRAL); - p->GetSession()->SendPacket(&data); - } - //remove player from map, if he's there - auto itr = m_QueuedPlayers.find(guid); + auto const& itr = m_QueuedPlayers.find(guid); if (itr == m_QueuedPlayers.end()) { - ABORT(); + //This happens if a player logs out while in a bg because WorldSession::LogoutPlayer() notifies the bg twice + std::string playerName = "Unknown"; + + if (Player* player = ObjectAccessor::FindPlayer(guid)) + { + playerName = player->GetName(); + } + + LOG_ERROR("bg.battleground", "BattlegroundQueue: couldn't find player {} ({})", playerName, guid.ToString()); + //ABORT("BattlegroundQueue: couldn't find player {} ({})", playerName, guid.ToString()); return; } GroupQueueInfo* groupInfo = itr->second; - uint32 _bracketId = groupInfo->_bracketId; - uint32 _groupType = groupInfo->_groupType; + uint32 _bracketId = groupInfo->BracketId; + uint32 _groupType = groupInfo->GroupType; // find iterator auto group_itr = m_QueuedGroups[_bracketId][_groupType].end(); - for (auto k = m_QueuedGroups[_bracketId][_groupType].begin(); k != m_QueuedGroups[_bracketId][_groupType].end(); ++k) + + for (auto k = m_QueuedGroups[_bracketId][_groupType].begin(); k != m_QueuedGroups[_bracketId][_groupType].end(); k++) if ((*k) == groupInfo) { group_itr = k; break; } - //player can't be in queue without group, but just in case + // player can't be in queue without group, but just in case if (group_itr == m_QueuedGroups[_bracketId][_groupType].end()) { - ABORT(); + LOG_ERROR("bg.battleground", "BattlegroundQueue: ERROR Cannot find groupinfo for {}", guid.ToString()); + //ABORT("BattlegroundQueue: ERROR Cannot find groupinfo for {}", guid.ToString()); return; } + LOG_DEBUG("bg.battleground", "BattlegroundQueue: Removing {}, from bracket_id {}", guid.ToString(), _bracketId); + // remove player from group queue info - auto pitr = groupInfo->Players.find(guid); + auto const& pitr = groupInfo->Players.find(guid); ASSERT(pitr != groupInfo->Players.end()); if (pitr != groupInfo->Players.end()) groupInfo->Players.erase(pitr); - // if invited to bg, then always decrease invited count when removed from queue - // sending player to bg will increase it again - if (groupInfo->IsInvitedToBGInstanceGUID) - if (Battleground* bg = sBattlegroundMgr->GetBattleground(groupInfo->IsInvitedToBGInstanceGUID)) + // if invited to bg, and should decrease invited count, then do it + if (decreaseInvitedCount && groupInfo->IsInvitedToBGInstanceGUID) + if (Battleground* bg = sBattlegroundMgr->GetBattleground(groupInfo->IsInvitedToBGInstanceGUID, groupInfo->BgTypeId)) bg->DecreaseInvitedCount(groupInfo->teamId); // remove player queue info @@ -315,13 +317,20 @@ void BattlegroundQueue::RemovePlayer(ObjectGuid guid, bool sentToBg, uint32 play SendExitMessageArenaQueue(groupInfo); // if player leaves queue and he is invited to a rated arena match, then count it as he lost - if (groupInfo->IsInvitedToBGInstanceGUID && groupInfo->IsRated && !sentToBg) + if (groupInfo->IsInvitedToBGInstanceGUID && groupInfo->IsRated && decreaseInvitedCount) + { if (ArenaTeam* at = sArenaTeamMgr->GetArenaTeamById(groupInfo->ArenaTeamId)) { + LOG_DEBUG("bg.battleground", "UPDATING memberLost's personal arena rating for {} by opponents rating: {}", guid.ToString(), groupInfo->OpponentsTeamRating); + if (Player* player = ObjectAccessor::FindConnectedPlayer(guid)) + { at->MemberLost(player, groupInfo->OpponentsMatchmakerRating); + } + at->SaveToDB(); } + } // remove group queue info no players left if (groupInfo->Players.empty()) @@ -335,17 +344,20 @@ void BattlegroundQueue::RemovePlayer(ObjectGuid guid, bool sentToBg, uint32 play // if it's a rated arena and any member leaves when group not yet invited - everyone from group leaves too! if (groupInfo->IsRated && !groupInfo->IsInvitedToBGInstanceGUID) { - uint32 queueSlot = PLAYER_MAX_BATTLEGROUND_QUEUES; - if (Player* plr = ObjectAccessor::FindConnectedPlayer(*(groupInfo->Players.begin()))) { + Battleground* bg = sBattlegroundMgr->GetBattlegroundTemplate(groupInfo->BgTypeId); BattlegroundQueueTypeId bgQueueTypeId = BattlegroundMgr::BGQueueTypeId(groupInfo->BgTypeId, groupInfo->ArenaType); - queueSlot = plr->GetBattlegroundQueueIndex(bgQueueTypeId); + uint32 queueSlot = plr->GetBattlegroundQueueIndex(bgQueueTypeId); plr->RemoveBattlegroundQueueId(bgQueueTypeId); + + WorldPacket data; + sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bg, queueSlot, STATUS_NONE, 0, 0, 0, TEAM_NEUTRAL); + plr->SendDirectMessage(&data); } // recursive call - RemovePlayer(*(groupInfo->Players.begin()), false, queueSlot); + RemovePlayer(*groupInfo->Players.begin(), decreaseInvitedCount); } } @@ -377,79 +389,116 @@ bool BattlegroundQueue::GetPlayerGroupInfoData(ObjectGuid guid, GroupQueueInfo* } // this function is filling pools given free slots on both sides, result is ballanced -void BattlegroundQueue::FillPlayersToBG(Battleground* bg, const int32 aliFree, const int32 hordeFree, BattlegroundBracketId bracket_id) +void BattlegroundQueue::FillPlayersToBG(Battleground* bg, BattlegroundBracketId bracket_id) { - if (!sScriptMgr->CanFillPlayersToBG(this, bg, aliFree, hordeFree, bracket_id)) - return; - - // clear selection pools - m_SelectionPools[TEAM_ALLIANCE].Init(); - m_SelectionPools[TEAM_HORDE].Init(); - - // quick check if nothing we can do: - if (!sBattlegroundMgr->isTesting()) - if ((aliFree > hordeFree && m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE].empty()) || - (hordeFree > aliFree && m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_HORDE].empty())) - return; - - // ally: at first fill as much as possible - auto Ali_itr = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE].begin(); - for (; Ali_itr != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE].end() && m_SelectionPools[TEAM_ALLIANCE].AddGroup((*Ali_itr), aliFree); ++Ali_itr); - - // horde: at first fill as much as possible - auto Horde_itr = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_HORDE].begin(); - for (; Horde_itr != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_HORDE].end() && m_SelectionPools[TEAM_HORDE].AddGroup((*Horde_itr), hordeFree); ++Horde_itr); - - // calculate free space after adding - int32 aliDiff = aliFree - int32(m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount()); - int32 hordeDiff = hordeFree - int32(m_SelectionPools[TEAM_HORDE].GetPlayerCount()); - - int32 invType = sWorld->getIntConfig(CONFIG_BATTLEGROUND_INVITATION_TYPE); - int32 invDiff = 0; - - // check balance configuration and set the max difference between teams - switch (invType) + if (!sScriptMgr->CanFillPlayersToBG(this, bg, bracket_id)) { - case BG_QUEUE_INVITATION_TYPE_NO_BALANCE: - return; - case BG_QUEUE_INVITATION_TYPE_BALANCED: - invDiff = 1; - break; - case BG_QUEUE_INVITATION_TYPE_EVEN: - break; - default: - return; + return; } - // balance the teams based on the difference allowed - while (std::abs(aliDiff - hordeDiff) > invDiff && (m_SelectionPools[TEAM_HORDE].GetPlayerCount() > 0 || m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() > 0)) - { - // if results in more alliance players than horde: - if (aliDiff < hordeDiff) - { - // no more alliance in pool, invite whatever we can from horde - if (!m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount()) - break; + int32 hordeFree = bg->GetFreeSlotsForTeam(TEAM_HORDE); + int32 aliFree = bg->GetFreeSlotsForTeam(TEAM_ALLIANCE); + uint32 aliCount = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE].size(); + uint32 hordeCount = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_HORDE].size(); - // kick alliance, returns true if kicked more than needed, so then try to fill up - if (m_SelectionPools[TEAM_ALLIANCE].KickGroup(hordeDiff - aliDiff)) - for (; Ali_itr != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE].end() && m_SelectionPools[TEAM_ALLIANCE].AddGroup((*Ali_itr), aliFree >= hordeDiff ? aliFree - hordeDiff : 0); ++Ali_itr); + // try to get even teams + if (sWorld->getIntConfig(CONFIG_BATTLEGROUND_INVITATION_TYPE) == BG_QUEUE_INVITATION_TYPE_EVEN) + { + // check if the teams are even + if (hordeFree == 1 && aliFree == 1) + { + // if we are here, the teams have the same amount of players + // then we have to allow to join the same amount of players + int32 hordeExtra = hordeCount - aliCount; + int32 aliExtra = aliCount - hordeCount; + + hordeExtra = std::max(hordeExtra, 0); + aliExtra = std::max(aliExtra, 0); + + if (aliCount != hordeCount) + { + aliFree -= aliExtra; + hordeFree -= hordeExtra; + + aliFree = std::max(aliFree, 0); + hordeFree = std::max(hordeFree, 0); + } + } + } + + GroupsQueueType::const_iterator Ali_itr = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE].begin(); + //count of groups in queue - used to stop cycles + + //index to queue which group is current + uint32 aliIndex = 0; + for (; aliIndex < aliCount && m_SelectionPools[TEAM_ALLIANCE].AddGroup((*Ali_itr), aliFree); aliIndex++) + ++Ali_itr; + + //the same thing for horde + GroupsQueueType::const_iterator Horde_itr = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_HORDE].begin(); + + uint32 hordeIndex = 0; + for (; hordeIndex < hordeCount && m_SelectionPools[TEAM_HORDE].AddGroup((*Horde_itr), hordeFree); hordeIndex++) + ++Horde_itr; + + //if ofc like BG queue invitation is set in config, then we are happy + if (sWorld->getIntConfig(CONFIG_BATTLEGROUND_INVITATION_TYPE) == BG_QUEUE_INVITATION_TYPE_NO_BALANCE) + return; + + /* + if we reached this code, then we have to solve NP - complete problem called Subset sum problem + So one solution is to check all possible invitation subgroups, or we can use these conditions: + 1. Last time when BattlegroundQueue::Update was executed we invited all possible players - so there is only small possibility + that we will invite now whole queue, because only 1 change has been made to queues from the last BattlegroundQueue::Update call + 2. Other thing we should consider is group order in queue + */ + + // At first we need to compare free space in bg and our selection pool + int32 diffAli = aliFree - int32(m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount()); + int32 diffHorde = hordeFree - int32(m_SelectionPools[TEAM_HORDE].GetPlayerCount()); + + while (std::abs(diffAli - diffHorde) > 1 && (m_SelectionPools[TEAM_HORDE].GetPlayerCount() > 0 || m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() > 0)) + { + //each cycle execution we need to kick at least 1 group + if (diffAli < diffHorde) + { + //kick alliance group, add to pool new group if needed + if (m_SelectionPools[TEAM_ALLIANCE].KickGroup(diffHorde - diffAli)) + { + for (; aliIndex < aliCount && m_SelectionPools[TEAM_ALLIANCE].AddGroup((*Ali_itr), (aliFree >= diffHorde) ? aliFree - diffHorde : 0); aliIndex++) + ++Ali_itr; + } + + //if ali selection is already empty, then kick horde group, but if there are less horde than ali in bg - break; + if (!m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount()) + { + if (aliFree <= diffHorde + 1) + break; + + m_SelectionPools[TEAM_HORDE].KickGroup(diffHorde - diffAli); + } } - // if results in more horde players than alliance: else { - // no more horde in pool, invite whatever we can from alliance - if (!m_SelectionPools[TEAM_HORDE].GetPlayerCount()) - break; + //kick horde group, add to pool new group if needed + if (m_SelectionPools[TEAM_HORDE].KickGroup(diffAli - diffHorde)) + { + for (; hordeIndex < hordeCount && m_SelectionPools[TEAM_HORDE].AddGroup((*Horde_itr), (hordeFree >= diffAli) ? hordeFree - diffAli : 0); hordeIndex++) + ++Horde_itr; + } - // kick horde, returns true if kicked more than needed, so then try to fill up - if (m_SelectionPools[TEAM_HORDE].KickGroup(aliDiff - hordeDiff)) - for (; Horde_itr != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_HORDE].end() && m_SelectionPools[TEAM_HORDE].AddGroup((*Horde_itr), hordeFree >= aliDiff ? hordeFree - aliDiff : 0); ++Horde_itr); + if (!m_SelectionPools[TEAM_HORDE].GetPlayerCount()) + { + if (hordeFree <= diffAli + 1) + break; + + m_SelectionPools[TEAM_ALLIANCE].KickGroup(diffAli - diffHorde); + } } - // recalculate free space after adding - aliDiff = aliFree - static_cast(m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount()); - hordeDiff = hordeFree - static_cast(m_SelectionPools[TEAM_HORDE].GetPlayerCount()); + //count diffs after small update + diffAli = aliFree - int32(m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount()); + diffHorde = hordeFree - int32(m_SelectionPools[TEAM_HORDE].GetPlayerCount()); } } @@ -457,136 +506,191 @@ void BattlegroundQueue::FillPlayersToBG(Battleground* bg, const int32 aliFree, c // then after 30 mins (default) in queue it moves premade group to normal queue bool BattlegroundQueue::CheckPremadeMatch(BattlegroundBracketId bracket_id, uint32 MinPlayersPerTeam, uint32 MaxPlayersPerTeam) { - // clear selection pools - m_SelectionPools[TEAM_ALLIANCE].Init(); - m_SelectionPools[TEAM_HORDE].Init(); - if (!m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].empty() && !m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].empty()) { - // find premade group for both factions: + //start premade match + //if groups aren't invited GroupsQueueType::const_iterator ali_group, horde_group; for (ali_group = m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].begin(); ali_group != m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].end(); ++ali_group) - if (!(*ali_group)->IsInvitedToBGInstanceGUID && (*ali_group)->Players.size() >= MinPlayersPerTeam) + if (!(*ali_group)->IsInvitedToBGInstanceGUID) break; + for (horde_group = m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].begin(); horde_group != m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].end(); ++horde_group) - if (!(*horde_group)->IsInvitedToBGInstanceGUID && (*horde_group)->Players.size() >= MinPlayersPerTeam) + if (!(*horde_group)->IsInvitedToBGInstanceGUID) break; // if found both groups if (ali_group != m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].end() && horde_group != m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].end()) { - // add premade groups to selection pools m_SelectionPools[TEAM_ALLIANCE].AddGroup((*ali_group), MaxPlayersPerTeam); m_SelectionPools[TEAM_HORDE].AddGroup((*horde_group), MaxPlayersPerTeam); - // battleground will be immediately filled (after calling this function and creating new battleground) with more players from normal queue + //add groups/players from normal queue to size of bigger group + uint32 maxPlayers = std::min(m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount(), m_SelectionPools[TEAM_HORDE].GetPlayerCount()); + GroupsQueueType::const_iterator itr; - return m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() >= MinPlayersPerTeam && m_SelectionPools[TEAM_HORDE].GetPlayerCount() >= MinPlayersPerTeam; + for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++) + { + for (itr = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].begin(); itr != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].end(); ++itr) + { + //if itr can join BG and player count is less that maxPlayers, then add group to selectionpool + if (!(*itr)->IsInvitedToBGInstanceGUID && !m_SelectionPools[i].AddGroup((*itr), maxPlayers)) + break; + } + } + + //premade selection pools are set + return true; } } - // now check if we can move groups from premade queue to normal queue - // this happens if timer has expired or group size lowered - - uint32 premade_time = sWorld->getIntConfig(CONFIG_BATTLEGROUND_PREMADE_GROUP_WAIT_FOR_MATCH); - uint32 time_before = GameTime::GetGameTimeMS().count() >= premade_time ? GameTime::GetGameTimeMS().count() - premade_time : 0; + // now check if we can move group from Premade queue to normal queue (timer has expired) or group size lowered!! + // this could be 2 cycles but i'm checking only first team in queue - it can cause problem - + // if first is invited to BG and seconds timer expired, but we can ignore it, because players have only 80 seconds to click to enter bg + // and when they click or after 80 seconds the queue info is removed from queue + uint32 time_before = GameTime::GetGameTimeMS().count() - sWorld->getIntConfig(CONFIG_BATTLEGROUND_PREMADE_GROUP_WAIT_FOR_MATCH); for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++) + { if (!m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE + i].empty()) - for (auto itr = m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE + i].begin(); itr != m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE + i].end(); ) + { + GroupsQueueType::iterator itr = m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE + i].begin(); + if (!(*itr)->IsInvitedToBGInstanceGUID && ((*itr)->JoinTime < time_before || (*itr)->Players.size() < MinPlayersPerTeam)) { - if (!(*itr)->IsInvitedToBGInstanceGUID && ((*itr)->JoinTime < time_before || (*itr)->Players.size() < MinPlayersPerTeam)) - { - (*itr)->_groupType = BG_QUEUE_NORMAL_ALLIANCE + i; // pussywizard: update GroupQueueInfo internal variable - m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].push_front((*itr)); - m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE + i].erase(itr++); - continue; - } - ++itr; + //we must insert group to normal queue and erase pointer from premade queue + (*itr)->GroupType = BG_QUEUE_NORMAL_ALLIANCE + i; // pussywizard: update GroupQueueInfo internal variable + m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].push_front((*itr)); + m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE + i].erase(itr); } + } + } + //selection pools are not set return false; } // this method tries to create battleground or arena with MinPlayersPerTeam against MinPlayersPerTeam bool BattlegroundQueue::CheckNormalMatch(Battleground* bgTemplate, BattlegroundBracketId bracket_id, uint32 minPlayers, uint32 maxPlayers) { - uint32 Coef = 1; + if (sScriptMgr->IsCheckNormalMatch(this, bgTemplate, bracket_id, minPlayers, maxPlayers)) + { + return true; + } - sScriptMgr->OnCheckNormalMatch(this, Coef, bgTemplate, bracket_id, minPlayers, maxPlayers); + GroupsQueueType::const_iterator itr_team[PVP_TEAMS_COUNT]; + for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++) + { + itr_team[i] = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].begin(); + for (; itr_team[i] != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].end(); ++(itr_team[i])) + { + if (!(*(itr_team[i]))->IsInvitedToBGInstanceGUID) + { + m_SelectionPools[i].AddGroup(*(itr_team[i]), maxPlayers); + if (m_SelectionPools[i].GetPlayerCount() >= minPlayers) + break; + } + } + } - minPlayers = minPlayers * Coef; + //try to invite same number of players - this cycle may cause longer wait time even if there are enough players in queue, but we want ballanced bg + uint32 j = TEAM_ALLIANCE; + if (m_SelectionPools[TEAM_HORDE].GetPlayerCount() < m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount()) + j = TEAM_HORDE; - FillPlayersToBG(bgTemplate, maxPlayers, maxPlayers, bracket_id); + if (sWorld->getIntConfig(CONFIG_BATTLEGROUND_INVITATION_TYPE) != BG_QUEUE_INVITATION_TYPE_NO_BALANCE + && m_SelectionPools[TEAM_HORDE].GetPlayerCount() >= minPlayers && m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() >= minPlayers) + { + //we will try to invite more groups to team with less players indexed by j + ++(itr_team[j]); //this will not cause a crash, because for cycle above reached break; + for (; itr_team[j] != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + j].end(); ++(itr_team[j])) + { + if (!(*(itr_team[j]))->IsInvitedToBGInstanceGUID) + if (!m_SelectionPools[j].AddGroup(*(itr_team[j]), m_SelectionPools[(j + 1) % PVP_TEAMS_COUNT].GetPlayerCount())) + break; + } + + // do not allow to start bg with more than 2 players more on 1 faction + if (std::abs((int32)(m_SelectionPools[TEAM_HORDE].GetPlayerCount() - m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount())) > 2) + return false; + } //allow 1v0 if debug bg if (sBattlegroundMgr->isTesting() && bgTemplate->isBattleground() && (m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() || m_SelectionPools[TEAM_HORDE].GetPlayerCount())) return true; - switch (sWorld->getIntConfig(CONFIG_BATTLEGROUND_INVITATION_TYPE)) - { - case BG_QUEUE_INVITATION_TYPE_NO_BALANCE: // in this case, as soon as both teams have > mincount, start - return m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() >= minPlayers && m_SelectionPools[TEAM_HORDE].GetPlayerCount() >= minPlayers; - - case BG_QUEUE_INVITATION_TYPE_BALANCED: // check difference between selection pools - if = 1 or less start. - return std::abs(static_cast(m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount()) - static_cast(m_SelectionPools[TEAM_HORDE].GetPlayerCount())) <= 1 && m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() >= minPlayers && m_SelectionPools[TEAM_HORDE].GetPlayerCount() >= minPlayers; - - case BG_QUEUE_INVITATION_TYPE_EVEN: // if both counts are same then it's an even match - return (m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() == m_SelectionPools[TEAM_HORDE].GetPlayerCount()) && m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() >= minPlayers && m_SelectionPools[TEAM_HORDE].GetPlayerCount() >= minPlayers; - - default: // same as unbalanced (in case wrong setting is entered...) - return m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() >= minPlayers && m_SelectionPools[TEAM_HORDE].GetPlayerCount() >= minPlayers; - } + //return true if there are enough players in selection pools - enable to work .debug bg command correctly + return m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() >= minPlayers && m_SelectionPools[TEAM_HORDE].GetPlayerCount() >= minPlayers; } // this method will check if we can invite players to same faction skirmish match bool BattlegroundQueue::CheckSkirmishForSameFaction(BattlegroundBracketId bracket_id, uint32 minPlayersPerTeam) { - for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++) - if (!m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].empty()) + if (m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() < minPlayersPerTeam && m_SelectionPools[TEAM_HORDE].GetPlayerCount() < minPlayersPerTeam) + return false; + + TeamId teamIndex = TEAM_ALLIANCE; + TeamId otherTeam = TEAM_HORDE; + + if (m_SelectionPools[TEAM_HORDE].GetPlayerCount() == minPlayersPerTeam) + { + teamIndex = TEAM_HORDE; + otherTeam = TEAM_ALLIANCE; + } + + //clear other team's selection + m_SelectionPools[otherTeam].Init(); + + //store last ginfo pointer + GroupQueueInfo* ginfo = m_SelectionPools[teamIndex].SelectedGroups.back(); + + //set itr_team to group that was added to selection pool latest + GroupsQueueType::iterator itr_team = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + static_cast(teamIndex)].begin(); + for (; itr_team != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + static_cast(teamIndex)].end(); ++itr_team) + if (ginfo == *itr_team) + break; + + if (itr_team == m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + static_cast(teamIndex)].end()) + return false; + + GroupsQueueType::iterator itr_team2 = itr_team; + ++itr_team2; + + //invite players to other selection pool + for (; itr_team2 != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + static_cast(teamIndex)].end(); ++itr_team2) + { + //if selection pool is full then break; + if (!(*itr_team2)->IsInvitedToBGInstanceGUID && !m_SelectionPools[otherTeam].AddGroup(*itr_team2, minPlayersPerTeam)) + break; + } + + if (m_SelectionPools[otherTeam].GetPlayerCount() != minPlayersPerTeam) + return false; + + //here we have correct 2 selections and we need to change one teams team and move selection pool teams to other team's queue + for (GroupsQueueType::iterator itr = m_SelectionPools[otherTeam].SelectedGroups.begin(); itr != m_SelectionPools[otherTeam].SelectedGroups.end(); ++itr) + { + //set correct team + (*itr)->teamId = otherTeam; + (*itr)->GroupType = static_cast(BG_QUEUE_NORMAL_ALLIANCE) + static_cast(otherTeam); + + //add team to other queue + m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + static_cast(otherTeam)].push_front(*itr); + + //remove team from old queue + GroupsQueueType::iterator itr2 = itr_team; + ++itr2; + + for (; itr2 != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + static_cast(teamIndex)].end(); ++itr2) { - // clear selection pools - m_SelectionPools[TEAM_ALLIANCE].Init(); - m_SelectionPools[TEAM_HORDE].Init(); - - // fill one queue to both selection pools - for (auto itr = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].begin(); itr != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].end(); ++itr) - for (uint32 j = 0; j < PVP_TEAMS_COUNT; j++) // try to add this group to both pools - if (m_SelectionPools[TEAM_ALLIANCE + j].GetPlayerCount() < minPlayersPerTeam) // if this pool is not full - if (m_SelectionPools[TEAM_ALLIANCE + j].AddGroup((*itr), minPlayersPerTeam)) // successfully added - { - // if both selection pools are full - if (m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() >= minPlayersPerTeam && m_SelectionPools[TEAM_HORDE].GetPlayerCount() >= minPlayersPerTeam) - { - // need to move groups from one pool to another queue (for another faction) - TeamId wrongTeamId = (i == 0 ? TEAM_HORDE : TEAM_ALLIANCE); - - for (auto pitr = m_SelectionPools[wrongTeamId].SelectedGroups.begin(); pitr != m_SelectionPools[wrongTeamId].SelectedGroups.end(); ++pitr) - { - // update internal GroupQueueInfo data - (*pitr)->teamId = wrongTeamId; - (*pitr)->_groupType = static_cast(BG_QUEUE_NORMAL_ALLIANCE) + wrongTeamId; - - // add GroupQueueInfo to new queue - m_QueuedGroups[bracket_id][static_cast(BG_QUEUE_NORMAL_ALLIANCE) + wrongTeamId].push_front(*pitr); - - // remove GroupQueueInfo from old queue - for (auto qitr = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].begin(); qitr != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].end(); ++qitr) - if (*qitr == *pitr) - { - m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].erase(qitr); - break; - } - } - - return true; - } - - break; // added to one pool, prevent adding it to the second pool - } + if (*itr2 == *itr) + { + m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + static_cast(teamIndex)].erase(itr2); + break; + } } + } - return false; + return true; } void BattlegroundQueue::UpdateEvents(uint32 diff) @@ -594,123 +698,117 @@ void BattlegroundQueue::UpdateEvents(uint32 diff) m_events.Update(diff); } -void BattlegroundQueue::BattlegroundQueueUpdate(uint32 diff, BattlegroundBracketId bracket_id, bool isRated, uint32 arenaRatedTeamId) +void BattlegroundQueue::BattlegroundQueueUpdate(uint32 diff, BattlegroundTypeId bgTypeId, BattlegroundBracketId bracket_id, uint8 arenaType, bool isRated, uint32 arenaRating) { // if no players in queue - do nothing if (IsAllQueuesEmpty(bracket_id)) return; - Battleground* bg_template = sBattlegroundMgr->GetBattlegroundTemplate(m_bgTypeId); + auto InviteAllGroupsToBg = [this](Battleground* bg) + { + // invite those selection pools + for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++) + { + for (auto const& citr : m_SelectionPools[TEAM_ALLIANCE + i].SelectedGroups) + { + InviteGroupToBG(citr, bg, citr->teamId); + } + } + }; + + // battleground with free slot for player should be always in the beggining of the queue + // maybe it would be better to create bgfreeslotqueue for each bracket_id + BGFreeSlotQueueContainer& bgQueues = sBattlegroundMgr->GetBGFreeSlotQueueStore(bgTypeId); + for (BGFreeSlotQueueContainer::iterator itr = bgQueues.begin(); itr != bgQueues.end();) + { + Battleground* bg = *itr; ++itr; + // DO NOT allow queue manager to invite new player to rated games + if (!bg->isRated() && bg->GetBgTypeID() == bgTypeId && bg->GetBracketId() == bracket_id && + bg->GetStatus() > STATUS_WAIT_QUEUE && bg->GetStatus() < STATUS_WAIT_LEAVE) + { + // clear selection pools + m_SelectionPools[TEAM_ALLIANCE].Init(); + m_SelectionPools[TEAM_HORDE].Init(); + + // call a function that does the job for us + FillPlayersToBG(bg, bracket_id); + + // now everything is set, invite players + InviteAllGroupsToBg(bg); + + if (!bg->HasFreeSlots()) + bg->RemoveFromBGFreeSlotQueue(); + } + } + + Battleground* bg_template = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); if (!bg_template) + { + LOG_ERROR("bg.battleground", "Battleground: Update: bg template not found for {}", bgTypeId); return; + } PvPDifficultyEntry const* bracketEntry = GetBattlegroundBracketById(bg_template->GetMapId(), bracket_id); if (!bracketEntry) - return; - - // battlegrounds with free slots should be populated first using players in queue - if (!BattlegroundMgr::IsArenaType(m_bgTypeId)) { - std::vector bgsToCheck; - - // sort from most needing (most empty) to least needing using a std::set with functor - sBattlegroundMgr->DoForAllBattlegrounds([&](Battleground* bg) - { - if (!BattlegroundMgr::IsArenaType(bg->GetBgTypeID()) && - (bg->GetBgTypeID(true) == m_bgTypeId || m_bgTypeId == BATTLEGROUND_RB) && - bg->HasFreeSlots() && bg->GetMinLevel() <= bracketEntry->minLevel && - bg->GetMaxLevel() >= bracketEntry->maxLevel) - { - bgsToCheck.emplace_back(bg); - } - }); - - std::sort(bgsToCheck.begin(), bgsToCheck.end(), [](Battleground* const& bg1, Battleground* const& bg2) - { - return ((float)bg1->GetMaxFreeSlots() / (float)bg1->GetMaxPlayersPerTeam()) > ((float)bg2->GetMaxFreeSlots() / (float)bg2->GetMaxPlayersPerTeam()); - }); - - // now iterate needing battlegrounds - for (auto const& bg : bgsToCheck) - { - // call a function that fills whatever we can from normal queues - FillPlayersToBG(bg, bg->GetFreeSlotsForTeam(TEAM_ALLIANCE), bg->GetFreeSlotsForTeam(TEAM_HORDE), bracket_id); - - // invite players - for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++) - for (auto itr : m_SelectionPools[TEAM_ALLIANCE + i].SelectedGroups) - InviteGroupToBG(itr, bg, itr->RealTeamID); - } - - // prevent new BGs to be created if there are some non-empty BGs running - // TODO: note that this is a workaround, - // however it shouldn't cause issues as the queue update is constantly called - if (!bg_template->isArena() && !bgsToCheck.empty()) - return; + LOG_ERROR("bg.battleground", "Battleground: Update: bg bracket entry not found for map {} bracket id {}", bg_template->GetMapId(), bracket_id); + return; } - // finished iterating through battlegrounds with free slots, maybe we need to create a new bg - // get min and max players per team uint32 MinPlayersPerTeam = bg_template->GetMinPlayersPerTeam(); uint32 MaxPlayersPerTeam = bg_template->GetMaxPlayersPerTeam(); if (bg_template->isArena()) { - MinPlayersPerTeam = sBattlegroundMgr->isArenaTesting() ? 1 : m_arenaType; - MaxPlayersPerTeam = m_arenaType; + MinPlayersPerTeam = sBattlegroundMgr->isArenaTesting() ? 1 : arenaType; + MaxPlayersPerTeam = arenaType; } + else if (sBattlegroundMgr->isTesting()) + MinPlayersPerTeam = 1; - sScriptMgr->OnQueueUpdate(this, bracket_id, isRated, arenaRatedTeamId); + sScriptMgr->OnQueueUpdate(this, diff, bgTypeId, bracket_id, arenaType, isRated, arenaRating); + + m_SelectionPools[TEAM_ALLIANCE].Init(); + m_SelectionPools[TEAM_HORDE].Init(); // check if can start new premade battleground - if (bg_template->isBattleground() && m_bgTypeId != BATTLEGROUND_RB) - if (CheckPremadeMatch(bracket_id, MinPlayersPerTeam, MaxPlayersPerTeam)) + if (bg_template->isBattleground() && bgTypeId != BATTLEGROUND_RB && CheckPremadeMatch(bracket_id, MinPlayersPerTeam, MaxPlayersPerTeam)) + { + // create new battleground + Battleground* bg = sBattlegroundMgr->CreateNewBattleground(bgTypeId, bracketEntry, 0, false); + if (!bg) { - // create new battleground - Battleground* bg = sBattlegroundMgr->CreateNewBattleground(m_bgTypeId, bracketEntry->minLevel, bracketEntry->maxLevel, 0, false); - if (!bg) - return; - - // invite players - for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++) - for (auto& SelectedGroup : m_SelectionPools[TEAM_ALLIANCE + i].SelectedGroups) - InviteGroupToBG(SelectedGroup, bg, SelectedGroup->teamId); - - bg->StartBattleground(); - - // now fill the premade bg if possible (only one team for each side has been invited yet) - if (bg->HasFreeSlots()) - { - // call a function that fills whatever we can from normal queues - FillPlayersToBG(bg, bg->GetFreeSlotsForTeam(TEAM_ALLIANCE), bg->GetFreeSlotsForTeam(TEAM_HORDE), bracket_id); - - // invite players - for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++) - for (auto& SelectedGroup : m_SelectionPools[TEAM_ALLIANCE + i].SelectedGroups) - InviteGroupToBG(SelectedGroup, bg, SelectedGroup->teamId); - } + LOG_ERROR("bg.battleground", "BattlegroundQueue::Update - Cannot create battleground: {}", bgTypeId); + return; } + // invite those selection pools + InviteAllGroupsToBg(bg); + + bg->StartBattleground(); + + // clear structures + m_SelectionPools[TEAM_ALLIANCE].Init(); + m_SelectionPools[TEAM_HORDE].Init(); + } + // check if can start new normal battleground or non-rated arena if (!isRated) { if (CheckNormalMatch(bg_template, bracket_id, MinPlayersPerTeam, MaxPlayersPerTeam) || - (bg_template->isArena() && CheckSkirmishForSameFaction(bracket_id, MinPlayersPerTeam))) + (bg_template->isArena() && CheckSkirmishForSameFaction(bracket_id, MinPlayersPerTeam))) { - BattlegroundTypeId newBgTypeId = m_bgTypeId; - uint32 minLvl = bracketEntry->minLevel; - uint32 maxLvl = bracketEntry->maxLevel; - // create new battleground - Battleground* bg = sBattlegroundMgr->CreateNewBattleground(newBgTypeId, minLvl, maxLvl, m_arenaType, false); + Battleground* bg = sBattlegroundMgr->CreateNewBattleground(bgTypeId, bracketEntry, arenaType, false); if (!bg) + { + LOG_ERROR("bg.battleground", "BattlegroundQueue::Update - Cannot create battleground: {}", bgTypeId); return; + } // invite players - for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++) - for (auto& SelectedGroup : m_SelectionPools[TEAM_ALLIANCE + i].SelectedGroups) - InviteGroupToBG(SelectedGroup, bg, SelectedGroup->teamId); + InviteAllGroupsToBg(bg); bg->StartBattleground(); } @@ -718,163 +816,134 @@ void BattlegroundQueue::BattlegroundQueueUpdate(uint32 diff, BattlegroundBracket // check if can start new rated arenas (can create many in single queue update) else if (bg_template->isArena()) { - // pussywizard: everything inside this section is mine, do NOT destroy! + // found out the minimum and maximum ratings the newly added team should battle against + // arenaRating is the rating of the latest joined team, or 0 + // 0 is on (automatic update call) and we must set it to team's with longest wait time + if (!arenaRating) + { + GroupQueueInfo* front1 = nullptr; + GroupQueueInfo* front2 = nullptr; - const uint32 currMSTime = GameTime::GetGameTimeMS().count(); - const uint32 discardTime = sBattlegroundMgr->GetRatingDiscardTimer(); - const uint32 maxDefaultRatingDifference = (MaxPlayersPerTeam > 2 ? 300 : 200); - const uint32 maxCountedMMR = 2500; + if (!m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].empty()) + { + front1 = m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].front(); + arenaRating = front1->ArenaMatchmakerRating; + } + + if (!m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].empty()) + { + front2 = m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].front(); + arenaRating = front2->ArenaMatchmakerRating; + } + + if (front1 && front2) + { + if (front1->JoinTime < front2->JoinTime) + arenaRating = front1->ArenaMatchmakerRating; + } + else if (!front1 && !front2) + return; // queues are empty + } + + //set rating range + uint32 arenaMinRating = (arenaRating <= sBattlegroundMgr->GetMaxRatingDifference()) ? 0 : arenaRating - sBattlegroundMgr->GetMaxRatingDifference(); + uint32 arenaMaxRating = arenaRating + sBattlegroundMgr->GetMaxRatingDifference(); + + // if max rating difference is set and the time past since server startup is greater than the rating discard time + // (after what time the ratings aren't taken into account when making teams) then + // the discard time is current_time - time_to_discard, teams that joined after that, will have their ratings taken into account + // else leave the discard time on 0, this way all ratings will be discarded + // this has to be signed value - when the server starts, this value would be negative and thus overflow + int32 discardTime = GameTime::GetGameTimeMS().count() - sBattlegroundMgr->GetRatingDiscardTimer(); + + // timer for previous opponents + int32 discardOpponentsTime = GameTime::GetGameTimeMS().count() - sWorld->getIntConfig(CONFIG_ARENA_PREV_OPPONENTS_DISCARD_TIMER); // we need to find 2 teams which will play next game GroupsQueueType::iterator itr_teams[PVP_TEAMS_COUNT]; + uint8 found = 0; + uint8 team = 0; - bool increaseItr = true; - bool reverse1 = urand(0, 1) != 0; - for (uint8 ii = BG_QUEUE_PREMADE_ALLIANCE; ii <= BG_QUEUE_PREMADE_HORDE; ii++) + for (uint8 i = BG_QUEUE_PREMADE_ALLIANCE; i < BG_QUEUE_NORMAL_ALLIANCE; i++) { - uint8 i = reverse1 ? (BG_QUEUE_PREMADE_HORDE - ii) : ii; - for (auto itr = m_QueuedGroups[bracket_id][i].begin(); itr != m_QueuedGroups[bracket_id][i].end(); (increaseItr ? ++itr : itr)) + // take the group that joined first + GroupsQueueType::iterator itr2 = m_QueuedGroups[bracket_id][i].begin(); + for (; itr2 != m_QueuedGroups[bracket_id][i].end(); ++itr2) { - increaseItr = true; - - // if arenaRatedTeamId is set - look for oponents only for one team, if not - pair every possible team - if (arenaRatedTeamId != 0 && arenaRatedTeamId != (*itr)->ArenaTeamId) - continue; - if ((*itr)->IsInvitedToBGInstanceGUID) - continue; - - uint32 MMR1 = std::min((*itr)->ArenaMatchmakerRating, maxCountedMMR); - - GroupsQueueType::iterator oponentItr; - uint8 oponentQueue = BG_QUEUE_MAX; - uint32 minOponentMMRDiff = 0xffffffff; - uint8 oponentValid = 0; - - bool reverse2 = urand(0, 1) != 0; - for (uint8 jj = BG_QUEUE_PREMADE_ALLIANCE; jj <= BG_QUEUE_PREMADE_HORDE; jj++) + // if group match conditions, then add it to pool + if (!(*itr2)->IsInvitedToBGInstanceGUID + && (((*itr2)->ArenaMatchmakerRating >= arenaMinRating && (*itr2)->ArenaMatchmakerRating <= arenaMaxRating) + || (int32)(*itr2)->JoinTime < discardTime)) { - uint8 j = reverse2 ? (BG_QUEUE_PREMADE_HORDE - jj) : jj; - bool brk = false; - for (auto itr2 = m_QueuedGroups[bracket_id][j].begin(); itr2 != m_QueuedGroups[bracket_id][j].end(); ++itr2) - { - if ((*itr)->ArenaTeamId == (*itr2)->ArenaTeamId) - continue; - if ((*itr2)->IsInvitedToBGInstanceGUID) - continue; - uint32 MMR2 = std::min((*itr2)->ArenaMatchmakerRating, maxCountedMMR); - uint32 MMRDiff = (MMR2 >= MMR1 ? MMR2 - MMR1 : MMR1 - MMR2); - - uint32 maxAllowedDiff = maxDefaultRatingDifference; - uint32 shorterWaitTime, longerWaitTime; - if (currMSTime - (*itr)->JoinTime <= currMSTime - (*itr2)->JoinTime) - { - shorterWaitTime = currMSTime - (*itr)->JoinTime; - longerWaitTime = currMSTime - (*itr2)->JoinTime; - } - else - { - shorterWaitTime = currMSTime - (*itr2)->JoinTime; - longerWaitTime = currMSTime - (*itr)->JoinTime; - } - if (longerWaitTime >= discardTime) - maxAllowedDiff += 150; - maxAllowedDiff += shorterWaitTime / 600; // increased by 100 for each minute - - // now check if this team is more appropriate than previous ones: - - if (currMSTime - (*itr)->JoinTime >= 20 * MINUTE * IN_MILLISECONDS && (oponentValid < 3 || MMRDiff < minOponentMMRDiff)) // after 20 minutes of waiting, pair with closest mmr, regardless the difference - { - oponentValid = 3; - minOponentMMRDiff = MMRDiff; - oponentItr = itr2; - oponentQueue = j; - } - else if (MMR1 >= 2000 && MMR2 >= 2000 && longerWaitTime >= 2 * discardTime && (oponentValid < 2 || MMRDiff < minOponentMMRDiff)) // after 6 minutes of waiting, pair any 2000+ vs 2000+ - { - oponentValid = 2; - minOponentMMRDiff = MMRDiff; - oponentItr = itr2; - oponentQueue = j; - } - else if (oponentValid < 2 && MMRDiff < minOponentMMRDiff) - { - if (!oponentValid) - { - minOponentMMRDiff = MMRDiff; - oponentItr = itr2; - oponentQueue = j; - if (MMRDiff <= maxAllowedDiff) - oponentValid = 1; - } - if ((MMR1 < 1800 || MMR2 < 1800) && MaxPlayersPerTeam == 2 && MMRDiff <= maxDefaultRatingDifference) // in 2v2 below 1800 mmr - priority for default allowed difference - { - minOponentMMRDiff = MMRDiff; - oponentItr = itr2; - oponentQueue = j; - brk = true; - break; - } - } - } - if (brk) - break; - } - - if (oponentQueue != BG_QUEUE_MAX) - { - if (oponentValid) - { - itr_teams[i] = itr; - itr_teams[i == 0 ? 1 : 0] = oponentItr; - - { - GroupQueueInfo* aTeam = *itr_teams[TEAM_ALLIANCE]; - GroupQueueInfo* hTeam = *itr_teams[TEAM_HORDE]; - Battleground* arena = sBattlegroundMgr->CreateNewBattleground(m_bgTypeId, bracketEntry->minLevel, bracketEntry->maxLevel, m_arenaType, true); - if (!arena) - return; - - aTeam->OpponentsTeamRating = hTeam->ArenaTeamRating; - hTeam->OpponentsTeamRating = aTeam->ArenaTeamRating; - aTeam->OpponentsMatchmakerRating = hTeam->ArenaMatchmakerRating; - hTeam->OpponentsMatchmakerRating = aTeam->ArenaMatchmakerRating; - - // now we must move team if we changed its faction to another faction queue, because then we will spam log by errors in Queue::RemovePlayer - if (aTeam->teamId != TEAM_ALLIANCE) - { - aTeam->_groupType = BG_QUEUE_PREMADE_ALLIANCE; - m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].push_front(aTeam); - m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].erase(itr_teams[TEAM_ALLIANCE]); - increaseItr = false; - itr = m_QueuedGroups[bracket_id][i].begin(); - } - if (hTeam->teamId != TEAM_HORDE) - { - hTeam->_groupType = BG_QUEUE_PREMADE_HORDE; - m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].push_front(hTeam); - m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].erase(itr_teams[TEAM_HORDE]); - increaseItr = false; - itr = m_QueuedGroups[bracket_id][i].begin(); - } - - arena->SetArenaMatchmakerRating(TEAM_ALLIANCE, aTeam->ArenaMatchmakerRating); - arena->SetArenaMatchmakerRating(TEAM_HORDE, hTeam->ArenaMatchmakerRating); - InviteGroupToBG(aTeam, arena, TEAM_ALLIANCE); - InviteGroupToBG(hTeam, arena, TEAM_HORDE); - - arena->StartBattleground(); - } - - if (arenaRatedTeamId) - return; - else - continue; - } - else if (arenaRatedTeamId) - return; + itr_teams[found++] = itr2; + team = i; + break; } } } + + if (!found) + return; + + if (found == 1) + { + for (GroupsQueueType::iterator itr3 = itr_teams[0]; itr3 != m_QueuedGroups[bracket_id][team].end(); ++itr3) + { + if (!(*itr3)->IsInvitedToBGInstanceGUID + && (((*itr3)->ArenaMatchmakerRating >= arenaMinRating && (*itr3)->ArenaMatchmakerRating <= arenaMaxRating) || (int32)(*itr3)->JoinTime < discardTime) + && ((*itr_teams[0])->ArenaTeamId != (*itr3)->PreviousOpponentsTeamId || ((int32)(*itr3)->JoinTime < discardOpponentsTime)) + && (*itr_teams[0])->ArenaTeamId != (*itr3)->ArenaTeamId) + { + itr_teams[found++] = itr3; + break; + } + } + } + + //if we have 2 teams, then start new arena and invite players! + if (found == 2) + { + GroupQueueInfo* aTeam = *itr_teams[TEAM_ALLIANCE]; + GroupQueueInfo* hTeam = *itr_teams[TEAM_HORDE]; + + Battleground* arena = sBattlegroundMgr->CreateNewBattleground(bgTypeId, bracketEntry, arenaType, true); + if (!arena) + { + LOG_ERROR("bg.battleground", "BattlegroundQueue::Update couldn't create arena instance for rated arena match!"); + return; + } + + aTeam->OpponentsTeamRating = hTeam->ArenaTeamRating; + hTeam->OpponentsTeamRating = aTeam->ArenaTeamRating; + aTeam->OpponentsMatchmakerRating = hTeam->ArenaMatchmakerRating; + hTeam->OpponentsMatchmakerRating = aTeam->ArenaMatchmakerRating; + + LOG_DEBUG("bg.battleground", "setting oposite teamrating for team {} to {}", aTeam->ArenaTeamId, aTeam->OpponentsTeamRating); + LOG_DEBUG("bg.battleground", "setting oposite teamrating for team {} to {}", hTeam->ArenaTeamId, hTeam->OpponentsTeamRating); + + // now we must move team if we changed its faction to another faction queue, because then we will spam log by errors in Queue::RemovePlayer + if (aTeam->teamId != TEAM_ALLIANCE) + { + aTeam->GroupType = BG_QUEUE_PREMADE_ALLIANCE; + m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].push_front(aTeam); + m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].erase(itr_teams[TEAM_ALLIANCE]); + } + + if (hTeam->teamId != TEAM_HORDE) + { + hTeam->GroupType = BG_QUEUE_PREMADE_HORDE; + m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].push_front(hTeam); + m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].erase(itr_teams[TEAM_HORDE]); + } + + arena->SetArenaMatchmakerRating(TEAM_ALLIANCE, aTeam->ArenaMatchmakerRating); + arena->SetArenaMatchmakerRating(TEAM_HORDE, hTeam->ArenaMatchmakerRating); + InviteGroupToBG(aTeam, arena, TEAM_ALLIANCE); + InviteGroupToBG(hTeam, arena, TEAM_HORDE); + + LOG_DEBUG("bg.battleground", "Starting rated arena match!"); + arena->StartBattleground(); + } } if (sWorld->getBoolConfig(CONFIG_BATTLEGROUND_QUEUE_ANNOUNCER_TIMED)) @@ -1096,16 +1165,17 @@ int32 BattlegroundQueue::GetQueueAnnouncementTimer(uint32 bracketId) const void BattlegroundQueue::InviteGroupToBG(GroupQueueInfo* ginfo, Battleground* bg, TeamId teamId) { - if (ginfo->IsInvitedToBGInstanceGUID) - return; - // set side if needed if (teamId != TEAM_NEUTRAL) ginfo->teamId = teamId; + if (ginfo->IsInvitedToBGInstanceGUID) + return; + // set invitation ginfo->IsInvitedToBGInstanceGUID = bg->GetInstanceID(); + BattlegroundTypeId bgTypeId = bg->GetBgTypeID(); BattlegroundQueueTypeId bgQueueTypeId = BattlegroundMgr::BGQueueTypeId(ginfo->BgTypeId, ginfo->ArenaType); BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId); @@ -1129,25 +1199,31 @@ void BattlegroundQueue::InviteGroupToBG(GroupQueueInfo* ginfo, Battleground* bg, // increase invited counter for each invited player bg->IncreaseInvitedCount(ginfo->teamId); + player->SetInviteForBattlegroundQueueType(bgQueueTypeId, ginfo->IsInvitedToBGInstanceGUID); + // create remind invite events - BGQueueInviteEvent* inviteEvent = new BGQueueInviteEvent(player->GetGUID(), ginfo->IsInvitedToBGInstanceGUID, ginfo->BgTypeId, ginfo->ArenaType, ginfo->RemoveInviteTime); + BGQueueInviteEvent* inviteEvent = new BGQueueInviteEvent(player->GetGUID(), ginfo->IsInvitedToBGInstanceGUID, bgTypeId, ginfo->ArenaType, ginfo->RemoveInviteTime); bgQueue.AddEvent(inviteEvent, INVITATION_REMIND_TIME); + // create automatic remove events - BGQueueRemoveEvent* removeEvent = new BGQueueRemoveEvent(player->GetGUID(), ginfo->IsInvitedToBGInstanceGUID, bgQueueTypeId, ginfo->RemoveInviteTime); + BGQueueRemoveEvent* removeEvent = new BGQueueRemoveEvent(player->GetGUID(), ginfo->IsInvitedToBGInstanceGUID, bgTypeId, bgQueueTypeId, ginfo->RemoveInviteTime); bgQueue.AddEvent(removeEvent, INVITE_ACCEPT_WAIT_TIME); // Check queueSlot uint32 queueSlot = player->GetBattlegroundQueueIndex(bgQueueTypeId); ASSERT(queueSlot < PLAYER_MAX_BATTLEGROUND_QUEUES); + LOG_DEBUG("bg.battleground", "Battleground: invited player {} {} to BG instance {} queueindex {} bgtype {}", + player->GetName(), player->GetGUID().ToString(), bg->GetInstanceID(), queueSlot, bgTypeId); + // send status packet WorldPacket data; - sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bg, queueSlot, STATUS_WAIT_JOIN, INVITE_ACCEPT_WAIT_TIME, 0, ginfo->ArenaType, TEAM_NEUTRAL, bg->isRated(), ginfo->BgTypeId); + sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bg, queueSlot, STATUS_WAIT_JOIN, INVITE_ACCEPT_WAIT_TIME, 0, ginfo->ArenaType, TEAM_NEUTRAL, bg->isRated()); player->GetSession()->SendPacket(&data); // pussywizard: if (bg->isArena() && bg->isRated()) - bg->ArenaLogEntries[player->GetGUID()].Fill(player->GetName().c_str(), player->GetGUID().GetCounter(), player->GetSession()->GetAccountId(), ginfo->ArenaTeamId, player->GetSession()->GetRemoteAddress()); + bg->ArenaLogEntries[player->GetGUID()].Fill(player->GetName(), player->GetGUID().GetCounter(), player->GetSession()->GetAccountId(), ginfo->ArenaTeamId, player->GetSession()->GetRemoteAddress()); } } @@ -1163,7 +1239,7 @@ bool BGQueueInviteEvent::Execute(uint64 /*e_time*/, uint32 /*p_time*/) if (!player) return true; - Battleground* bg = sBattlegroundMgr->GetBattleground(m_BgInstanceGUID); + Battleground* bg = sBattlegroundMgr->GetBattleground(m_BgInstanceGUID, m_BgTypeId); // if battleground ended, do nothing if (!bg) @@ -1201,7 +1277,7 @@ bool BGQueueRemoveEvent::Execute(uint64 /*e_time*/, uint32 /*p_time*/) if (!player) return true; - Battleground* bg = sBattlegroundMgr->GetBattleground(m_BgInstanceGUID); + Battleground* bg = sBattlegroundMgr->GetBattleground(m_BgInstanceGUID, m_BgTypeId); // battleground can be already deleted, bg may be nullptr! @@ -1226,8 +1302,19 @@ bool BGQueueRemoveEvent::Execute(uint64 /*e_time*/, uint32 /*p_time*/) sScriptMgr->OnBattlegroundDesertion(player, BG_DESERTION_TYPE_NO_ENTER_BUTTON); } + + LOG_DEBUG("bg.battleground", "Battleground: removing player {} from bg queue for instance {} because of not pressing enter battle in time.", player->GetGUID().ToString(), m_BgInstanceGUID); + player->RemoveBattlegroundQueueId(m_BgQueueTypeId); - bgQueue.RemovePlayer(m_PlayerGuid, false, queueSlot); + bgQueue.RemovePlayer(m_PlayerGuid, true); + + //update queues if battleground isn't ended + if (bg && bg->isBattleground() && bg->GetStatus() != STATUS_WAIT_LEAVE) + sBattlegroundMgr->ScheduleQueueUpdate(0, 0, m_BgQueueTypeId, m_BgTypeId, bg->GetBracketId()); + + WorldPacket data; + sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bg, queueSlot, STATUS_NONE, 0, 0, 0, TEAM_NEUTRAL); + player->SendDirectMessage(&data); } } diff --git a/src/server/game/Battlegrounds/BattlegroundQueue.h b/src/server/game/Battlegrounds/BattlegroundQueue.h index b2b10eb09..1ea357fe7 100644 --- a/src/server/game/Battlegrounds/BattlegroundQueue.h +++ b/src/server/game/Battlegrounds/BattlegroundQueue.h @@ -25,7 +25,7 @@ #include #include -#define COUNT_OF_PLAYERS_TO_AVERAGE_WAIT_TIME 10 +constexpr auto COUNT_OF_PLAYERS_TO_AVERAGE_WAIT_TIME = 10; struct GroupQueueInfo // stores information about the group in queue (also used when joined as solo!) { @@ -43,10 +43,9 @@ struct GroupQueueInfo // stores informatio uint32 ArenaMatchmakerRating; // if rated match, inited to the rating of the team uint32 OpponentsTeamRating; // for rated arena matches uint32 OpponentsMatchmakerRating; // for rated arena matches - - // pussywizard: for internal use - uint8 _bracketId; - uint8 _groupType; + uint32 PreviousOpponentsTeamId; // excluded from the current queue until the timer is met + uint8 BracketId; // BattlegroundBracketId + uint8 GroupType; // BattlegroundQueueGroupTypes }; enum BattlegroundQueueGroupTypes @@ -61,22 +60,21 @@ enum BattlegroundQueueGroupTypes BG_QUEUE_MAX = 10 }; -class Battleground; class BattlegroundQueue { public: BattlegroundQueue(); ~BattlegroundQueue(); - void BattlegroundQueueUpdate(uint32 diff, BattlegroundBracketId bracket_id, bool isRated, uint32 arenaRatedTeamId); + void BattlegroundQueueUpdate(uint32 diff, BattlegroundTypeId bgTypeId, BattlegroundBracketId bracket_id, uint8 arenaType, bool isRated, uint32 arenaRating); void UpdateEvents(uint32 diff); - void FillPlayersToBG(Battleground* bg, int32 aliFree, int32 hordeFree, BattlegroundBracketId bracket_id); + void FillPlayersToBG(Battleground* bg, BattlegroundBracketId bracket_id); bool CheckPremadeMatch(BattlegroundBracketId bracket_id, uint32 MinPlayersPerTeam, uint32 MaxPlayersPerTeam); bool CheckNormalMatch(Battleground* bgTemplate, BattlegroundBracketId bracket_id, uint32 minPlayers, uint32 maxPlayers); bool CheckSkirmishForSameFaction(BattlegroundBracketId bracket_id, uint32 minPlayersPerTeam); - GroupQueueInfo* AddGroup(Player* leader, Group* group, PvPDifficultyEntry const* bracketEntry, bool isRated, bool isPremade, uint32 ArenaRating, uint32 MatchmakerRating, uint32 ArenaTeamId); - void RemovePlayer(ObjectGuid guid, bool sentToBg, uint32 playerQueueSlot); + GroupQueueInfo* AddGroup(Player* leader, Group* group, BattlegroundTypeId bgTypeId, PvPDifficultyEntry const* bracketEntry, uint8 arenaType, bool isRated, bool isPremade, uint32 arenaRating, uint32 matchmakerRating, uint32 arenaTeamId = 0, uint32 opponentsArenaTeamId = 0); + void RemovePlayer(ObjectGuid guid, bool decreaseInvitedCount); bool IsPlayerInvitedToRatedArena(ObjectGuid pl_guid); bool IsPlayerInvited(ObjectGuid pl_guid, uint32 bgInstanceGuid, uint32 removeTime); bool GetPlayerGroupInfoData(ObjectGuid guid, GroupQueueInfo* ginfo); @@ -89,7 +87,6 @@ public: void SendJoinMessageArenaQueue(Player* leader, GroupQueueInfo* ginfo, PvPDifficultyEntry const* bracketEntry, bool isRated); void SendExitMessageArenaQueue(GroupQueueInfo* ginfo); - void SetBgTypeIdAndArenaType(BattlegroundTypeId b, uint8 a) { m_bgTypeId = b; m_arenaType = ArenaType(a); } // pussywizard void AddEvent(BasicEvent* Event, uint64 e_time); typedef std::map QueuedPlayersMap; @@ -127,15 +124,10 @@ public: //one selection pool for horde, other one for alliance SelectionPool m_SelectionPools[PVP_TEAMS_COUNT]; - ArenaType GetArenaType() { return m_arenaType; } - BattlegroundTypeId GetBGTypeID() { return m_bgTypeId; } - void SetQueueAnnouncementTimer(uint32 bracketId, int32 timer, bool isCrossFactionBG = true); [[nodiscard]] int32 GetQueueAnnouncementTimer(uint32 bracketId) const; private: - BattlegroundTypeId m_bgTypeId; - ArenaType m_arenaType; uint32 m_WaitTimes[PVP_TEAMS_COUNT][MAX_BATTLEGROUND_BRACKETS][COUNT_OF_PLAYERS_TO_AVERAGE_WAIT_TIME]; uint32 m_WaitTimeLastIndex[PVP_TEAMS_COUNT][MAX_BATTLEGROUND_BRACKETS]; @@ -176,9 +168,8 @@ private: class BGQueueRemoveEvent : public BasicEvent { public: - BGQueueRemoveEvent(ObjectGuid pl_guid, uint32 bgInstanceGUID, BattlegroundQueueTypeId bgQueueTypeId, uint32 removeTime) - : m_PlayerGuid(pl_guid), m_BgInstanceGUID(bgInstanceGUID), m_RemoveTime(removeTime), m_BgQueueTypeId(bgQueueTypeId) - {} + BGQueueRemoveEvent(ObjectGuid pl_guid, uint32 bgInstanceGUID, BattlegroundTypeId BgTypeId, BattlegroundQueueTypeId bgQueueTypeId, uint32 removeTime) : + m_PlayerGuid(pl_guid), m_BgInstanceGUID(bgInstanceGUID), m_RemoveTime(removeTime), m_BgTypeId(BgTypeId), m_BgQueueTypeId(bgQueueTypeId) { } ~BGQueueRemoveEvent() override = default; @@ -188,6 +179,7 @@ private: ObjectGuid m_PlayerGuid; uint32 m_BgInstanceGUID; uint32 m_RemoveTime; + BattlegroundTypeId m_BgTypeId; BattlegroundQueueTypeId m_BgQueueTypeId; }; diff --git a/src/server/game/Conditions/ConditionMgr.cpp b/src/server/game/Conditions/ConditionMgr.cpp index 2d7afe538..163412d25 100644 --- a/src/server/game/Conditions/ConditionMgr.cpp +++ b/src/server/game/Conditions/ConditionMgr.cpp @@ -61,19 +61,27 @@ bool Condition::Meets(ConditionSourceInfo& sourceInfo) } case CONDITION_ITEM: { - if (Player* player = object->ToPlayer()) + if (Unit* unit = object->ToUnit()) { - // don't allow 0 items (it's checked during table load) - ASSERT(ConditionValue2); - bool checkBank = !!ConditionValue3; - condMeets = player->HasItemCount(ConditionValue1, ConditionValue2, checkBank); + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + // don't allow 0 items (it's checked during table load) + ASSERT(ConditionValue2); + bool checkBank = !!ConditionValue3; + condMeets = player->HasItemCount(ConditionValue1, ConditionValue2, checkBank); + } } break; } case CONDITION_ITEM_EQUIPPED: { - if (Player* player = object->ToPlayer()) - condMeets = player->HasItemOrGemWithIdEquipped(ConditionValue1, 1); + if (Unit* unit = object->ToUnit()) + { + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + condMeets = player->HasItemOrGemWithIdEquipped(ConditionValue1, 1); + } + } break; } case CONDITION_ZONEID: @@ -81,26 +89,39 @@ bool Condition::Meets(ConditionSourceInfo& sourceInfo) break; case CONDITION_REPUTATION_RANK: { - if (Player* player = object->ToPlayer()) + if (Unit* unit = object->ToUnit()) { - if (FactionEntry const* faction = sFactionStore.LookupEntry(ConditionValue1)) - condMeets = (ConditionValue2 & (1 << player->GetReputationMgr().GetRank(faction))); + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + if (FactionEntry const* faction = sFactionStore.LookupEntry(ConditionValue1)) + { + condMeets = (ConditionValue2 & (1 << player->GetReputationMgr().GetRank(faction))); + } + } } break; } case CONDITION_ACHIEVEMENT: { - if (Player* player = object->ToPlayer()) - condMeets = player->HasAchieved(ConditionValue1); + if (Unit* unit = object->ToUnit()) + { + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + condMeets = player->HasAchieved(ConditionValue1); + } + } break; } case CONDITION_TEAM: { - if (Player* player = object->ToPlayer()) + if (Unit* unit = object->ToUnit()) { - // Xinef: DB Data compatibility... - uint32 teamOld = player->GetTeamId() == TEAM_ALLIANCE ? ALLIANCE : HORDE; - condMeets = teamOld == ConditionValue1; + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + // Xinef: DB Data compatibility... + uint32 teamOld = player->GetTeamId() == TEAM_ALLIANCE ? ALLIANCE : HORDE; + condMeets = teamOld == ConditionValue1; + } } break; } @@ -118,56 +139,83 @@ bool Condition::Meets(ConditionSourceInfo& sourceInfo) } case CONDITION_GENDER: { - if (Player* player = object->ToPlayer()) - condMeets = player->getGender() == ConditionValue1; + if (Unit* unit = object->ToUnit()) + { + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + condMeets = player->getGender() == ConditionValue1; + } + } break; } case CONDITION_SKILL: { - if (Player* player = object->ToPlayer()) - condMeets = player->HasSkill(ConditionValue1) && player->GetBaseSkillValue(ConditionValue1) >= ConditionValue2; + if (Unit* unit = object->ToUnit()) + { + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + condMeets = player->HasSkill(ConditionValue1) && player->GetBaseSkillValue(ConditionValue1) >= ConditionValue2; + } + } break; } case CONDITION_QUESTREWARDED: { - if (Player* player = object->ToPlayer()) - condMeets = player->GetQuestRewardStatus(ConditionValue1); + if (Unit* unit = object->ToUnit()) + { + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + condMeets = player->GetQuestRewardStatus(ConditionValue1); + } + } break; } case CONDITION_QUESTTAKEN: { - if (Player* player = object->ToPlayer()) + if (Unit* unit = object->ToUnit()) { - QuestStatus status = player->GetQuestStatus(ConditionValue1); - condMeets = (status == QUEST_STATUS_INCOMPLETE); + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + QuestStatus status = player->GetQuestStatus(ConditionValue1); + condMeets = (status == QUEST_STATUS_INCOMPLETE); + } } break; } case CONDITION_QUEST_COMPLETE: { - if (Player* player = object->ToPlayer()) + if (Unit* unit = object->ToUnit()) { - QuestStatus status = player->GetQuestStatus(ConditionValue1); - condMeets = (status == QUEST_STATUS_COMPLETE && !player->GetQuestRewardStatus(ConditionValue1)); + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + QuestStatus status = player->GetQuestStatus(ConditionValue1); + condMeets = (status == QUEST_STATUS_COMPLETE && !player->GetQuestRewardStatus(ConditionValue1)); + } } break; } case CONDITION_QUEST_NONE: { - if (Player* player = object->ToPlayer()) + if (Unit* unit = object->ToUnit()) { - QuestStatus status = player->GetQuestStatus(ConditionValue1); - condMeets = (status == QUEST_STATUS_NONE); + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + QuestStatus status = player->GetQuestStatus(ConditionValue1); + condMeets = (status == QUEST_STATUS_NONE); + } } break; } case CONDITION_QUEST_SATISFY_EXCLUSIVE: { - if (Player* player = object->ToPlayer()) + if (Unit* unit = object->ToUnit()) { - // Xinef: cannot be null, checked at loading - const Quest* quest = sObjectMgr->GetQuestTemplate(ConditionValue1); - condMeets = !player->IsQuestRewarded(ConditionValue1) && player->SatisfyQuestExclusiveGroup(quest, false); + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + // Xinef: cannot be null, checked at loading + const Quest* quest = sObjectMgr->GetQuestTemplate(ConditionValue1); + condMeets = !player->IsQuestRewarded(ConditionValue1) && player->SatisfyQuestExclusiveGroup(quest, false); + } } break; } @@ -183,18 +231,18 @@ bool Condition::Meets(ConditionSourceInfo& sourceInfo) { switch (ConditionValue3) { - case INSTANCE_INFO_DATA: - condMeets = instance->GetData(ConditionValue1) == ConditionValue2; - break; - case INSTANCE_INFO_GUID_DATA: - condMeets = instance->GetGuidData(ConditionValue1) == ObjectGuid(uint64(ConditionValue2)); - break; - case INSTANCE_INFO_BOSS_STATE: - condMeets = instance->GetBossState(ConditionValue1) == EncounterState(ConditionValue2); - break; - case INSTANCE_INFO_DATA64: - condMeets = instance->GetData64(ConditionValue1) == ConditionValue2; - break; + case INSTANCE_INFO_DATA: + condMeets = instance->GetData(ConditionValue1) == ConditionValue2; + break; + case INSTANCE_INFO_GUID_DATA: + condMeets = instance->GetGuidData(ConditionValue1) == ObjectGuid(uint64(ConditionValue2)); + break; + case INSTANCE_INFO_BOSS_STATE: + condMeets = instance->GetBossState(ConditionValue1) == EncounterState(ConditionValue2); + break; + case INSTANCE_INFO_DATA64: + condMeets = instance->GetData64(ConditionValue1) == ConditionValue2; + break; } } } @@ -208,8 +256,13 @@ bool Condition::Meets(ConditionSourceInfo& sourceInfo) break; case CONDITION_SPELL: { - if (Player* player = object->ToPlayer()) - condMeets = player->HasSpell(ConditionValue1); + if (Unit* unit = object->ToUnit()) + { + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + condMeets = player->HasSpell(ConditionValue1); + } + } break; } case CONDITION_LEVEL: @@ -220,8 +273,13 @@ bool Condition::Meets(ConditionSourceInfo& sourceInfo) } case CONDITION_DRUNKENSTATE: { - if (Player* player = object->ToPlayer()) - condMeets = (uint32) Player::GetDrunkenstateByValue(player->GetDrunkValue()) >= ConditionValue1; + if (Unit* unit = object->ToUnit()) + { + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + condMeets = (uint32)Player::GetDrunkenstateByValue(player->GetDrunkValue()) >= ConditionValue1; + } + } break; } case CONDITION_NEAR_CREATURE: @@ -248,14 +306,14 @@ bool Condition::Meets(ConditionSourceInfo& sourceInfo) { switch (object->GetTypeId()) { - case TYPEID_UNIT: - condMeets &= object->ToCreature()->GetSpawnId() == ConditionValue3; - break; - case TYPEID_GAMEOBJECT: - condMeets &= object->ToGameObject()->GetSpawnId() == ConditionValue3; - break; - default: - break; + case TYPEID_UNIT: + condMeets &= object->ToCreature()->GetSpawnId() == ConditionValue3; + break; + case TYPEID_GAMEOBJECT: + condMeets &= object->ToGameObject()->GetSpawnId() == ConditionValue3; + break; + default: + break; } } } @@ -276,24 +334,24 @@ bool Condition::Meets(ConditionSourceInfo& sourceInfo) { switch (ConditionValue2) { - case RELATION_SELF: - condMeets = unit == toUnit; - break; - case RELATION_IN_PARTY: - condMeets = unit->IsInPartyWith(toUnit); - break; - case RELATION_IN_RAID_OR_PARTY: - condMeets = unit->IsInRaidWith(toUnit); - break; - case RELATION_OWNED_BY: - condMeets = unit->GetOwnerGUID() == toUnit->GetGUID(); - break; - case RELATION_PASSENGER_OF: - condMeets = unit->IsOnVehicle(toUnit); - break; - case RELATION_CREATED_BY: - condMeets = unit->GetCreatorGUID() == toUnit->GetGUID(); - break; + case RELATION_SELF: + condMeets = unit == toUnit; + break; + case RELATION_IN_PARTY: + condMeets = unit->IsInPartyWith(toUnit); + break; + case RELATION_IN_RAID_OR_PARTY: + condMeets = unit->IsInRaidWith(toUnit); + break; + case RELATION_OWNED_BY: + condMeets = unit->GetOwnerGUID() == toUnit->GetGUID(); + break; + case RELATION_PASSENGER_OF: + condMeets = unit->IsOnVehicle(toUnit); + break; + case RELATION_CREATED_BY: + condMeets = unit->GetCreatorGUID() == toUnit->GetGUID(); + break; } } } @@ -346,8 +404,13 @@ bool Condition::Meets(ConditionSourceInfo& sourceInfo) } case CONDITION_TITLE: { - if (Player* player = object->ToPlayer()) - condMeets = player->HasTitle(ConditionValue1); + if (Unit* unit = object->ToUnit()) + { + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + condMeets = player->HasTitle(ConditionValue1); + } + } break; } case CONDITION_SPAWNMASK: @@ -382,35 +445,52 @@ bool Condition::Meets(ConditionSourceInfo& sourceInfo) } case CONDITION_QUESTSTATE: { - if (Player* player = object->ToPlayer()) + if (Unit* unit = object->ToUnit()) { - uint32 queststateConditionValue1 = player->GetQuestStatus(ConditionValue1); - if (((ConditionValue2 & (1 << QUEST_STATUS_NONE)) && (queststateConditionValue1 == QUEST_STATUS_NONE)) || ((ConditionValue2 & (1 << QUEST_STATUS_COMPLETE)) && (queststateConditionValue1 == QUEST_STATUS_COMPLETE)) || ((ConditionValue2 & (1 << QUEST_STATUS_INCOMPLETE)) && (queststateConditionValue1 == QUEST_STATUS_INCOMPLETE)) || ((ConditionValue2 & (1 << QUEST_STATUS_FAILED)) && (queststateConditionValue1 == QUEST_STATUS_FAILED)) || - ((ConditionValue2 & (1 << QUEST_STATUS_REWARDED)) && player->GetQuestRewardStatus(ConditionValue1))) + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) { - condMeets = true; + uint32 queststateConditionValue1 = player->GetQuestStatus(ConditionValue1); + if (((ConditionValue2 & (1 << QUEST_STATUS_NONE)) && (queststateConditionValue1 == QUEST_STATUS_NONE)) || + ((ConditionValue2 & (1 << QUEST_STATUS_COMPLETE)) && (queststateConditionValue1 == QUEST_STATUS_COMPLETE)) || + ((ConditionValue2 & (1 << QUEST_STATUS_INCOMPLETE)) && (queststateConditionValue1 == QUEST_STATUS_INCOMPLETE)) || + ((ConditionValue2 & (1 << QUEST_STATUS_FAILED)) && (queststateConditionValue1 == QUEST_STATUS_FAILED)) || + ((ConditionValue2 & (1 << QUEST_STATUS_REWARDED)) && player->GetQuestRewardStatus(ConditionValue1))) + { + condMeets = true; + } } } break; } case CONDITION_DAILY_QUEST_DONE: { - if (Player* player = object->ToPlayer()) + if (Unit* unit = object->ToUnit()) { - condMeets = player->IsDailyQuestDone(ConditionValue1); + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + condMeets = player->IsDailyQuestDone(ConditionValue1); + } } break; } case CONDITION_QUEST_OBJECTIVE_PROGRESS: { - if (Player* player = object->ToPlayer()) + if (Unit* unit = object->ToUnit()) { - const Quest* quest = ASSERT_NOTNULL(sObjectMgr->GetQuestTemplate(ConditionValue1)); - uint16 log_slot = player->FindQuestSlot(quest->GetQuestId()); - if (log_slot >= MAX_QUEST_LOG_SIZE) - break; - if (player->GetQuestSlotCounter(log_slot, ConditionValue2) == ConditionValue3) - condMeets = true; + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + Quest const* quest = ASSERT_NOTNULL(sObjectMgr->GetQuestTemplate(ConditionValue1)); + uint16 log_slot = player->FindQuestSlot(quest->GetQuestId()); + if (log_slot >= MAX_QUEST_LOG_SIZE) + { + break; + } + + if (player->GetQuestSlotCounter(log_slot, ConditionValue2) == ConditionValue3) + { + condMeets = true; + } + } } break; } @@ -427,15 +507,27 @@ bool Condition::Meets(ConditionSourceInfo& sourceInfo) } case CONDITION_PET_TYPE: { - if (Player* player = object->ToPlayer()) - if (Pet* pet = player->GetPet()) - condMeets = (((1 << pet->getPetType()) & ConditionValue1) != 0); + if (Unit* unit = object->ToUnit()) + { + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + if (Pet* pet = player->GetPet()) + { + condMeets = (((1 << pet->getPetType()) & ConditionValue1) != 0); + } + } + } break; } case CONDITION_TAXI: { - if (Player* player = object->ToPlayer()) - condMeets = player->IsInFlight(); + if (Unit* unit = object->ToUnit()) + { + if (Player* player = unit->GetCharmerOrOwnerPlayerOrPlayerItself()) + { + condMeets = player->IsInFlight(); + } + } break; } case CONDITION_CHARMED: diff --git a/src/server/game/DataStores/M2Stores.cpp b/src/server/game/DataStores/M2Stores.cpp index ca8f5008f..e2b81712a 100644 --- a/src/server/game/DataStores/M2Stores.cpp +++ b/src/server/game/DataStores/M2Stores.cpp @@ -250,7 +250,7 @@ void LoadM2Cameras(std::string const& dataPath) LOG_ERROR("server.loading", "Camera file %s is damaged. Camera references position beyond file end", filename.string().c_str()); } - LOG_INFO("server.loading", ">> Loaded %u cinematic waypoint sets in %u ms", (uint32)sFlyByCameraStore.size(), GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", ">> Loaded {} cinematic waypoint sets in {} ms", (uint32)sFlyByCameraStore.size(), GetMSTimeDiffToNow(oldMSTime)); } std::vector const* GetFlyByCameras(uint32 cinematicCameraId) diff --git a/src/server/game/DungeonFinding/LFGMgr.cpp b/src/server/game/DungeonFinding/LFGMgr.cpp index 7f763775c..3dad1840d 100644 --- a/src/server/game/DungeonFinding/LFGMgr.cpp +++ b/src/server/game/DungeonFinding/LFGMgr.cpp @@ -1708,7 +1708,7 @@ namespace lfg if (bgQueueTypeId != BATTLEGROUND_QUEUE_NONE) { plr->RemoveBattlegroundQueueId(bgQueueTypeId); - sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId).RemovePlayer(plr->GetGUID(), false, i); + sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId).RemovePlayer(plr->GetGUID(), true); } } } diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 590e9d8c8..ff04227cb 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -248,7 +248,10 @@ Player::Player(WorldSession* session): Unit(true), m_mover(this) m_swingErrorMsg = 0; for (uint8 j = 0; j < PLAYER_MAX_BATTLEGROUND_QUEUES; ++j) - m_bgBattlegroundQueueID[j] = BATTLEGROUND_QUEUE_NONE; + { + _BgBattlegroundQueueID[j].bgQueueTypeId = BATTLEGROUND_QUEUE_NONE; + _BgBattlegroundQueueID[j].invitedToInstance = 0; + } m_logintime = GameTime::GetGameTime().count(); m_Last_tick = m_logintime; @@ -11025,6 +11028,8 @@ void Player::LeaveBattleground(Battleground* bg) sScriptMgr->OnBattlegroundDesertion(this, BG_DESERTION_TYPE_LEAVE_BG); } + bg->RemovePlayerAtLeave(this); + // xinef: reset corpse reclaim time m_deathExpireTime = GameTime::GetGameTime().count(); @@ -11853,10 +11858,102 @@ Battleground* Player::GetBattleground(bool create) const if (GetBattlegroundId() == 0) return nullptr; - Battleground* bg = sBattlegroundMgr->GetBattleground(GetBattlegroundId()); + Battleground* bg = sBattlegroundMgr->GetBattleground(GetBattlegroundId(), GetBattlegroundTypeId()); return (create || (bg && bg->FindBgMap()) ? bg : nullptr); } +bool Player::InBattlegroundQueue(bool ignoreArena) const +{ + for (uint8 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + if (_BgBattlegroundQueueID[i].bgQueueTypeId != BATTLEGROUND_QUEUE_NONE && + (!ignoreArena || (_BgBattlegroundQueueID[i].bgQueueTypeId != BATTLEGROUND_QUEUE_2v2 && + _BgBattlegroundQueueID[i].bgQueueTypeId != BATTLEGROUND_QUEUE_3v3 && + _BgBattlegroundQueueID[i].bgQueueTypeId != BATTLEGROUND_QUEUE_5v5))) + return true; + return false; +} + +BattlegroundQueueTypeId Player::GetBattlegroundQueueTypeId(uint32 index) const +{ + return _BgBattlegroundQueueID[index].bgQueueTypeId; +} + +uint32 Player::GetBattlegroundQueueIndex(BattlegroundQueueTypeId bgQueueTypeId) const +{ + for (uint8 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + if (_BgBattlegroundQueueID[i].bgQueueTypeId == bgQueueTypeId) + return i; + + return PLAYER_MAX_BATTLEGROUND_QUEUES; +} + +bool Player::IsInvitedForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId) const +{ + for (uint8 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + if (_BgBattlegroundQueueID[i].bgQueueTypeId == bgQueueTypeId) + return _BgBattlegroundQueueID[i].invitedToInstance != 0; + + return false; +} + +bool Player::InBattlegroundQueueForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId) const +{ + return GetBattlegroundQueueIndex(bgQueueTypeId) < PLAYER_MAX_BATTLEGROUND_QUEUES; +} + +uint32 Player::AddBattlegroundQueueId(BattlegroundQueueTypeId val) +{ + for (uint8 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + { + if (_BgBattlegroundQueueID[i].bgQueueTypeId == BATTLEGROUND_QUEUE_NONE || _BgBattlegroundQueueID[i].bgQueueTypeId == val) + { + _BgBattlegroundQueueID[i].bgQueueTypeId = val; + _BgBattlegroundQueueID[i].invitedToInstance = 0; + return i; + } + } + + return PLAYER_MAX_BATTLEGROUND_QUEUES; +} + +bool Player::HasFreeBattlegroundQueueId() const +{ + for (uint8 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + if (_BgBattlegroundQueueID[i].bgQueueTypeId == BATTLEGROUND_QUEUE_NONE) + return true; + + return false; +} + +void Player::RemoveBattlegroundQueueId(BattlegroundQueueTypeId val) +{ + for (uint8 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + { + if (_BgBattlegroundQueueID[i].bgQueueTypeId == val) + { + _BgBattlegroundQueueID[i].bgQueueTypeId = BATTLEGROUND_QUEUE_NONE; + _BgBattlegroundQueueID[i].invitedToInstance = 0; + return; + } + } +} + +void Player::SetInviteForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId, uint32 instanceId) +{ + for (uint8 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + if (_BgBattlegroundQueueID[i].bgQueueTypeId == bgQueueTypeId) + _BgBattlegroundQueueID[i].invitedToInstance = instanceId; +} + +bool Player::IsInvitedForBattlegroundInstance(uint32 instanceId) const +{ + for (uint8 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) + if (_BgBattlegroundQueueID[i].invitedToInstance == instanceId) + return true; + + return false; +} + bool Player::InArena() const { Battleground* bg = GetBattleground(); @@ -11868,16 +11965,6 @@ bool Player::InArena() const void Player::SetBattlegroundId(uint32 id, BattlegroundTypeId bgTypeId, uint32 queueSlot, bool invited, bool isRandom, TeamId teamId) { - // if leaving current bg (and was invited) - decrease invited count for current one - if (m_bgData.bgInstanceID && m_bgData.isInvited) - if (Battleground* bg = sBattlegroundMgr->GetBattleground(m_bgData.bgInstanceID)) - bg->DecreaseInvitedCount(m_bgData.bgTeamId); - - // if entering new bg (and is invited) - increase invited count for new one - if (id && invited) - if (Battleground* bg = sBattlegroundMgr->GetBattleground(id)) - bg->IncreaseInvitedCount(teamId); - m_bgData.bgInstanceID = id; m_bgData.bgTypeID = bgTypeId; m_bgData.bgQueueSlot = queueSlot; diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 3896615e5..1d2b0246b 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -177,7 +177,7 @@ enum TalentTree // talent tabs // Spell modifier (used for modify other spells) struct SpellModifier { - SpellModifier(Aura* _ownerAura = nullptr) : op(SPELLMOD_DAMAGE), type(SPELLMOD_FLAT), charges(0), mask(), ownerAura(_ownerAura) {} + SpellModifier(Aura* _ownerAura = nullptr) : op(SPELLMOD_DAMAGE), type(SPELLMOD_FLAT), charges(0), mask(), ownerAura(_ownerAura) {} SpellModOp op : 8; SpellModType type : 8; int16 charges : 16; @@ -2188,59 +2188,20 @@ public: void SetBGData(BGData& bgdata) { m_bgData = bgdata; } [[nodiscard]] Battleground* GetBattleground(bool create = false) const; - [[nodiscard]] bool InBattlegroundQueue() const - { - for (auto i : m_bgBattlegroundQueueID) - if (i != BATTLEGROUND_QUEUE_NONE) - return true; - return false; - } + [[nodiscard]] bool InBattlegroundQueue(bool ignoreArena = false) const; + [[nodiscard]] bool IsDeserter() const { return HasAura(26013); } - [[nodiscard]] BattlegroundQueueTypeId GetBattlegroundQueueTypeId(uint32 index) const { return m_bgBattlegroundQueueID[index]; } - - [[nodiscard]] uint32 GetBattlegroundQueueIndex(BattlegroundQueueTypeId bgQueueTypeId) const - { - for (uint8 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) - if (m_bgBattlegroundQueueID[i] == bgQueueTypeId) - return i; - return PLAYER_MAX_BATTLEGROUND_QUEUES; - } - - [[nodiscard]] bool InBattlegroundQueueForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId) const - { - return GetBattlegroundQueueIndex(bgQueueTypeId) < PLAYER_MAX_BATTLEGROUND_QUEUES; - } + [[nodiscard]] BattlegroundQueueTypeId GetBattlegroundQueueTypeId(uint32 index) const; + [[nodiscard]] uint32 GetBattlegroundQueueIndex(BattlegroundQueueTypeId bgQueueTypeId) const; + [[nodiscard]] bool IsInvitedForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId) const; + [[nodiscard]] bool InBattlegroundQueueForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId) const; void SetBattlegroundId(uint32 id, BattlegroundTypeId bgTypeId, uint32 queueSlot, bool invited, bool isRandom, TeamId teamId); - - uint32 AddBattlegroundQueueId(BattlegroundQueueTypeId val) - { - for (uint8 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) - if (m_bgBattlegroundQueueID[i] == BATTLEGROUND_QUEUE_NONE || m_bgBattlegroundQueueID[i] == val) - { - m_bgBattlegroundQueueID[i] = val; - return i; - } - return PLAYER_MAX_BATTLEGROUND_QUEUES; - } - - bool HasFreeBattlegroundQueueId() - { - for (auto & i : m_bgBattlegroundQueueID) - if (i == BATTLEGROUND_QUEUE_NONE) - return true; - return false; - } - - void RemoveBattlegroundQueueId(BattlegroundQueueTypeId val) - { - for (auto & i : m_bgBattlegroundQueueID) - if (i == val) - { - i = BATTLEGROUND_QUEUE_NONE; - return; - } - } + uint32 AddBattlegroundQueueId(BattlegroundQueueTypeId val); + bool HasFreeBattlegroundQueueId() const; + void RemoveBattlegroundQueueId(BattlegroundQueueTypeId val); + void SetInviteForBattlegroundQueueType(BattlegroundQueueTypeId bgQueueTypeId, uint32 instanceId); + bool IsInvitedForBattlegroundInstance(uint32 instanceId) const; [[nodiscard]] TeamId GetBgTeamId() const { return m_bgData.bgTeamId != TEAM_NEUTRAL ? m_bgData.bgTeamId : GetTeamId(); } @@ -2601,7 +2562,13 @@ public: /*** BATTLEGROUND SYSTEM ***/ /*********************************************************/ - BattlegroundQueueTypeId m_bgBattlegroundQueueID[PLAYER_MAX_BATTLEGROUND_QUEUES]; + struct BgBattlegroundQueueID_Rec + { + BattlegroundQueueTypeId bgQueueTypeId; + uint32 invitedToInstance; + }; + + std::array _BgBattlegroundQueueID; BGData m_bgData; bool m_IsBGRandomWinner; diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 3cd4b73e4..721a2973c 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -7233,6 +7233,12 @@ bool Unit::HandleDummyAuraProc(Unit* victim, uint32 damage, AuraEffect* triggere if (!victim) return false; + // Do not proc from Glyph of Holy Light and Judgement of Light + if (procSpell->Id == 20267 || procSpell->Id == 54968) + { + return false; + } + Unit* beaconTarget = triggeredByAura->GetBase()->GetCaster(); if (!beaconTarget || beaconTarget == this || !beaconTarget->GetAura(53563, victim->GetGUID())) return false; diff --git a/src/server/game/Groups/Group.cpp b/src/server/game/Groups/Group.cpp index 7549e58eb..c638cfbf0 100644 --- a/src/server/game/Groups/Group.cpp +++ b/src/server/game/Groups/Group.cpp @@ -2189,6 +2189,11 @@ ObjectGuid Group::GetLeaderGUID() const return m_leaderGuid; } +Player* Group::GetLeader() +{ + return ObjectAccessor::FindConnectedPlayer(m_leaderGuid); +} + ObjectGuid Group::GetGUID() const { return m_guid; @@ -2425,3 +2430,15 @@ void Group::SetDifficultyChangePrevention(DifficultyPreventionChangeType type) _difficultyChangePreventionTime = GameTime::GetGameTime().count() + MINUTE; _difficultyChangePreventionType = type; } + +void Group::DoForAllMembers(std::function const& worker) +{ + for (GroupReference* itr = GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member) + continue; + + worker(member); + } +} diff --git a/src/server/game/Groups/Group.h b/src/server/game/Groups/Group.h index 278cc5168..a1d142fb8 100644 --- a/src/server/game/Groups/Group.h +++ b/src/server/game/Groups/Group.h @@ -23,6 +23,7 @@ #include "LootMgr.h" #include "QueryResult.h" #include "SharedDefines.h" +#include class Battlefield; class Battleground; @@ -214,6 +215,7 @@ public: bool isBGGroup() const; bool IsCreated() const; ObjectGuid GetLeaderGUID() const; + Player* GetLeader(); ObjectGuid GetGUID() const; const char* GetLeaderName() const; LootMethod GetLootMethod() const; @@ -313,6 +315,8 @@ public: DifficultyPreventionChangeType GetDifficultyChangePreventionReason() const { return _difficultyChangePreventionType; } void SetDifficultyChangePrevention(DifficultyPreventionChangeType type); + void DoForAllMembers(std::function const& worker); + protected: void _homebindIfInstance(Player* player); void _cancelHomebindIfInstance(Player* player); diff --git a/src/server/game/Handlers/BattleGroundHandler.cpp b/src/server/game/Handlers/BattleGroundHandler.cpp index ddf30758f..ae8da6564 100644 --- a/src/server/game/Handlers/BattleGroundHandler.cpp +++ b/src/server/game/Handlers/BattleGroundHandler.cpp @@ -72,8 +72,9 @@ void WorldSession::HandleBattlemasterJoinOpcode(WorldPacket& recvData) { ObjectGuid guid; uint32 bgTypeId_; - uint32 instanceId; // sent to queue for particular bg from battlemaster's list, currently not used + uint32 instanceId; uint8 joinAsGroup; + bool isPremade = false; recvData >> guid; // battlemaster guid recvData >> bgTypeId_; // battleground type id (DBC id) @@ -82,7 +83,10 @@ void WorldSession::HandleBattlemasterJoinOpcode(WorldPacket& recvData) // entry not found if (!sBattlemasterListStore.LookupEntry(bgTypeId_)) + { + LOG_ERROR("network", "Battleground: invalid bgtype ({}) received. possible cheater? player {}", bgTypeId_, _player->GetGUID().ToString()); return; + } // chosen battleground type is disabled if (DisableMgr::IsDisabledFor(DISABLE_TYPE_BATTLEGROUND, bgTypeId_, nullptr)) @@ -91,6 +95,8 @@ void WorldSession::HandleBattlemasterJoinOpcode(WorldPacket& recvData) return; } + LOG_DEBUG("network", "WORLD: Recvd CMSG_BATTLEMASTER_JOIN Message from {}", guid.ToString()); + // get queue typeid and random typeid to check if already queued for them BattlegroundTypeId bgTypeId = BattlegroundTypeId(bgTypeId_); BattlegroundQueueTypeId bgQueueTypeId = BattlegroundMgr::BGQueueTypeId(bgTypeId, 0); @@ -100,35 +106,26 @@ void WorldSession::HandleBattlemasterJoinOpcode(WorldPacket& recvData) if (bgQueueTypeId == BATTLEGROUND_QUEUE_NONE) return; - // get bg template - Battleground* bgt = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); - if (!bgt) + // ignore if player is already in BG + if (_player->InBattleground()) + return; + + // get bg instance or bg template if instance not found + Battleground* bg = nullptr; + if (instanceId) + bg = sBattlegroundMgr->GetBattlegroundThroughClientInstance(instanceId, bgTypeId); + + if (!bg) + bg = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); + + if (!bg) return; // expected bracket entry - PvPDifficultyEntry const* bracketEntry = GetBattlegroundBracketByLevel(bgt->GetMapId(), _player->getLevel()); + PvPDifficultyEntry const* bracketEntry = GetBattlegroundBracketByLevel(bg->GetMapId(), _player->getLevel()); if (!bracketEntry) return; - // pussywizard: if trying to queue for already queued - // just remove from queue and it will requeue! - uint32 qSlot = _player->GetBattlegroundQueueIndex(bgQueueTypeId); - if (qSlot < PLAYER_MAX_BATTLEGROUND_QUEUES) - { - BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId); - - if (bgQueue.IsPlayerInvitedToRatedArena(_player->GetGUID())) - { - WorldPacket data; - sBattlegroundMgr->BuildGroupJoinedBattlegroundPacket(&data, ERR_BATTLEGROUND_JOIN_FAILED); - SendPacket(&data); - return; - } - - bgQueue.RemovePlayer(_player->GetGUID(), false, qSlot); - _player->RemoveBattlegroundQueueId(bgQueueTypeId); - } - // must have free queue slot if (!_player->HasFreeBattlegroundQueueId()) { @@ -139,7 +136,7 @@ void WorldSession::HandleBattlemasterJoinOpcode(WorldPacket& recvData) } // queue result (default ok) - GroupJoinBattlegroundResult err = GroupJoinBattlegroundResult(bgt->GetBgTypeID()); + GroupJoinBattlegroundResult err = GroupJoinBattlegroundResult(bg->GetBgTypeID()); if (!sScriptMgr->CanJoinInBattlegroundQueue(_player, guid, bgTypeId, joinAsGroup, err) && err <= 0) { @@ -149,6 +146,8 @@ void WorldSession::HandleBattlemasterJoinOpcode(WorldPacket& recvData) return; } + BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId); + // check if player can queue: if (!joinAsGroup) { @@ -169,6 +168,10 @@ void WorldSession::HandleBattlemasterJoinOpcode(WorldPacket& recvData) { err = ERR_IN_RANDOM_BG; } + else if (_player->InBattlegroundQueueForBattlegroundQueueType(bgQueueTypeId)) // queued for this bg + { + err = ERR_BATTLEGROUND_NONE; + } else if (_player->InBattlegroundQueue() && bgTypeId == BATTLEGROUND_RB) // already in queue, so can't queue for random { err = ERR_IN_NON_RANDOM_BG; @@ -193,15 +196,13 @@ void WorldSession::HandleBattlemasterJoinOpcode(WorldPacket& recvData) return; } - BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId); - GroupQueueInfo* ginfo = bgQueue.AddGroup(_player, nullptr, bracketEntry, false, false, 0, 0, 0); + GroupQueueInfo* ginfo = bgQueue.AddGroup(_player, nullptr, bgTypeId, bracketEntry, 0, false, isPremade, 0, 0); uint32 avgWaitTime = bgQueue.GetAverageQueueWaitTime(ginfo); - uint32 queueSlot = _player->AddBattlegroundQueueId(bgQueueTypeId); // send status packet WorldPacket data; - sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bgt, queueSlot, STATUS_WAIT_QUEUE, avgWaitTime, 0, 0, TEAM_NEUTRAL); + sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bg, queueSlot, STATUS_WAIT_QUEUE, avgWaitTime, 0, 0, TEAM_NEUTRAL); SendPacket(&data); sScriptMgr->OnPlayerJoinBG(_player); @@ -214,30 +215,6 @@ void WorldSession::HandleBattlemasterJoinOpcode(WorldPacket& recvData) if (!grp || grp->GetLeaderGUID() != _player->GetGUID()) return; - // pussywizard: for party members - remove queues for which leader is not queued to! - std::set leaderQueueTypeIds; - for (uint32 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) - leaderQueueTypeIds.insert((uint32)_player->GetBattlegroundQueueTypeId(i)); - for (GroupReference* itr = grp->GetFirstMember(); itr != nullptr; itr = itr->next()) - if (Player* member = itr->GetSource()) - for (uint32 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) - if (BattlegroundQueueTypeId mqtid = member->GetBattlegroundQueueTypeId(i)) - if (leaderQueueTypeIds.count((uint32)mqtid) == 0) - { - BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(mqtid); - - if (bgQueue.IsPlayerInvitedToRatedArena(member->GetGUID())) - { - WorldPacket data; - sBattlegroundMgr->BuildGroupJoinedBattlegroundPacket(&data, ERR_BATTLEGROUND_JOIN_FAILED); - SendPacket(&data); - return; - } - - bgQueue.RemovePlayer(member->GetGUID(), false, i); - member->RemoveBattlegroundQueueId(mqtid); - } - if (_player->InBattlegroundQueueForBattlegroundQueueType(bgQueueTypeIdRandom)) // queued for random bg, so can't queue for anything else err = ERR_IN_RANDOM_BG; else if (_player->InBattlegroundQueue() && bgTypeId == BATTLEGROUND_RB) // already in queue, so can't queue for random @@ -248,44 +225,42 @@ void WorldSession::HandleBattlemasterJoinOpcode(WorldPacket& recvData) err = ERR_BATTLEGROUND_QUEUED_FOR_RATED; if (err > 0) - err = grp->CanJoinBattlegroundQueue(bgt, bgQueueTypeId, 0, bgt->GetMaxPlayersPerTeam(), false, 0); + err = grp->CanJoinBattlegroundQueue(bg, bgQueueTypeId, 0, bg->GetMaxPlayersPerTeam(), false, 0); - bool isPremade = (grp->GetMembersCount() >= bgt->GetMinPlayersPerTeam() && bgTypeId != BATTLEGROUND_RB); + isPremade = (grp->GetMembersCount() >= bg->GetMinPlayersPerTeam() && bgTypeId != BATTLEGROUND_RB); uint32 avgWaitTime = 0; if (err > 0) { - BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId); - GroupQueueInfo* ginfo = bgQueue.AddGroup(_player, grp, bracketEntry, false, isPremade, 0, 0, 0); + GroupQueueInfo* ginfo = bgQueue.AddGroup(_player, grp, bgTypeId, bracketEntry, 0, false, isPremade, 0, 0); avgWaitTime = bgQueue.GetAverageQueueWaitTime(ginfo); } - WorldPacket data; - for (GroupReference* itr = grp->GetFirstMember(); itr != nullptr; itr = itr->next()) + grp->DoForAllMembers([bg, err, bgQueueTypeId, avgWaitTime](Player* member) { - Player* member = itr->GetSource(); - if (!member) - continue; + WorldPacket data; if (err <= 0) { sBattlegroundMgr->BuildGroupJoinedBattlegroundPacket(&data, err); member->GetSession()->SendPacket(&data); - continue; + return; } uint32 queueSlot = member->AddBattlegroundQueueId(bgQueueTypeId); // send status packet - sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bgt, queueSlot, STATUS_WAIT_QUEUE, avgWaitTime, 0, 0, TEAM_NEUTRAL); + sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bg, queueSlot, STATUS_WAIT_QUEUE, avgWaitTime, 0, 0, TEAM_NEUTRAL); member->GetSession()->SendPacket(&data); sBattlegroundMgr->BuildGroupJoinedBattlegroundPacket(&data, err); member->GetSession()->SendPacket(&data); sScriptMgr->OnPlayerJoinBG(member); - } + }); } + + sBattlegroundMgr->ScheduleQueueUpdate(0, 0, bgQueueTypeId, bgTypeId, bracketEntry->GetBracketId()); } void WorldSession::HandleBattlegroundPlayerPositionsOpcode(WorldPacket& /*recvData*/) @@ -373,7 +348,10 @@ void WorldSession::HandleBattlefieldListOpcode(WorldPacket& recvData) BattlemasterListEntry const* bl = sBattlemasterListStore.LookupEntry(bgTypeId); if (!bl) + { + LOG_DEBUG("bg.battleground", "BattlegroundHandler: invalid bgtype ({}) with player (Name: {}, {}) received.", bgTypeId, _player->GetName(), _player->GetGUID().ToString()); return; + } WorldPacket data; sBattlegroundMgr->BuildBattlegroundListPacket(&data, ObjectGuid::Empty, _player, BattlegroundTypeId(bgTypeId), fromWhere); @@ -382,21 +360,27 @@ void WorldSession::HandleBattlefieldListOpcode(WorldPacket& recvData) void WorldSession::HandleBattleFieldPortOpcode(WorldPacket& recvData) { - uint8 arenaType; // arenatype if arena - uint8 unk2; // unk, can be 0x0 (may be if was invited?) and 0x1 - uint32 bgTypeId_; // type id from dbc - uint16 unk; // 0x1F90 constant? - uint8 action; // enter battle 0x1, leave queue 0x0 + uint8 arenaType; // arenatype if arena + uint8 unk2; // unk, can be 0x0 (may be if was invited?) and 0x1 + uint32 bgTypeId_; // type id from dbc + uint16 unk; // 0x1F90 constant? + uint8 action; // enter battle 0x1, leave queue 0x0 recvData >> arenaType >> unk2 >> bgTypeId_ >> unk >> action; // bgTypeId not valid if (!sBattlemasterListStore.LookupEntry(bgTypeId_)) + { + LOG_DEBUG("bg.battleground", "CMSG_BATTLEFIELD_PORT {} ArenaType: {}, Unk: {}, BgType: {}, Action: {}. Invalid BgType!", GetPlayerInfo(), arenaType, unk2, bgTypeId_, action); return; + } // player not in any queue, so can't really answer if (!_player->InBattlegroundQueue()) + { + LOG_DEBUG("bg.battleground", "CMSG_BATTLEFIELD_PORT {} ArenaType: {}, Unk: {}, BgType: {}, Action: {}. Player not in queue!", GetPlayerInfo(), arenaType, unk2, bgTypeId_, action); return; + } // get BattlegroundQueue for received BattlegroundTypeId bgTypeId = BattlegroundTypeId(bgTypeId_); @@ -409,20 +393,40 @@ void WorldSession::HandleBattleFieldPortOpcode(WorldPacket& recvData) // get group info from queue GroupQueueInfo ginfo; if (!bgQueue.GetPlayerGroupInfoData(_player->GetGUID(), &ginfo)) + { + LOG_DEBUG("bg.battleground", "CMSG_BATTLEFIELD_PORT {} ArenaType: {}, Unk: {}, BgType: {}, Action: {}. Player not in queue (No player Group Info)!", + GetPlayerInfo(), arenaType, unk2, bgTypeId_, action); return; + } // to accept, player must be invited to particular battleground id if (!ginfo.IsInvitedToBGInstanceGUID && action == 1) + { + LOG_DEBUG("bg.battleground", "CMSG_BATTLEFIELD_PORT {} ArenaType: {}, Unk: {}, BgType: {}, Action: {}. Player is not invited to any bg!", + GetPlayerInfo(), arenaType, unk2, bgTypeId_, action); return; + } - Battleground* bg = sBattlegroundMgr->GetBattleground(ginfo.IsInvitedToBGInstanceGUID); - - // use template if leaving queue (instance might not be created yet) - if (!bg && action == 0) - bg = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); - + Battleground* bg = sBattlegroundMgr->GetBattleground(ginfo.IsInvitedToBGInstanceGUID, bgTypeId); if (!bg) - return; + { + if (action) + { + LOG_DEBUG("bg.battleground", "CMSG_BATTLEFIELD_PORT {} ArenaType: {}, Unk: {}, BgType: {}, Action: {}. Cant find BG with id {}!", + GetPlayerInfo(), arenaType, unk2, bgTypeId_, action, ginfo.IsInvitedToBGInstanceGUID); + return; + } + + bg = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); + if (!bg) + { + LOG_ERROR("network", "BattlegroundHandler: bg_template not found for type id {}.", bgTypeId); + return; + } + } + + LOG_DEBUG("bg.battleground", "CMSG_BATTLEFIELD_PORT {} ArenaType: {}, Unk: {}, BgType: {}, Action: {}.", + GetPlayerInfo(), arenaType, unk2, bgTypeId_, action); // expected bracket entry PvPDifficultyEntry const* bracketEntry = GetBattlegroundBracketByLevel(bg->GetMapId(), _player->getLevel()); @@ -439,74 +443,90 @@ void WorldSession::HandleBattleFieldPortOpcode(WorldPacket& recvData) sBattlegroundMgr->BuildGroupJoinedBattlegroundPacket(&data, ERR_GROUP_JOIN_BATTLEGROUND_DESERTERS); SendPacket(&data); action = 0; + LOG_DEBUG("bg.battleground", "Player {} {} has a deserter debuff, do not port him to battleground!", _player->GetName(), _player->GetGUID().ToString()); } if (_player->getLevel() > bg->GetMaxLevel()) + { + LOG_ERROR("network", "Player {} {} has level ({}) higher than maxlevel ({}) of battleground ({})! Do not port him to battleground!", + _player->GetName(), _player->GetGUID().ToString(), _player->getLevel(), bg->GetMaxLevel(), bg->GetBgTypeID()); action = 0; + } } // get player queue slot index for this bg (can be in up to 2 queues at the same time) uint32 queueSlot = _player->GetBattlegroundQueueIndex(bgQueueTypeId); - WorldPacket data; - switch (action) + + if (action) // accept { - case 1: // accept + // check Freeze debuff + if (_player->HasAura(9454)) + return; + + if (!_player->IsInvitedForBattlegroundQueueType(bgQueueTypeId)) + return; // cheating? + + // set entry point if not in battleground + if (!_player->InBattleground()) + _player->SetEntryPoint(); + + // resurrect the player + if (!_player->IsAlive()) + { + _player->ResurrectPlayer(1.0f); + _player->SpawnCorpseBones(); + } + + TeamId teamId = ginfo.teamId; + + // send status packet + sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bg, queueSlot, STATUS_IN_PROGRESS, 0, bg->GetStartTime(), bg->GetArenaType(), teamId); + SendPacket(&data); + + // remove battleground queue status from BGmgr + bgQueue.RemovePlayer(_player->GetGUID(), false); + + // this is still needed here if battleground "jumping" shouldn't add deserter debuff + // also this is required to prevent stuck at old battleground after SetBattlegroundId set to new + if (Battleground* currentBg = _player->GetBattleground()) + currentBg->RemovePlayerAtLeave(_player); + + // Remove from LFG queues + sLFGMgr->LeaveAllLfgQueues(_player->GetGUID(), false); + + _player->SetBattlegroundId(bg->GetInstanceID(), bg->GetBgTypeID(), queueSlot, true, bgTypeId == BATTLEGROUND_RB, teamId); + sBattlegroundMgr->SendToBattleground(_player, ginfo.IsInvitedToBGInstanceGUID, bgTypeId); + + LOG_DEBUG("bg.battleground", "Battleground: player {} {} joined battle for bg {}, bgtype {}, queue type {}.", _player->GetName(), _player->GetGUID().ToString(), bg->GetInstanceID(), bg->GetBgTypeID(), bgQueueTypeId); + } + else // leave queue + { + bgQueue.RemovePlayer(_player->GetGUID(), true); + _player->RemoveBattlegroundQueueId(bgQueueTypeId); + + sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bg, queueSlot, STATUS_NONE, 0, 0, 0, TEAM_NEUTRAL); + SendPacket(&data); + + LOG_DEBUG("bg.battleground", "Battleground: player {} {} left queue for bgtype {}, queue type {}.", _player->GetName(), _player->GetGUID().ToString(), bg->GetBgTypeID(), bgQueueTypeId); + + // player left queue, we should update it - do not update Arena Queue + if (!ginfo.ArenaType) + sBattlegroundMgr->ScheduleQueueUpdate(ginfo.ArenaMatchmakerRating, ginfo.ArenaType, bgQueueTypeId, bgTypeId, bracketEntry->GetBracketId()); + + // track if player refuses to join the BG after being invited + if (bg->isBattleground() && (bg->GetStatus() == STATUS_IN_PROGRESS || bg->GetStatus() == STATUS_WAIT_JOIN)) + { + if (sWorld->getBoolConfig(CONFIG_BATTLEGROUND_TRACK_DESERTERS)) { - // set entry point if not in battleground - if (!_player->InBattleground()) - _player->SetEntryPoint(); - - // resurrect the player - if (!_player->IsAlive()) - { - _player->ResurrectPlayer(1.0f); - _player->SpawnCorpseBones(); - } - - TeamId teamId = ginfo.teamId; - - // remove player from all bg queues - for (uint32 qslot = 0; qslot < PLAYER_MAX_BATTLEGROUND_QUEUES; ++qslot) - if (BattlegroundQueueTypeId q = _player->GetBattlegroundQueueTypeId(qslot)) - { - BattlegroundQueue& queue = sBattlegroundMgr->GetBattlegroundQueue(q); - queue.RemovePlayer(_player->GetGUID(), (bgQueueTypeId == q), qslot); - _player->RemoveBattlegroundQueueId(q); - } - - // send status packet - sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bg, queueSlot, STATUS_IN_PROGRESS, 0, bg->GetStartTime(), bg->GetArenaType(), teamId); - SendPacket(&data); - - // Remove from LFG queues - sLFGMgr->LeaveAllLfgQueues(_player->GetGUID(), false); - - _player->SetBattlegroundId(bg->GetInstanceID(), bg->GetBgTypeID(), queueSlot, true, bgTypeId == BATTLEGROUND_RB, teamId); - sBattlegroundMgr->SendToBattleground(_player, ginfo.IsInvitedToBGInstanceGUID, bgTypeId); + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_DESERTER_TRACK); + stmt->SetData(0, _player->GetGUID().GetCounter()); + stmt->SetData(1, BG_DESERTION_TYPE_LEAVE_QUEUE); + CharacterDatabase.Execute(stmt); } - break; - case 0: // leave queue - { - bgQueue.RemovePlayer(_player->GetGUID(), false, queueSlot); - _player->RemoveBattlegroundQueueId(bgQueueTypeId); - // track if player refuses to join the BG after being invited - if (bg->isBattleground() && (bg->GetStatus() == STATUS_IN_PROGRESS || bg->GetStatus() == STATUS_WAIT_JOIN)) - { - if (sWorld->getBoolConfig(CONFIG_BATTLEGROUND_TRACK_DESERTERS)) - { - CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_DESERTER_TRACK); - stmt->SetData(0, _player->GetGUID().GetCounter()); - stmt->SetData(1, BG_DESERTION_TYPE_LEAVE_QUEUE); - CharacterDatabase.Execute(stmt); - } - sScriptMgr->OnBattlegroundDesertion(_player, BG_DESERTION_TYPE_LEAVE_QUEUE); - } - } - break; - default: - break; + sScriptMgr->OnBattlegroundDesertion(_player, BG_DESERTION_TYPE_LEAVE_QUEUE); + } } } @@ -562,7 +582,7 @@ void WorldSession::HandleBattlefieldStatusOpcode(WorldPacket& /*recvData*/) // if invited - send STATUS_WAIT_JOIN if (ginfo.IsInvitedToBGInstanceGUID) { - Battleground* bg = sBattlegroundMgr->GetBattleground(ginfo.IsInvitedToBGInstanceGUID); + Battleground* bg = sBattlegroundMgr->GetBattleground(ginfo.IsInvitedToBGInstanceGUID, bgTypeId); if (!bg) continue; @@ -591,10 +611,12 @@ void WorldSession::HandleBattlefieldStatusOpcode(WorldPacket& /*recvData*/) void WorldSession::HandleBattlemasterJoinArena(WorldPacket& recvData) { - ObjectGuid guid; // arena Battlemaster guid - uint8 arenaslot; // 2v2, 3v3 or 5v5 - uint8 asGroup; // asGroup - uint8 isRated; // isRated + LOG_DEBUG("network", "WORLD: CMSG_BATTLEMASTER_JOIN_ARENA"); + + ObjectGuid guid; // arena Battlemaster guid + uint8 arenaslot; // 2v2, 3v3 or 5v5 + uint8 asGroup; // asGroup + uint8 isRated; // isRated recvData >> guid >> arenaslot >> asGroup >> isRated; @@ -602,6 +624,10 @@ void WorldSession::HandleBattlemasterJoinArena(WorldPacket& recvData) if (isRated && !asGroup) return; + // ignore if we already in BG or BG queue + if (_player->InBattleground()) + return; + // find creature by guid Creature* unit = GetPlayer()->GetMap()->GetCreature(guid); if (!unit || !unit->IsBattleMaster()) @@ -609,6 +635,11 @@ void WorldSession::HandleBattlemasterJoinArena(WorldPacket& recvData) // get arena type uint8 arenatype = 0; + uint32 ateamId = 0; + uint32 arenaRating = 0; + uint32 matchmakerRating = 0; + uint32 previousOpponents = 0; + switch (arenaslot) { case 0: @@ -621,13 +652,17 @@ void WorldSession::HandleBattlemasterJoinArena(WorldPacket& recvData) arenatype = ARENA_TYPE_5v5; break; default: + LOG_ERROR("network", "Unknown arena slot {} at HandleBattlemasterJoinArena()", arenaslot); return; } // get template for all arenas Battleground* bgt = sBattlegroundMgr->GetBattlegroundTemplate(BATTLEGROUND_AA); if (!bgt) + { + LOG_ERROR("network", "Battleground: template bg (all arenas) not found"); return; + } // arenas disabled if (DisableMgr::IsDisabledFor(DISABLE_TYPE_BATTLEGROUND, BATTLEGROUND_AA, nullptr)) @@ -637,32 +672,12 @@ void WorldSession::HandleBattlemasterJoinArena(WorldPacket& recvData) } BattlegroundTypeId bgTypeId = bgt->GetBgTypeID(); - BattlegroundQueueTypeId bgQueueTypeId = BattlegroundMgr::BGQueueTypeId(bgTypeId, arenatype); // expected bracket entry PvPDifficultyEntry const* bracketEntry = GetBattlegroundBracketByLevel(bgt->GetMapId(), _player->getLevel()); if (!bracketEntry) return; - // pussywizard: if trying to queue for already queued - // just remove from queue and it will requeue! - uint32 qSlot = _player->GetBattlegroundQueueIndex(bgQueueTypeId); - if (qSlot < PLAYER_MAX_BATTLEGROUND_QUEUES) - { - BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId); - - if (bgQueue.IsPlayerInvitedToRatedArena(_player->GetGUID())) - { - WorldPacket data; - sBattlegroundMgr->BuildGroupJoinedBattlegroundPacket(&data, ERR_BATTLEGROUND_JOIN_FAILED); - SendPacket(&data); - return; - } - - bgQueue.RemovePlayer(_player->GetGUID(), false, qSlot); - _player->RemoveBattlegroundQueueId(bgQueueTypeId); - } - // must have free queue slot // pussywizard: allow being queued only in one arena queue, and it even cannot be together with bg queues if (_player->InBattlegroundQueue()) @@ -684,6 +699,9 @@ void WorldSession::HandleBattlemasterJoinArena(WorldPacket& recvData) return; } + BattlegroundQueueTypeId bgQueueTypeId = BattlegroundMgr::BGQueueTypeId(bgTypeId, arenatype); + BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId); + // check if player can queue: if (!asGroup) { @@ -705,16 +723,25 @@ void WorldSession::HandleBattlemasterJoinArena(WorldPacket& recvData) return; } - BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId); - GroupQueueInfo* ginfo = bgQueue.AddGroup(_player, nullptr, bracketEntry, false, false, 0, 0, 0); - uint32 avgWaitTime = bgQueue.GetAverageQueueWaitTime(ginfo); + // check if already in queue + if (_player->GetBattlegroundQueueIndex(bgQueueTypeId) < PLAYER_MAX_BATTLEGROUND_QUEUES) + //player is already in this queue + return; + // check if has free queue slots + if (!_player->HasFreeBattlegroundQueueId()) + return; + + GroupQueueInfo* ginfo = bgQueue.AddGroup(_player, nullptr, bgTypeId, bracketEntry, arenatype, isRated != 0, false, arenaRating, matchmakerRating, ateamId, previousOpponents); + uint32 avgWaitTime = bgQueue.GetAverageQueueWaitTime(ginfo); uint32 queueSlot = _player->AddBattlegroundQueueId(bgQueueTypeId); WorldPacket data; sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bgt, queueSlot, STATUS_WAIT_QUEUE, avgWaitTime, 0, arenatype, TEAM_NEUTRAL); SendPacket(&data); + LOG_DEBUG("bg.battleground", "Battleground: player joined queue for arena, skirmish, bg queue type {} bg type {}: {}, NAME {}", bgQueueTypeId, bgTypeId, _player->GetGUID().ToString(), _player->GetName()); + sScriptMgr->OnPlayerJoinArena(_player); } // check if group can queue: @@ -725,34 +752,6 @@ void WorldSession::HandleBattlemasterJoinArena(WorldPacket& recvData) if (!grp || grp->GetLeaderGUID() != _player->GetGUID()) return; - // pussywizard: for party members - remove queues for which leader is not queued to! - std::set leaderQueueTypeIds; - for (uint32 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) - leaderQueueTypeIds.insert((uint32)_player->GetBattlegroundQueueTypeId(i)); - for (GroupReference* itr = grp->GetFirstMember(); itr != nullptr; itr = itr->next()) - if (Player* member = itr->GetSource()) - for (uint32 i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) - if (BattlegroundQueueTypeId mqtid = member->GetBattlegroundQueueTypeId(i)) - if (leaderQueueTypeIds.count((uint32)mqtid) == 0) - { - BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(mqtid); - - if (bgQueue.IsPlayerInvitedToRatedArena(member->GetGUID())) - { - WorldPacket data; - sBattlegroundMgr->BuildGroupJoinedBattlegroundPacket(&data, ERR_BATTLEGROUND_JOIN_FAILED); - SendPacket(&data); - return; - } - - bgQueue.RemovePlayer(member->GetGUID(), false, i); - member->RemoveBattlegroundQueueId(mqtid); - } - - uint32 ateamId = 0; - uint32 arenaRating = 0; - uint32 matchmakerRating = 0; - // additional checks for rated arenas if (isRated) { @@ -775,15 +774,40 @@ void WorldSession::HandleBattlemasterJoinArena(WorldPacket& recvData) matchmakerRating = at->GetAverageMMR(grp); if (arenaRating <= 0) arenaRating = 1; + + previousOpponents = at->GetPreviousOpponents(); } err = grp->CanJoinBattlegroundQueue(bgt, bgQueueTypeId, arenatype, arenatype, (bool)isRated, arenaslot); + // Check queue group members + if (err) + { + grp->DoForAllMembers([&bgQueue, &err](Player* member) + { + if (bgQueue.IsPlayerInvitedToRatedArena(member->GetGUID())) + { + err = ERR_BATTLEGROUND_JOIN_FAILED; + } + }); + } + uint32 avgWaitTime = 0; if (err > 0) { - BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId); - GroupQueueInfo* ginfo = bgQueue.AddGroup(_player, grp, bracketEntry, isRated, false, arenaRating, matchmakerRating, ateamId); + LOG_DEBUG("bg.battleground", "Battleground: arena join as group start"); + + if (isRated) + { + LOG_DEBUG("bg.battleground", "Battleground: arena team id {}, leader {} queued with matchmaker rating {} for type {}", _player->GetArenaTeamId(arenaslot), _player->GetName(), matchmakerRating, arenatype); + bgt->SetRated(true); + } + else + { + bgt->SetRated(false); + } + + GroupQueueInfo* ginfo = bgQueue.AddGroup(_player, grp, bgTypeId, bracketEntry, arenatype, isRated != 0, false, arenaRating, matchmakerRating, ateamId, previousOpponents); avgWaitTime = bgQueue.GetAverageQueueWaitTime(ginfo); } @@ -810,13 +834,13 @@ void WorldSession::HandleBattlemasterJoinArena(WorldPacket& recvData) sBattlegroundMgr->BuildGroupJoinedBattlegroundPacket(&data, err); member->GetSession()->SendPacket(&data); + LOG_DEBUG("bg.battleground", "Battleground: player joined queue for arena as group bg queue type {} bg type {}: {}, NAME {}", bgQueueTypeId, bgTypeId, member->GetGUID().ToString(), member->GetName()); + sScriptMgr->OnPlayerJoinArena(member); } - - // pussywizard: schedule update for rated arena - if (ateamId) - sBattlegroundMgr->ScheduleArenaQueueUpdate(ateamId, bgQueueTypeId, bracketEntry->GetBracketId()); } + + sBattlegroundMgr->ScheduleQueueUpdate(matchmakerRating, arenatype, bgQueueTypeId, bgTypeId, bracketEntry->GetBracketId()); } void WorldSession::HandleReportPvPAFK(WorldPacket& recvData) diff --git a/src/server/game/Handlers/CharacterHandler.cpp b/src/server/game/Handlers/CharacterHandler.cpp index 79a046b4d..abd1f2b67 100644 --- a/src/server/game/Handlers/CharacterHandler.cpp +++ b/src/server/game/Handlers/CharacterHandler.cpp @@ -561,12 +561,7 @@ void WorldSession::HandleCharCreateOpcode(WorldPacket& recvData) newChar->SaveToDB(characterTransaction, true, false); createInfo->CharCount++; - LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_DEL_REALM_CHARACTERS_BY_REALM); - stmt->SetData(0, GetAccountId()); - stmt->SetData(1, realm.Id.Realm); - trans->Append(stmt); - - stmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_REALM_CHARACTERS); + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_REP_REALM_CHARACTERS); stmt->SetData(0, createInfo->CharCount); stmt->SetData(1, GetAccountId()); stmt->SetData(2, realm.Id.Realm); diff --git a/src/server/game/Handlers/MovementHandler.cpp b/src/server/game/Handlers/MovementHandler.cpp index e4d6c4886..540273a2a 100644 --- a/src/server/game/Handlers/MovementHandler.cpp +++ b/src/server/game/Handlers/MovementHandler.cpp @@ -170,7 +170,7 @@ void WorldSession::HandleMoveWorldportAck() if (uint32 inviteInstanceId = _player->GetPendingSpectatorInviteInstanceId()) { - if (Battleground* tbg = sBattlegroundMgr->GetBattleground(inviteInstanceId)) + if (Battleground* tbg = sBattlegroundMgr->GetBattleground(inviteInstanceId, BATTLEGROUND_TYPE_NONE)) tbg->RemoveToBeTeleported(_player->GetGUID()); _player->SetPendingSpectatorInviteInstanceId(0); } @@ -904,14 +904,14 @@ void WorldSession::ComputeNewClockDelta() std::vector latencies; std::vector clockDeltasAfterFiltering; - for (auto pair : _timeSyncClockDeltaQueue.content()) + for (auto& pair : _timeSyncClockDeltaQueue.content()) latencies.push_back(pair.second); uint32 latencyMedian = median(latencies); uint32 latencyStandardDeviation = standard_deviation(latencies); uint32 sampleSizeAfterFiltering = 0; - for (auto pair : _timeSyncClockDeltaQueue.content()) + for (auto& pair : _timeSyncClockDeltaQueue.content()) { if (pair.second <= latencyMedian + latencyStandardDeviation) { clockDeltasAfterFiltering.push_back(pair.first); diff --git a/src/server/game/Scripting/ScriptDefines/BGScript.cpp b/src/server/game/Scripting/ScriptDefines/BGScript.cpp index 602cb342a..3e06e2ce3 100644 --- a/src/server/game/Scripting/ScriptDefines/BGScript.cpp +++ b/src/server/game/Scripting/ScriptDefines/BGScript.cpp @@ -66,58 +66,41 @@ void ScriptMgr::OnBattlegroundRemovePlayerAtLeave(Battleground* bg, Player* play }); } -void ScriptMgr::OnAddGroup(BattlegroundQueue* queue, GroupQueueInfo* ginfo, uint32& index, Player* leader, Group* grp, PvPDifficultyEntry const* bracketEntry, bool isPremade) +void ScriptMgr::OnAddGroup(BattlegroundQueue* queue, GroupQueueInfo* ginfo, uint32& index, Player* leader, Group* group, BattlegroundTypeId bgTypeId, PvPDifficultyEntry const* bracketEntry, + uint8 arenaType, bool isRated, bool isPremade, uint32 arenaRating, uint32 matchmakerRating, uint32 arenaTeamId, uint32 opponentsArenaTeamId) { ExecuteScript([&](BGScript* script) { - script->OnAddGroup(queue, ginfo, index, leader, grp, bracketEntry, isPremade); + script->OnAddGroup(queue, ginfo, index, leader, group, bgTypeId, bracketEntry, + arenaType, isRated, isPremade, arenaRating, matchmakerRating, arenaTeamId, opponentsArenaTeamId); }); } -bool ScriptMgr::CanFillPlayersToBG(BattlegroundQueue* queue, Battleground* bg, const int32 aliFree, const int32 hordeFree, BattlegroundBracketId bracket_id) +bool ScriptMgr::CanFillPlayersToBG(BattlegroundQueue* queue, Battleground* bg, BattlegroundBracketId bracket_id) { auto ret = IsValidBoolScript([&](BGScript* script) { - return !script->CanFillPlayersToBG(queue, bg, aliFree, hordeFree, bracket_id); + return !script->CanFillPlayersToBG(queue, bg, bracket_id); }); - if (ret && *ret) - { - return false; - } - - return true; + return ReturnValidBool(ret); } -bool ScriptMgr::CanFillPlayersToBGWithSpecific(BattlegroundQueue* queue, Battleground* bg, const int32 aliFree, const int32 hordeFree, - BattlegroundBracketId thisBracketId, BattlegroundQueue* specificQueue, BattlegroundBracketId specificBracketId) +bool ScriptMgr::IsCheckNormalMatch(BattlegroundQueue* queue, Battleground* bgTemplate, BattlegroundBracketId bracket_id, uint32 minPlayers, uint32 maxPlayers) { auto ret = IsValidBoolScript([&](BGScript* script) { - return !script->CanFillPlayersToBGWithSpecific(queue, bg, aliFree, hordeFree, thisBracketId, specificQueue, specificBracketId); + return script->IsCheckNormalMatch(queue, bgTemplate, bracket_id, minPlayers, maxPlayers); }); - if (ret && *ret) - { - return false; - } - - return true; + return ReturnValidBool(ret, true); } -void ScriptMgr::OnCheckNormalMatch(BattlegroundQueue* queue, uint32& Coef, Battleground* bgTemplate, BattlegroundBracketId bracket_id, uint32& minPlayers, uint32& maxPlayers) +void ScriptMgr::OnQueueUpdate(BattlegroundQueue* queue, uint32 diff, BattlegroundTypeId bgTypeId, BattlegroundBracketId bracket_id, uint8 arenaType, bool isRated, uint32 arenaRating) { ExecuteScript([&](BGScript* script) { - script->OnCheckNormalMatch(queue, Coef, bgTemplate, bracket_id, minPlayers, maxPlayers); - }); -} - -void ScriptMgr::OnQueueUpdate(BattlegroundQueue* queue, BattlegroundBracketId bracket_id, bool isRated, uint32 arenaRatedTeamId) -{ - ExecuteScript([&](BGScript* script) - { - script->OnQueueUpdate(queue, bracket_id, isRated, arenaRatedTeamId); + script->OnQueueUpdate(queue, diff, bgTypeId, bracket_id, arenaType, isRated, arenaRating); }); } @@ -128,12 +111,7 @@ bool ScriptMgr::CanSendMessageBGQueue(BattlegroundQueue* queue, Player* leader, return !script->CanSendMessageBGQueue(queue, leader, bg, bracketEntry); }); - if (ret && *ret) - { - return false; - } - - return true; + return ReturnValidBool(ret); } bool ScriptMgr::OnBeforeSendJoinMessageArenaQueue(BattlegroundQueue* queue, Player* leader, GroupQueueInfo* ginfo, PvPDifficultyEntry const* bracketEntry, bool isRated) @@ -143,12 +121,7 @@ bool ScriptMgr::OnBeforeSendJoinMessageArenaQueue(BattlegroundQueue* queue, Play return !script->OnBeforeSendJoinMessageArenaQueue(queue, leader, ginfo, bracketEntry, isRated); }); - if (ret && *ret) - { - return false; - } - - return true; + return ReturnValidBool(ret); } bool ScriptMgr::OnBeforeSendExitMessageArenaQueue(BattlegroundQueue* queue, GroupQueueInfo* ginfo) @@ -158,12 +131,7 @@ bool ScriptMgr::OnBeforeSendExitMessageArenaQueue(BattlegroundQueue* queue, Grou return !script->OnBeforeSendExitMessageArenaQueue(queue, ginfo); }); - if (ret && *ret) - { - return false; - } - - return true; + return ReturnValidBool(ret); } void ScriptMgr::OnBattlegroundEnd(Battleground* bg, TeamId winnerTeam) diff --git a/src/server/game/Scripting/ScriptMgr.h b/src/server/game/Scripting/ScriptMgr.h index d3f5571a4..bf874c096 100644 --- a/src/server/game/Scripting/ScriptMgr.h +++ b/src/server/game/Scripting/ScriptMgr.h @@ -1569,16 +1569,14 @@ public: // Remove player at leave BG virtual void OnBattlegroundRemovePlayerAtLeave(Battleground* /*bg*/, Player* /*player*/) { } - virtual void OnQueueUpdate(BattlegroundQueue* /*queue*/, BattlegroundBracketId /*bracket_id*/, bool /*isRated*/, uint32 /*arenaRatedTeamId*/) { } + virtual void OnQueueUpdate(BattlegroundQueue* /*queue*/, uint32 /* diff */, BattlegroundTypeId /* bgTypeId */, BattlegroundBracketId /* bracket_id */, uint8 /* arenaType */, bool /* isRated */, uint32 /* arenaRating */) { } - virtual void OnAddGroup(BattlegroundQueue* /*queue*/, GroupQueueInfo* /*ginfo*/, uint32& /*index*/, Player* /*leader*/, Group* /*grp*/, PvPDifficultyEntry const* /*bracketEntry*/, bool /*isPremade*/) { } + virtual void OnAddGroup(BattlegroundQueue* /*queue*/, GroupQueueInfo* /*ginfo*/, uint32& /*index*/, Player* /*leader*/, Group* /*group*/, BattlegroundTypeId /* bgTypeId */, PvPDifficultyEntry const* /* bracketEntry */, + uint8 /* arenaType */, bool /* isRated */, bool /* isPremade */, uint32 /* arenaRating */, uint32 /* matchmakerRating */, uint32 /* arenaTeamId */, uint32 /* opponentsArenaTeamId */) { } - [[nodiscard]] virtual bool CanFillPlayersToBG(BattlegroundQueue* /*queue*/, Battleground* /*bg*/, const int32 /*aliFree*/, const int32 /*hordeFree*/, BattlegroundBracketId /*bracket_id*/) { return true; } + [[nodiscard]] virtual bool CanFillPlayersToBG(BattlegroundQueue* /*queue*/, Battleground* /*bg*/, BattlegroundBracketId /*bracket_id*/) { return true; } - [[nodiscard]] virtual bool CanFillPlayersToBGWithSpecific(BattlegroundQueue* /*queue*/, Battleground* /*bg*/, const int32 /*aliFree*/, const int32 /*hordeFree*/, - BattlegroundBracketId /*thisBracketId*/, BattlegroundQueue* /*specificQueue*/, BattlegroundBracketId /*specificBracketId*/) { return true; } - - virtual void OnCheckNormalMatch(BattlegroundQueue* /*queue*/, uint32& /*Coef*/, Battleground* /*bgTemplate*/, BattlegroundBracketId /*bracket_id*/, uint32& /*minPlayers*/, uint32& /*maxPlayers*/) { } + [[nodiscard]] virtual bool IsCheckNormalMatch(BattlegroundQueue* /*queue*/, Battleground* /*bgTemplate*/, BattlegroundBracketId /*bracket_id*/, uint32 /*minPlayers*/, uint32 /*maxPlayers*/) { return false; }; [[nodiscard]] virtual bool CanSendMessageBGQueue(BattlegroundQueue* /*queue*/, Player* /*leader*/, Battleground* /*bg*/, PvPDifficultyEntry const* /*bracketEntry*/) { return true; } @@ -2406,12 +2404,11 @@ public: /* BGScript */ void OnBattlegroundAddPlayer(Battleground* bg, Player* player); void OnBattlegroundBeforeAddPlayer(Battleground* bg, Player* player); void OnBattlegroundRemovePlayerAtLeave(Battleground* bg, Player* player); - void OnQueueUpdate(BattlegroundQueue* queue, BattlegroundBracketId bracket_id, bool isRated, uint32 arenaRatedTeamId); - void OnAddGroup(BattlegroundQueue* queue, GroupQueueInfo* ginfo, uint32& index, Player* leader, Group* grp, PvPDifficultyEntry const* bracketEntry, bool isPremade); - bool CanFillPlayersToBG(BattlegroundQueue* queue, Battleground* bg, const int32 aliFree, const int32 hordeFree, BattlegroundBracketId bracket_id); - bool CanFillPlayersToBGWithSpecific(BattlegroundQueue* queue, Battleground* bg, const int32 aliFree, const int32 hordeFree, - BattlegroundBracketId thisBracketId, BattlegroundQueue* specificQueue, BattlegroundBracketId specificBracketId); - void OnCheckNormalMatch(BattlegroundQueue* queue, uint32& Coef, Battleground* bgTemplate, BattlegroundBracketId bracket_id, uint32& minPlayers, uint32& maxPlayers); + void OnQueueUpdate(BattlegroundQueue* queue, uint32 diff, BattlegroundTypeId bgTypeId, BattlegroundBracketId bracket_id, uint8 arenaType, bool isRated, uint32 arenaRating); + void OnAddGroup(BattlegroundQueue* queue, GroupQueueInfo* ginfo, uint32& index, Player* leader, Group* group, BattlegroundTypeId bgTypeId, PvPDifficultyEntry const* bracketEntry, + uint8 arenaType, bool isRated, bool isPremade, uint32 arenaRating, uint32 matchmakerRating, uint32 arenaTeamId, uint32 opponentsArenaTeamId); + bool CanFillPlayersToBG(BattlegroundQueue* queue, Battleground* bg, BattlegroundBracketId bracket_id); + bool IsCheckNormalMatch(BattlegroundQueue* queue, Battleground* bgTemplate, BattlegroundBracketId bracket_id, uint32 minPlayers, uint32 maxPlayers); bool CanSendMessageBGQueue(BattlegroundQueue* queue, Player* leader, Battleground* bg, PvPDifficultyEntry const* bracketEntry); bool OnBeforeSendJoinMessageArenaQueue(BattlegroundQueue* queue, Player* leader, GroupQueueInfo* ginfo, PvPDifficultyEntry const* bracketEntry, bool isRated); bool OnBeforeSendExitMessageArenaQueue(BattlegroundQueue* queue, GroupQueueInfo* ginfo); diff --git a/src/server/game/Scripting/ScriptMgrMacros.h b/src/server/game/Scripting/ScriptMgrMacros.h index 82fef5168..a26c6d71b 100644 --- a/src/server/game/Scripting/ScriptMgrMacros.h +++ b/src/server/game/Scripting/ScriptMgrMacros.h @@ -64,4 +64,9 @@ inline void ExecuteScript(std::function executeHook) } } +inline bool ReturnValidBool(Optional ret, bool need = false) +{ + return ret && *ret ? need : !need; +} + #endif // _SCRIPT_MGR_MACRO_H_ diff --git a/src/server/game/Server/WorldSession.cpp b/src/server/game/Server/WorldSession.cpp index e54c94c92..344bfeb5d 100644 --- a/src/server/game/Server/WorldSession.cpp +++ b/src/server/game/Server/WorldSession.cpp @@ -587,8 +587,6 @@ void WorldSession::LogoutPlayer(bool save) for (int i = 0; i < PLAYER_MAX_BATTLEGROUND_QUEUES; ++i) if (BattlegroundQueueTypeId bgQueueTypeId = _player->GetBattlegroundQueueTypeId(i)) { - _player->RemoveBattlegroundQueueId(bgQueueTypeId); - sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId).RemovePlayer(_player->GetGUID(), false, i); // track if player logs out after invited to join BG if (_player->IsInvitedForBattlegroundInstance()) { @@ -599,8 +597,12 @@ void WorldSession::LogoutPlayer(bool save) stmt->SetData(1, BG_DESERTION_TYPE_INVITE_LOGOUT); CharacterDatabase.Execute(stmt); } + sScriptMgr->OnBattlegroundDesertion(_player, BG_DESERTION_TYPE_INVITE_LOGOUT); } + + _player->RemoveBattlegroundQueueId(bgQueueTypeId); + sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId).RemovePlayer(_player->GetGUID(), true); } ///- If the player is in a guild, update the guild roster and broadcast a logout message to other guild members diff --git a/src/server/game/Spells/Auras/SpellAuraEffects.cpp b/src/server/game/Spells/Auras/SpellAuraEffects.cpp index 27ce8407f..3959d4f79 100644 --- a/src/server/game/Spells/Auras/SpellAuraEffects.cpp +++ b/src/server/game/Spells/Auras/SpellAuraEffects.cpp @@ -892,9 +892,16 @@ void AuraEffect::Update(uint32 diff, Unit* caster) { if (m_isPeriodic && (GetBase()->GetDuration() >= 0 || GetBase()->IsPassive() || GetBase()->IsPermanent())) { + uint32 totalTicks = GetTotalTicks(); + m_periodicTimer -= int32(diff); while (m_periodicTimer <= 0) { + if (!GetBase()->IsPermanent() && (m_tickNumber + 1) > totalTicks) + { + break; + } + ++m_tickNumber; // update before tick (aura can be removed in TriggerSpell or PeriodicTick calls) @@ -6993,3 +7000,19 @@ void AuraEffect::HandleRaidProcFromChargeWithValueAuraProc(AuraApplication* aurA LOG_DEBUG("spells.aura", "AuraEffect::HandleRaidProcFromChargeWithValueAuraProc: Triggering spell {} from aura {} proc", triggerSpellId, GetId()); target->CastCustomSpell(target, triggerSpellId, &value, nullptr, nullptr, true, nullptr, this, GetCasterGUID()); } + +int32 AuraEffect::GetTotalTicks() const +{ + uint32 totalTicks = 1; + if (m_amplitude) + { + totalTicks = GetBase()->GetMaxDuration() / m_amplitude; + + if (m_spellInfo->HasAttribute(SPELL_ATTR5_EXTRA_INITIAL_PERIOD)) + { + ++totalTicks; + } + } + + return totalTicks; +} diff --git a/src/server/game/Spells/Auras/SpellAuraEffects.h b/src/server/game/Spells/Auras/SpellAuraEffects.h index 290aea429..b85984341 100644 --- a/src/server/game/Spells/Auras/SpellAuraEffects.h +++ b/src/server/game/Spells/Auras/SpellAuraEffects.h @@ -84,7 +84,7 @@ public: void UpdatePeriodic(Unit* caster); uint32 GetTickNumber() const { return m_tickNumber; } - int32 GetTotalTicks() const { return m_amplitude ? (GetBase()->GetMaxDuration() / m_amplitude) : 1;} + int32 GetTotalTicks() const; void ResetPeriodic(bool resetPeriodicTimer = false) { if (resetPeriodicTimer) m_periodicTimer = m_amplitude; m_tickNumber = 0;} void ResetTicks() { m_tickNumber = 0; } diff --git a/src/server/game/Spells/SpellEffects.cpp b/src/server/game/Spells/SpellEffects.cpp index 1bf041b42..fd383e71b 100644 --- a/src/server/game/Spells/SpellEffects.cpp +++ b/src/server/game/Spells/SpellEffects.cpp @@ -5877,7 +5877,7 @@ void Spell::EffectKillCreditPersonal(SpellEffIndex effIndex) if (!unitTarget) return; - Player* player = unitTarget->ToPlayer(); + Player* player = unitTarget->GetCharmerOrOwnerPlayerOrPlayerItself(); if (!player) { return; @@ -5894,7 +5894,7 @@ void Spell::EffectKillCredit(SpellEffIndex effIndex) if (!unitTarget) return; - Player* player = unitTarget->ToPlayer(); + Player* player = unitTarget->GetCharmerOrOwnerPlayerOrPlayerItself(); if (!player) { return; diff --git a/src/server/game/World/IWorld.h b/src/server/game/World/IWorld.h index 41d73701a..05d3045e4 100644 --- a/src/server/game/World/IWorld.h +++ b/src/server/game/World/IWorld.h @@ -307,6 +307,7 @@ enum WorldIntConfigs CONFIG_BATTLEGROUND_QUEUE_ANNOUNCER_LIMIT_MIN_PLAYERS, CONFIG_ARENA_MAX_RATING_DIFFERENCE, CONFIG_ARENA_RATING_DISCARD_TIMER, + CONFIG_ARENA_PREV_OPPONENTS_DISCARD_TIMER, CONFIG_ARENA_AUTO_DISTRIBUTE_INTERVAL_DAYS, CONFIG_ARENA_GAMES_REQUIRED, CONFIG_ARENA_SEASON_ID, diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index b7d427100..4263c34ee 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -1163,25 +1163,26 @@ void World::LoadConfigSettings(bool reload) m_int_configs[CONFIG_BATTLEGROUND_SPEED_BUFF_RESPAWN] = 150; } - m_int_configs[CONFIG_ARENA_MAX_RATING_DIFFERENCE] = sConfigMgr->GetOption ("Arena.MaxRatingDifference", 150); - m_int_configs[CONFIG_ARENA_RATING_DISCARD_TIMER] = sConfigMgr->GetOption ("Arena.RatingDiscardTimer", 10 * MINUTE * IN_MILLISECONDS); + m_int_configs[CONFIG_ARENA_MAX_RATING_DIFFERENCE] = sConfigMgr->GetOption("Arena.MaxRatingDifference", 150); + m_int_configs[CONFIG_ARENA_RATING_DISCARD_TIMER] = sConfigMgr->GetOption("Arena.RatingDiscardTimer", 10 * MINUTE * IN_MILLISECONDS); + m_int_configs[CONFIG_ARENA_PREV_OPPONENTS_DISCARD_TIMER] = sConfigMgr->GetOption("Arena.PreviousOpponentsDiscardTimer", 2 * MINUTE * IN_MILLISECONDS); m_bool_configs[CONFIG_ARENA_AUTO_DISTRIBUTE_POINTS] = sConfigMgr->GetOption("Arena.AutoDistributePoints", false); - m_int_configs[CONFIG_ARENA_AUTO_DISTRIBUTE_INTERVAL_DAYS] = sConfigMgr->GetOption ("Arena.AutoDistributeInterval", 7); // pussywizard: spoiled by implementing constant day and hour, always 7 now - m_int_configs[CONFIG_ARENA_GAMES_REQUIRED] = sConfigMgr->GetOption ("Arena.GamesRequired", 10); - m_int_configs[CONFIG_ARENA_SEASON_ID] = sConfigMgr->GetOption ("Arena.ArenaSeason.ID", 1); - m_int_configs[CONFIG_ARENA_START_RATING] = sConfigMgr->GetOption ("Arena.ArenaStartRating", 0); - m_int_configs[CONFIG_ARENA_START_PERSONAL_RATING] = sConfigMgr->GetOption ("Arena.ArenaStartPersonalRating", 1000); - m_int_configs[CONFIG_ARENA_START_MATCHMAKER_RATING] = sConfigMgr->GetOption ("Arena.ArenaStartMatchmakerRating", 1500); + m_int_configs[CONFIG_ARENA_AUTO_DISTRIBUTE_INTERVAL_DAYS] = sConfigMgr->GetOption("Arena.AutoDistributeInterval", 7); // pussywizard: spoiled by implementing constant day and hour, always 7 now + m_int_configs[CONFIG_ARENA_GAMES_REQUIRED] = sConfigMgr->GetOption("Arena.GamesRequired", 10); + m_int_configs[CONFIG_ARENA_SEASON_ID] = sConfigMgr->GetOption("Arena.ArenaSeason.ID", 1); + m_int_configs[CONFIG_ARENA_START_RATING] = sConfigMgr->GetOption("Arena.ArenaStartRating", 0); + m_int_configs[CONFIG_ARENA_START_PERSONAL_RATING] = sConfigMgr->GetOption("Arena.ArenaStartPersonalRating", 1000); + m_int_configs[CONFIG_ARENA_START_MATCHMAKER_RATING] = sConfigMgr->GetOption("Arena.ArenaStartMatchmakerRating", 1500); m_bool_configs[CONFIG_ARENA_SEASON_IN_PROGRESS] = sConfigMgr->GetOption("Arena.ArenaSeason.InProgress", true); m_float_configs[CONFIG_ARENA_WIN_RATING_MODIFIER_1] = sConfigMgr->GetOption("Arena.ArenaWinRatingModifier1", 48.0f); m_float_configs[CONFIG_ARENA_WIN_RATING_MODIFIER_2] = sConfigMgr->GetOption("Arena.ArenaWinRatingModifier2", 24.0f); m_float_configs[CONFIG_ARENA_LOSE_RATING_MODIFIER] = sConfigMgr->GetOption("Arena.ArenaLoseRatingModifier", 24.0f); m_float_configs[CONFIG_ARENA_MATCHMAKER_RATING_MODIFIER] = sConfigMgr->GetOption("Arena.ArenaMatchmakerRatingModifier", 24.0f); - m_bool_configs[CONFIG_ARENA_QUEUE_ANNOUNCER_ENABLE] = sConfigMgr->GetOption ("Arena.QueueAnnouncer.Enable", false); - m_bool_configs[CONFIG_ARENA_QUEUE_ANNOUNCER_PLAYERONLY] = sConfigMgr->GetOption ("Arena.QueueAnnouncer.PlayerOnly", false); + m_bool_configs[CONFIG_ARENA_QUEUE_ANNOUNCER_ENABLE] = sConfigMgr->GetOption("Arena.QueueAnnouncer.Enable", false); + m_bool_configs[CONFIG_ARENA_QUEUE_ANNOUNCER_PLAYERONLY] = sConfigMgr->GetOption("Arena.QueueAnnouncer.PlayerOnly", false); m_bool_configs[CONFIG_OFFHAND_CHECK_AT_SPELL_UNLEARN] = sConfigMgr->GetOption("OffhandCheckAtSpellUnlearn", true); - m_int_configs[CONFIG_CREATURE_STOP_FOR_PLAYER] = sConfigMgr->GetOption("Creature.MovingStopTimeForPlayer", 3 * MINUTE * IN_MILLISECONDS); + m_int_configs[CONFIG_CREATURE_STOP_FOR_PLAYER] = sConfigMgr->GetOption("Creature.MovingStopTimeForPlayer", 3 * MINUTE * IN_MILLISECONDS); if (int32 clientCacheId = sConfigMgr->GetOption("ClientCacheVersion", 0)) { @@ -2046,7 +2047,7 @@ void World::SetInitialWorldSettings() ///- Initialize Battlegrounds LOG_INFO("server.loading", "Starting Battleground System"); - sBattlegroundMgr->CreateInitialBattlegrounds(); + sBattlegroundMgr->LoadBattlegroundTemplates(); sBattlegroundMgr->InitAutomaticArenaPointDistribution(); ///- Initialize outdoor pvp @@ -3039,12 +3040,7 @@ void World::_UpdateRealmCharCount(PreparedQueryResult resultCharCount) LoginDatabaseTransaction trans = LoginDatabase.BeginTransaction(); - LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_DEL_REALM_CHARACTERS_BY_REALM); - stmt->SetData(0, accountId); - stmt->SetData(1, realm.Id.Realm); - trans->Append(stmt); - - stmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_REALM_CHARACTERS); + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_REP_REALM_CHARACTERS); stmt->SetData(0, charCount); stmt->SetData(1, accountId); stmt->SetData(2, realm.Id.Realm); diff --git a/src/server/scripts/Commands/cs_misc.cpp b/src/server/scripts/Commands/cs_misc.cpp index b8a792627..b50068b6d 100644 --- a/src/server/scripts/Commands/cs_misc.cpp +++ b/src/server/scripts/Commands/cs_misc.cpp @@ -412,7 +412,7 @@ public: return false; } - Battleground* bg = sBattlegroundMgr->CreateNewBattleground(randomizedArenaBgTypeId, 80, 80, ArenaType(hcnt >= 2 ? hcnt : 2), false); + Battleground* bg = sBattlegroundMgr->CreateNewBattleground(randomizedArenaBgTypeId, GetBattlegroundBracketById(bgt->GetMapId(), bgt->GetBracketId()), ArenaType(hcnt >= 2 ? hcnt : 2), false); if (!bg) { handler->PSendSysMessage("Couldn't create arena map!"); diff --git a/src/server/scripts/Commands/cs_reload.cpp b/src/server/scripts/Commands/cs_reload.cpp index 8cb93cf28..a705a82e2 100644 --- a/src/server/scripts/Commands/cs_reload.cpp +++ b/src/server/scripts/Commands/cs_reload.cpp @@ -215,7 +215,7 @@ public: static bool HandleReloadBattlegroundTemplate(ChatHandler* handler) { LOG_INFO("server.loading", "Re-Loading Battleground Templates..."); - sBattlegroundMgr->CreateInitialBattlegrounds(); + sBattlegroundMgr->LoadBattlegroundTemplates(); handler->SendGlobalGMSysMessage("DB table `battleground_template` reloaded."); return true; } diff --git a/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp b/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp index 6c5162bb5..9336da65e 100644 --- a/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp +++ b/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp @@ -78,7 +78,6 @@ enum Events EVENT_HAND_OF_RAGNAROS, EVENT_MIGHT_OF_RAGNAROS, EVENT_LAVA_BURST, - EVENT_MAGMA_BLAST_MELEE_CHECK, EVENT_MAGMA_BLAST, EVENT_SUBMERGE, EVENT_LAVA_BURST_TRIGGER, @@ -122,6 +121,7 @@ public: boss_ragnarosAI(Creature* creature) : BossAI(creature, DATA_RAGNAROS), _isIntroDone(false), _hasYelledMagmaBurst(false), + _processingMagmaBurst(false), _hasSubmergedOnce(false), _isKnockbackEmoteAllowed(true) { @@ -143,6 +143,7 @@ public: } _hasYelledMagmaBurst = false; + _processingMagmaBurst = false; _hasSubmergedOnce = false; _isKnockbackEmoteAllowed = true; me->SetUInt32Value(UNIT_NPC_EMOTESTATE, 0); @@ -231,6 +232,28 @@ public: } } + void EnterEvadeMode() override + { + if (!me->getThreatMgr().getThreatList().empty()) + { + if (!_processingMagmaBurst) + { + // Boss try to evade, but still got some targets on threat list - it means that none of these targets are in melee range - cast magma blast + _processingMagmaBurst = true; + events.ScheduleEvent(EVENT_MAGMA_BLAST, 4000, PHASE_EMERGED, PHASE_EMERGED); + } + } + else + { + BossAI::EnterEvadeMode(); + } + } + + bool CanAIAttack(Unit const* victim) const override + { + return me->IsWithinMeleeRange(victim); + } + void UpdateAI(uint32 diff) override { if (!extraEvents.Empty()) @@ -297,7 +320,10 @@ public: if (!UpdateVictim()) { - return; + if (!_processingMagmaBurst) + { + return; + } } events.Update(diff); @@ -340,38 +366,10 @@ public: DoCastAOE(SPELL_LAVA_BURST); break; } - case EVENT_MAGMA_BLAST_MELEE_CHECK: - { - if (Unit* target = ObjectAccessor::GetUnit(*me, me->GetTarget())) - { - if (!target->IsAlive()) - { - me->SetTarget(ObjectGuid::Empty); - } - } - - if (!IsVictimWithinMeleeRange()) - { - if (Unit* target = SelectTarget(SelectTargetMethod::MaxThreat, 0, [&](Unit* u) { return u && u->IsPlayer() && me->IsWithinMeleeRange(u); })) - { - me->AttackerStateUpdate(target); - me->SetTarget(target->GetGUID()); - events.RepeatEvent(500); - } - else - { - events.ScheduleEvent(EVENT_MAGMA_BLAST, 4000, PHASE_EMERGED, PHASE_EMERGED); - } - } - else - { - _hasYelledMagmaBurst = false; - events.RepeatEvent(500); - } - break; - } case EVENT_MAGMA_BLAST: { + _processingMagmaBurst = false; + if (!IsVictimWithinMeleeRange()) { DoCastRandomTarget(SPELL_MAGMA_BLAST); @@ -383,7 +381,6 @@ public: } } - events.RescheduleEvent(EVENT_MAGMA_BLAST_MELEE_CHECK, 500, PHASE_EMERGED, PHASE_EMERGED); break; } case EVENT_MIGHT_OF_RAGNAROS: @@ -444,6 +441,7 @@ public: EventMap extraEvents; bool _isIntroDone; bool _hasYelledMagmaBurst; + bool _processingMagmaBurst; bool _hasSubmergedOnce; bool _isKnockbackEmoteAllowed; // Prevents possible text overlap @@ -480,7 +478,6 @@ public: events.RescheduleEvent(EVENT_WRATH_OF_RAGNAROS, 30000, PHASE_EMERGED, PHASE_EMERGED); events.RescheduleEvent(EVENT_HAND_OF_RAGNAROS, 25000, PHASE_EMERGED, PHASE_EMERGED); events.RescheduleEvent(EVENT_LAVA_BURST, 10000, PHASE_EMERGED, PHASE_EMERGED); - events.RescheduleEvent(EVENT_MAGMA_BLAST_MELEE_CHECK, 10000, PHASE_EMERGED, PHASE_EMERGED); events.RescheduleEvent(EVENT_SUBMERGE, 180000, PHASE_EMERGED, PHASE_EMERGED); events.RescheduleEvent(EVENT_MIGHT_OF_RAGNAROS, 11000, PHASE_EMERGED, PHASE_EMERGED); } diff --git a/src/server/scripts/EasternKingdoms/ZulGurub/boss_arlokk.cpp b/src/server/scripts/EasternKingdoms/ZulGurub/boss_arlokk.cpp index 6f28e80ff..be63141eb 100644 --- a/src/server/scripts/EasternKingdoms/ZulGurub/boss_arlokk.cpp +++ b/src/server/scripts/EasternKingdoms/ZulGurub/boss_arlokk.cpp @@ -426,7 +426,7 @@ public: bool OnGossipHello(Player* /*player*/, GameObject* go) override { - if (go->GetInstanceScript()) + if (go->GetInstanceScript() && !go->FindNearestCreature(NPC_ARLOKK, 25.0f)) { go->SetFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE); go->SendCustomAnim(0); diff --git a/src/server/scripts/EasternKingdoms/ZulGurub/instance_zulgurub.cpp b/src/server/scripts/EasternKingdoms/ZulGurub/instance_zulgurub.cpp index 3a2bcab66..5d29229f5 100644 --- a/src/server/scripts/EasternKingdoms/ZulGurub/instance_zulgurub.cpp +++ b/src/server/scripts/EasternKingdoms/ZulGurub/instance_zulgurub.cpp @@ -45,12 +45,6 @@ public: LoadDoorData(doorData); } - bool IsEncounterInProgress() const override - { - // not active in Zul'Gurub - return false; - } - void OnCreatureCreate(Creature* creature) override { switch (creature->GetEntry()) diff --git a/src/server/scripts/Spells/spell_generic.cpp b/src/server/scripts/Spells/spell_generic.cpp index 6da67612c..6af289974 100644 --- a/src/server/scripts/Spells/spell_generic.cpp +++ b/src/server/scripts/Spells/spell_generic.cpp @@ -1289,7 +1289,8 @@ class spell_gen_adaptive_warding : public AuraScript 45826 - East Frostwolf Warmaster 45828 - Dun Baldar North Marshal 45829 - Dun Baldar South Marshal - 45830 - Stonehearth Marshal */ + 45830 - Stonehearth Marshal + 45831 - Icewing Marshal */ class spell_gen_av_drekthar_presence : public AuraScript { PrepareAuraScript(spell_gen_av_drekthar_presence); diff --git a/src/server/scripts/World/npc_professions.cpp b/src/server/scripts/World/npc_professions.cpp index f984bcfcc..002eabbbf 100644 --- a/src/server/scripts/World/npc_professions.cpp +++ b/src/server/scripts/World/npc_professions.cpp @@ -18,7 +18,7 @@ /* ScriptData SDName: Npc_Professions SD%Complete: 80 -SDComment: Provides learn/unlearn/relearn-options for professions. Not supported: Unlearn engineering, re-learn engineering, re-learn leatherworking. +SDComment: Provides learn/unlearn/relearn-options for professions. Not supported: Unlearn engineering, re-learn engineering. SDCategory: NPCs EndScriptData */ @@ -86,10 +86,6 @@ there is no difference here (except that default text is chosen with `gameobject #define BOX_UNLEARN_WEAPON_SPEC "Do you really want to unlearn your weaponsmith specialty and lose all associated recipes? \n Cost: " -#define GOSSIP_LEARN_DRAGON "I wish to learn Dragonscale Leatherworking" -#define GOSSIP_LEARN_ELEMENTAL "I wish to learn Elemental Leatherworking" -#define GOSSIP_LEARN_TRIBAL "I wish to learn Tribal Leatherworking" - #define GOSSIP_LEARN_SPELLFIRE "Please teach me how to become a Spellcloth tailor" #define GOSSIP_UNLEARN_SPELLFIRE "I wish to unlearn Spellfire Tailoring" #define GOSSIP_LEARN_MOONCLOTH "Please teach me how to become a Mooncloth tailor" @@ -230,6 +226,37 @@ enum SpecializationQuests Q_MASTER_POTION = 10897, }; +// All referred to gossips (menu, menu_opt, actions) +enum Gossips +{ + // Leatherworking + GOSSIP_MENU_PETER_GALEN = 3067, + GOSSIP_MENU_THORKAF_DRAGONEYE = 3068, + GOSSIP_MENU_BRUMN_WINTERHOOF = 3069, + GOSSIP_MENU_SARAH_TANNER = 3070, + GOSSIP_MENU_CARYSSIA_MOONHUNTER = 3072, + GOSSIP_MENU_SEJIB = 3073, + + GOSSIP_MENU_UNLEARN_CONFIRM_DRAGONSCALE = 3075, + GOSSIP_MENU_UNLEARN_CONFIRM_ELEMENTAL = 3076, + GOSSIP_MENU_UNLEARN_CONFIRM_TRIBAL = 3077, + + GOSSIP_MENU_OPTION_TRAIN = 0, + GOSSIP_MENU_OPTION_CONFIRM_UNLEARN_DRAGONSCALE = 1, + GOSSIP_MENU_OPTION_CONFIRM_UNLEARN_ELEMENTAL = 1, + GOSSIP_MENU_OPTION_CONFIRM_UNLEARN_TRIBAL = 1, + + GOSSIP_TEXT_UNLEARN_CONFIRM_DRAGONSCALE = 10304, + GOSSIP_TEXT_UNLEARN_CONFIRM_ELEMENTAL = 10302, + GOSSIP_TEXT_UNLEARN_CONFIRM_TRIBAL = 10303, + + GOSSIP_MENU_GO_SOOTHSAYING_FOR_DUMMIES = 7058, + GOSSIP_MENU_OPTION_GO_LEARN_DRAGONSCALE = 4, + GOSSIP_MENU_OPTION_GO_LEARN_ELEMENTAL = 5, + GOSSIP_MENU_OPTION_GO_LEARN_TRIBAL = 6, + +}; + /*### # formulas to calculate unlearning cost ###*/ @@ -347,7 +374,11 @@ void ProfessionUnlearnSpells(Player* player, uint32 type) player->removeSpell(36259, SPEC_MASK_ALL, false); // Lionheart Executioner break; case S_UNLEARN_DRAGON: // S_UNLEARN_DRAGON + player->removeSpell(10619, SPEC_MASK_ALL, false); // Dragonscale Guantlets + player->removeSpell(10650, SPEC_MASK_ALL, false); // Dragonscale Breastplate player->removeSpell(36076, SPEC_MASK_ALL, false); // Dragonstrike Leggings + player->removeSpell(24655, SPEC_MASK_ALL, false); // Green Dragonscale Gauntlets + player->removeSpell(24654, SPEC_MASK_ALL, false); // Blue Dragonscale Leggings player->removeSpell(36079, SPEC_MASK_ALL, false); // Golden Dragonstrike Breastplate player->removeSpell(35576, SPEC_MASK_ALL, false); // Ebon Netherscale Belt player->removeSpell(35577, SPEC_MASK_ALL, false); // Ebon Netherscale Bracers @@ -362,6 +393,8 @@ void ProfessionUnlearnSpells(Player* player, uint32 type) player->removeSpell(35590, SPEC_MASK_ALL, false); // Primalstrike Belt player->removeSpell(35591, SPEC_MASK_ALL, false); // Primalstrike Bracers player->removeSpell(35589, SPEC_MASK_ALL, false); // Primalstrike Vest + player->removeSpell(10630, SPEC_MASK_ALL, false); // Gauntlets of the Sea + player->removeSpell(10632, SPEC_MASK_ALL, false); // Helm of Fire break; case S_UNLEARN_TRIBAL: // S_UNLEARN_TRIBAL player->removeSpell(35585, SPEC_MASK_ALL, false); // Windhawk Hauberk @@ -369,6 +402,8 @@ void ProfessionUnlearnSpells(Player* player, uint32 type) player->removeSpell(35588, SPEC_MASK_ALL, false); // Windhawk Bracers player->removeSpell(36075, SPEC_MASK_ALL, false); // Wildfeather Leggings player->removeSpell(36078, SPEC_MASK_ALL, false); // Living Crystal Breastplate + player->removeSpell(10621, SPEC_MASK_ALL, false); // Wolfshead Helm + player->removeSpell(10647, SPEC_MASK_ALL, false); // Feathered Breastplate break; case S_UNLEARN_SPELLFIRE: // S_UNLEARN_SPELLFIRE player->removeSpell(26752, SPEC_MASK_ALL, false); // Spellfire Belt @@ -950,44 +985,39 @@ public: bool OnGossipHello(Player* player, Creature* creature) override { + ClearGossipMenuFor(player); + if (creature->IsQuestGiver()) + { player->PrepareQuestMenu(creature->GetGUID()); + } - if (creature->IsVendor()) - AddGossipItemFor(player, GOSSIP_ICON_VENDOR, GOSSIP_TEXT_BROWSE_GOODS, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_TRADE); - - // pussywizard: NO UNLEARNING! LEATHERWORKING SPECIALTY IS A PERMANENT DECISION AND CANNOT BE CHANGED IN ANY WAY! - // pussywizard: CAN RE-LEARN ONLY THE ONE FOR WHICH QUEST IS COMPLETED! - - if (player->HasSkill(SKILL_LEATHERWORKING) && player->GetBaseSkillValue(SKILL_LEATHERWORKING) >= 250 && player->getLevel() > 49) + if (player->HasSkill(SKILL_LEATHERWORKING) && player->GetBaseSkillValue(SKILL_LEATHERWORKING) >= 225 && player->getLevel() > 40) { switch (creature->GetEntry()) { - case N_TRAINER_DRAGON1: //Peter Galen - case N_TRAINER_DRAGON2: //Thorkaf Dragoneye - if (!HasLeatherSpecialty(player) && (player->GetQuestRewardStatus(5141) || player->GetQuestRewardStatus(5145))) - AddGossipItemFor(player, GOSSIP_ICON_CHAT, GOSSIP_LEARN_DRAGON, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 1); + case N_TRAINER_DRAGON1: //Peter Galen + case N_TRAINER_DRAGON2: //Thorkaf Dragoneye if (player->HasSpell(S_DRAGON)) { - AddGossipItemFor(player, GOSSIP_ICON_TRAINER, GOSSIP_TEXT_TRAIN, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_TRAIN); + AddGossipItemFor(player, creature->GetEntry() == N_TRAINER_DRAGON1 ? GOSSIP_MENU_PETER_GALEN : GOSSIP_MENU_THORKAF_DRAGONEYE, GOSSIP_MENU_OPTION_TRAIN, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_TRAIN); + AddGossipItemFor(player, creature->GetEntry() == N_TRAINER_DRAGON1 ? GOSSIP_MENU_PETER_GALEN : GOSSIP_MENU_THORKAF_DRAGONEYE, GOSSIP_MENU_OPTION_CONFIRM_UNLEARN_DRAGONSCALE, GOSSIP_SENDER_MAIN, GOSSIP_MENU_UNLEARN_CONFIRM_DRAGONSCALE); } break; - case N_TRAINER_ELEMENTAL1: //Sarah Tanner - case N_TRAINER_ELEMENTAL2: //Brumn Winterhoof - if (!HasLeatherSpecialty(player) && (player->GetQuestRewardStatus(5144) || player->GetQuestRewardStatus(5146))) - AddGossipItemFor(player, GOSSIP_ICON_CHAT, GOSSIP_LEARN_ELEMENTAL, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 2); + case N_TRAINER_ELEMENTAL1: //Sarah Tanner + case N_TRAINER_ELEMENTAL2: //Brumn Winterhoof if (player->HasSpell(S_ELEMENTAL)) { - AddGossipItemFor(player, GOSSIP_ICON_TRAINER, GOSSIP_TEXT_TRAIN, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_TRAIN); + AddGossipItemFor(player, creature->GetEntry() == N_TRAINER_ELEMENTAL1 ? GOSSIP_MENU_SARAH_TANNER : GOSSIP_MENU_BRUMN_WINTERHOOF, GOSSIP_MENU_OPTION_TRAIN, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_TRAIN); + AddGossipItemFor(player, creature->GetEntry() == N_TRAINER_ELEMENTAL1 ? GOSSIP_MENU_SARAH_TANNER : GOSSIP_MENU_BRUMN_WINTERHOOF, GOSSIP_MENU_OPTION_CONFIRM_UNLEARN_ELEMENTAL, GOSSIP_SENDER_MAIN, GOSSIP_MENU_UNLEARN_CONFIRM_ELEMENTAL); } break; - case N_TRAINER_TRIBAL1: //Caryssia Moonhunter - case N_TRAINER_TRIBAL2: //Se'Jib - if (!HasLeatherSpecialty(player) && (player->GetQuestRewardStatus(5143) || player->GetQuestRewardStatus(5148))) - AddGossipItemFor(player, GOSSIP_ICON_CHAT, GOSSIP_LEARN_TRIBAL, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 3); + case N_TRAINER_TRIBAL1: //Caryssia Moonhunter + case N_TRAINER_TRIBAL2: //Se'Jib if (player->HasSpell(S_TRIBAL)) { - AddGossipItemFor(player, GOSSIP_ICON_TRAINER, GOSSIP_TEXT_TRAIN, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_TRAIN); + AddGossipItemFor(player, creature->GetEntry() == N_TRAINER_TRIBAL1 ? GOSSIP_MENU_CARYSSIA_MOONHUNTER : GOSSIP_MENU_SEJIB, GOSSIP_MENU_OPTION_TRAIN, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_TRAIN); + AddGossipItemFor(player, creature->GetEntry() == N_TRAINER_TRIBAL1 ? GOSSIP_MENU_CARYSSIA_MOONHUNTER : GOSSIP_MENU_SEJIB, GOSSIP_MENU_OPTION_CONFIRM_UNLEARN_TRIBAL, GOSSIP_SENDER_MAIN, GOSSIP_MENU_UNLEARN_CONFIRM_TRIBAL); } break; } @@ -997,36 +1027,35 @@ public: return true; } - void SendActionMenu(Player* player, Creature* creature, uint32 action) + bool OnGossipSelect(Player* player, Creature* creature, uint32 /*sender*/, uint32 action) override { + ClearGossipMenuFor(player); + switch (action) { - case GOSSIP_ACTION_TRADE: - player->GetSession()->SendListInventory(creature->GetGUID()); - break; case GOSSIP_ACTION_TRAIN: player->GetSession()->SendTrainerList(creature->GetGUID()); break; - // Learn Leather + case GOSSIP_MENU_UNLEARN_CONFIRM_DRAGONSCALE: + AddGossipItemFor(player, GOSSIP_MENU_UNLEARN_CONFIRM_DRAGONSCALE, GOSSIP_MENU_OPTION_CONFIRM_UNLEARN_DRAGONSCALE, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 1, DoMedUnlearnCost(player)); + SendGossipMenuFor(player, GOSSIP_TEXT_UNLEARN_CONFIRM_DRAGONSCALE, creature); + break; + case GOSSIP_MENU_UNLEARN_CONFIRM_ELEMENTAL: + AddGossipItemFor(player, GOSSIP_MENU_UNLEARN_CONFIRM_ELEMENTAL, GOSSIP_MENU_OPTION_CONFIRM_UNLEARN_ELEMENTAL, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 2, DoMedUnlearnCost(player)); + SendGossipMenuFor(player, GOSSIP_TEXT_UNLEARN_CONFIRM_ELEMENTAL, creature); + break; + case GOSSIP_MENU_UNLEARN_CONFIRM_TRIBAL: + AddGossipItemFor(player, GOSSIP_MENU_UNLEARN_CONFIRM_TRIBAL, GOSSIP_MENU_OPTION_CONFIRM_UNLEARN_TRIBAL, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 3, DoMedUnlearnCost(player)); + SendGossipMenuFor(player, GOSSIP_TEXT_UNLEARN_CONFIRM_TRIBAL, creature); + break; case GOSSIP_ACTION_INFO_DEF + 1: - ProcessCastaction(player, creature, S_DRAGON, S_LEARN_DRAGON, 0); + ProcessUnlearnAction(player, creature, S_UNLEARN_DRAGON, 0, DoMedUnlearnCost(player)); break; case GOSSIP_ACTION_INFO_DEF + 2: - ProcessCastaction(player, creature, S_ELEMENTAL, S_LEARN_ELEMENTAL, 0); + ProcessUnlearnAction(player, creature, S_UNLEARN_ELEMENTAL, 0, DoMedUnlearnCost(player)); break; case GOSSIP_ACTION_INFO_DEF + 3: - ProcessCastaction(player, creature, S_TRIBAL, S_LEARN_TRIBAL, 0); - break; - } - } - - bool OnGossipSelect(Player* player, Creature* creature, uint32 sender, uint32 action) override - { - ClearGossipMenuFor(player); - switch (sender) - { - case GOSSIP_SENDER_MAIN: - SendActionMenu(player, creature, action); + ProcessUnlearnAction(player, creature, S_UNLEARN_TRIBAL, 0, DoMedUnlearnCost(player)); break; } return true; @@ -1210,9 +1239,14 @@ class go_evil_book_for_dummies : public GameObjectScript public: go_evil_book_for_dummies() : GameObjectScript("go_evil_book_for_dummies") { } + inline bool HasLeatherSpecialty(Player* player) + { + return (player->HasSpell(S_DRAGON) || player->HasSpell(S_ELEMENTAL) || player->HasSpell(S_TRIBAL)); + } + bool OnGossipHello(Player* player, GameObject* gameobject) override { - //TAILORING SPEC + //ENGINEERING SPEC if (player->HasSkill(SKILL_ENGINEERING) && player->GetBaseSkillValue(SKILL_ENGINEERING) >= 225 && player->getLevel() >= 35) { if (player->GetQuestRewardStatus(3643) || player->GetQuestRewardStatus(3641) || player->GetQuestRewardStatus(3639)) @@ -1233,6 +1267,17 @@ public: } } + //LEATHERWORKING SPEC + if (player->HasSkill(SKILL_LEATHERWORKING) && player->GetBaseSkillValue(SKILL_LEATHERWORKING) >= 225 && player->getLevel() >= 40) + { + if (!HasLeatherSpecialty(player) && (player->GetQuestRewardStatus(5141) || player->GetQuestRewardStatus(5143) || player->GetQuestRewardStatus(5144) || player->GetQuestRewardStatus(5145) || player->GetQuestRewardStatus(5146) || player->GetQuestRewardStatus(5148))) + { + AddGossipItemFor(player, GOSSIP_MENU_GO_SOOTHSAYING_FOR_DUMMIES, GOSSIP_MENU_OPTION_GO_LEARN_DRAGONSCALE, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 5); + AddGossipItemFor(player, GOSSIP_MENU_GO_SOOTHSAYING_FOR_DUMMIES, GOSSIP_MENU_OPTION_GO_LEARN_ELEMENTAL, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 6); + AddGossipItemFor(player, GOSSIP_MENU_GO_SOOTHSAYING_FOR_DUMMIES, GOSSIP_MENU_OPTION_GO_LEARN_TRIBAL, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 7); + } + } + SendGossipMenuFor(player, player->GetGossipTextId(gameobject), gameobject->GetGUID()); return true; } @@ -1257,6 +1302,18 @@ public: case GOSSIP_ACTION_INFO_DEF + 4: ProcessUnlearnAction(player, nullptr, S_UNLEARN_GNOMISH, 0, DoHighUnlearnCost(player)); break; + //Learn Dragon + case GOSSIP_ACTION_INFO_DEF + 5: + ProcessCastaction(player, nullptr, S_DRAGON, S_LEARN_DRAGON, 0); + break; + //Learn Elemental + case GOSSIP_ACTION_INFO_DEF + 6: + ProcessCastaction(player, nullptr, S_ELEMENTAL, S_LEARN_ELEMENTAL, 0); + break; + //Learn Tribal + case GOSSIP_ACTION_INFO_DEF + 7: + ProcessCastaction(player, nullptr, S_TRIBAL, S_LEARN_TRIBAL, 0); + break; } } diff --git a/src/server/worldserver/worldserver.conf.dist b/src/server/worldserver/worldserver.conf.dist index 902bb72c5..28ab446c2 100644 --- a/src/server/worldserver/worldserver.conf.dist +++ b/src/server/worldserver/worldserver.conf.dist @@ -2923,6 +2923,14 @@ Arena.MaxRatingDifference = 150 Arena.RatingDiscardTimer = 600000 +# +# Arena.PreviousOpponentsDiscardTimer +# Description: Time (in milliseconds) after which the previous opponents will be ignored. +# Default: 120000 - (Enabled, 2 minutes - Blizzlike) +# 0 - (Disabled) + +Arena.PreviousOpponentsDiscardTimer = 120000 + # # Arena.AutoDistributePoints # Description: Automatically distribute arena points.