修復初始版本基於大神beebee102發出的版本,來源 Chuck Lu的博客園
補充:提取碼錯誤的原因是ChuckLu大佬輸入鏈接的時候不小心多加了個空格進去,處理一下就好。
本次修復基於之前修復至2020年11月的版本。主要詳細記錄爐石2021年3月底更新帶來的影響。
(maxiori提供的無殼版本帶有大量混淆,且使用de4dot + dnspy不易去除,不適合用於修復)
修復時間為2021年5月,按照順序修復,2020年11月之后的更新為本人獨立完成。
准備工作
-
准備win32版本的dnspy,更新前后新舊兩個版本的Managed文件,以及Hearthbuddy修復至2020年11月的版本。
-
以管理員身份運行dnspy,方便調試。
-
將Hearthbuddy加入程序集資源管理器,同時加入新舊兩個版本的Assembly-CSharp.dll。
-
嘗試使用dnspy啟動爐石兄弟。
不需要校驗任何文件hash值;
key隨便輸入什么字符;啟動成功
,教學結束。 -
如果沒有基礎,需要耐心,做好心理准備。同時,建議常做備份,而且每修改一處編譯一次。
第一個錯誤
錯誤提示
[CollectionManagerScene_COLLECTION] An exception occurred when calling CacheCustomDecks: System.MissingFieldException: Field 'CollectionDeckBoxVisual.m_isWild' not found.
at Triton.Game.Mono.MonoClass.method_5(String string_4)
at Triton.Game.Mono.MonoClass.method_2[T](String string_4)
at Triton.Bot.Utility.smethod_4()
at Triton.Bot.Logic.Bots.DefaultBot.DefaultBot.Struct57.MoveNext().
分析
Field 'xx' not found.
基本都是由於暴雪修改了方法名或者刪除了某方法導致的。
通過猜測,這里(牌庫收藏界面,讀取卡組) m_isWild 指的是卡組類型,之前是否是wild(狂野)就可以判斷,現在多了一個經典模式,所以出現了錯誤。
解決思路
-
通過編輯——搜索程序集,搜索兩個版本Assembly-CSharp中的
CollectionDeckBoxVisual
搜索
m_isWild
,在舊的程序集中可以找到,但是在新的程序集同一位置,已經被m_formatType
代替。正如我們猜測的一樣,這個代表的是卡組的模式,之前使用一個bool類型的
is_Wild
字段就可以代表狂野和標准,而現在新增了一個經典模式,暴雪官方使用了FormatType這樣一個枚舉來代替bool,來表示3種模式。 -
首先在Hearthbuddy的程序集中搜索FormatType,查看是否本來就有這個枚舉。
發現本身就存在這個枚舉類型,但是只有狂野和標准兩種值。
而在新Assembly-CSharp里的m_formatType前面的類型中右鍵,在新標簽頁中打開,發現新版本中,這個類型新增加了一個。
-
所以修復思路為,將原來的bool類型替換為新的枚舉類型,並將涉及到的引用也進行修改。
具體修復
-
在Hearthbuddy中的FormatType中右鍵編輯類,添加上經典模式的枚舉。
-
在Hearthbuddy中搜索CollectionDeckBoxVisual類,並定位到m_isWild。
-
右鍵分析,找到它的引用,先備存上。
-
我們分別進去看一看,通過閱讀代碼,分析出:第一個引用是與緩存卡組到本地,存儲為狂野、標准模式有關,第二個引用是與開始掛機時選擇卡組的時候判斷模式與卡組匹配時有關。
-
回到Hearthbuddy的CollectionDeckBoxVisual,修改m_isWild
-
修改返回類型
-
右鍵m_isWild編輯屬性
-
在簽名中點擊返回類型
-
首先點擊清除,然后點擊類型,在
Hearthbuddy-Triton.Game.Mapping
中找到FormatType,然后點擊確定。 -
右鍵get編輯方法,像上述步驟一樣。
-
-
修改屬性名(因為人家暴雪已經改名了,雖然你不改名,用着帶有歧義的
m_isWild
作為屬性名也沒關系,但是。。反正就是建議改了)-
右鍵m_isWild編輯屬性(還有右鍵get,同理)
-
直接將m_isWild修改為m_formatType,並確定。
-
-
修改獲取的結果
- 右鍵get編輯方法(C#)
- 修改
return base.method_2<bool>("m_isWild") ? FormatType.FT_WILD : FormatType.FT_UNKNOWN;
為return base.method_2<FormatType>("m_formatType");
,並編譯。(method_2不需要改變,具體見Chucklu的github中issue,MonoClass method usage)
-
-
修改原m_isWild,現m_formatType的引用,即上面3、4步中找到的2個方法。
-
先修復Triton.Bot.Utility.method_4()
-
簡單分析一下,這個方法需要把卡組模式存儲到customDeckCache中,所以需要先修改這個類。
-
右鍵在新標簽頁中打開,IsWild存儲於bool_0,像5-1的方法修改返回類型為FormatType,同時修改字段名為
formatType
。 -
找到
IsWild
屬性,右鍵IsWild、get、set,修改返回類型,同時修改屬性名與方法名。修改set時有不同。不要修改返回類型,而是修改下面的方法參數。先刪除原來的類型,然后點擊類型,找到FormatType,之后點擊添加。
-
右鍵編輯方法,修改為
public FormatType FormatType { get { return formatType; } set { if (!value.Equals(formatType)) { formatType = value; NotifyPropertyChanged(() => FormatType); } } }
-
-
回到Triton.Bot.Utility.method_4()右鍵編輯方法
原:
// Token: 0x0600121A RID: 4634 RVA: 0x000B7F84 File Offset: 0x000B6184 internal static void smethod_4() { try { CollectionDeckTray collectionDeckTray = CollectionDeckTray.Get(); if (collectionDeckTray != null) { foreach (TraySection traySection in collectionDeckTray.m_decksContent.m_traySections) { CollectionDeckBoxVisual deckBox = traySection.m_deckBox; long deckID = deckBox.GetDeckID(); string text = deckBox.m_deckName.Text; bool formatType = deckBox.m_formatType != FormatType.FT_UNKNOWN; if (deckID != -1L && deckBox.IsValid()) { CustomDeckCache customDeckCache = null; bool flag = true; foreach (CustomDeckCache customDeckCache2 in MainSettings.Instance.CustomDecks) { if (customDeckCache2.DeckId == deckID) { customDeckCache = customDeckCache2; customDeckCache.Name = text; customDeckCache.FormatType = (formatType ? FormatType.FT_WILD : FormatType.FT_UNKNOWN); customDeckCache.Save(); flag = false; break; } } if (customDeckCache == null) { customDeckCache = new CustomDeckCache(deckID) { DeckId = deckID, HeroCardId = deckBox.m_heroCardID, Name = deckBox.GetDeckNameText().Text, FormatType = (formatType ? FormatType.FT_WILD : FormatType.FT_UNKNOWN) }; } CollectionDeck deck = CollectionManager.Get().GetDeck(deckID); if (!deck.m_netContentsLoaded) { if (customDeckCache.CardIds.Count == 30 && flag) { MainSettings.Instance.CustomDecks.Add(customDeckCache); } } else { customDeckCache.CardIds.Clear(); foreach (CollectionDeckSlot collectionDeckSlot in deck.m_slots) { for (int i = 0; i < collectionDeckSlot.Count; i++) { customDeckCache.CardIds.Add(collectionDeckSlot.CardID); } } customDeckCache.Save(); if (flag) { MainSettings.Instance.CustomDecks.Add(customDeckCache); MainSettings.Instance.Save(); } } } } } } catch (Exception) { MainSettings.Instance.CustomDecks.Clear(); MainSettings.Instance.Save(); throw; } }
現:
using System; using log4net; using Triton.Bot.Settings; using Triton.Game.Mapping; namespace Triton.Bot { // Token: 0x02000228 RID: 552 public static partial class Utility { // Token: 0x0600121A RID: 4634 RVA: 0x000B7F84 File Offset: 0x000B6184 internal static void smethod_4() { try { CollectionDeckTray collectionDeckTray = CollectionDeckTray.Get(); if (collectionDeckTray != null) { foreach (TraySection traySection in collectionDeckTray.m_decksContent.m_traySections) { CollectionDeckBoxVisual deckBox = traySection.m_deckBox; long deckID = deckBox.GetDeckID(); string text = deckBox.m_deckName.Text; FormatType formatType = deckBox.m_formatType; if (deckID != -1L && deckBox.IsValid()) { CustomDeckCache customDeckCache = null; bool flag = true; foreach (CustomDeckCache customDeckCache2 in MainSettings.Instance.CustomDecks) { if (customDeckCache2.DeckId == deckID) { customDeckCache = customDeckCache2; customDeckCache.Name = text; customDeckCache.FormatType = formatType; customDeckCache.Save(); flag = false; break; } } if (customDeckCache == null) { customDeckCache = new CustomDeckCache(deckID) { DeckId = deckID, HeroCardId = deckBox.m_heroCardID, Name = deckBox.GetDeckNameText().Text, FormatType = formatType }; } CollectionDeck deck = CollectionManager.Get().GetDeck(deckID); if (!deck.m_netContentsLoaded) { if (customDeckCache.CardIds.Count == 30 && flag) { MainSettings.Instance.CustomDecks.Add(customDeckCache); } } else { customDeckCache.CardIds.Clear(); foreach (CollectionDeckSlot collectionDeckSlot in deck.m_slots) { for (int i = 0; i < collectionDeckSlot.Count; i++) { customDeckCache.CardIds.Add(collectionDeckSlot.CardID); } } customDeckCache.Save(); if (flag) { MainSettings.Instance.CustomDecks.Add(customDeckCache); MainSettings.Instance.Save(); } } } } } } catch (Exception) { MainSettings.Instance.CustomDecks.Clear(); MainSettings.Instance.Save(); throw; } } } }
-
-
修復 Triton.Bot.Logic.Bots.DefaultBot.DefaultBot.method_6(CollectionManagerScene)
-
首先右鍵編輯方法,直接編譯,發現有幾個變量名不符合規范,無法編譯,所以先修復變量名。
右鍵編輯字段,將
<>9
修改為Instance9
,類推。 -
回到method_6,再右鍵編輯方法,發現會將FormatType傳入Utility.smethod_2,所以需要先修改這個方法。右鍵在新標簽頁中打開。
-
右鍵編輯方法,在簽名中如6-1-1-2中,修改方法參數類型,刪除bool,加入FormatType。同時修改參數3的名稱。
-
發現他已經自動幫我們改好了
customDeckCache.FormatType == formatType
,所以我們不用再編輯方法了。
-
-
回到method_6,再右鍵編輯方法。
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using Buddy.Coroutines; using log4net; using Triton.Bot.Settings; using Triton.Common; using Triton.Game; using Triton.Game.Data; using Triton.Game.Mapping; namespace Triton.Bot.Logic.Bots.DefaultBot { // Token: 0x02000256 RID: 598 public partial class DefaultBot : IRunnable, IAuthored, IBase, IBot, IConfigurable { // Token: 0x06001368 RID: 4968 RVA: 0x000BCDAC File Offset: 0x000BAFAC private async Task method_6(CollectionManagerScene collectionManagerScene_0) { DefaultBot.ilog_0.InfoFormat("[CollectionManagerScene_COLLECTION]", Array.Empty<object>()); CollectionDeckTray collectionDeckTray = CollectionDeckTray.Get(); if (collectionDeckTray == null) { DefaultBot.ilog_0.DebugFormat("[CollectionManagerScene_COLLECTION] null.", Array.Empty<object>()); await Coroutine.Sleep(Client.Random.Next(1000, 2000)); } else { CollectionManager collectionManager = CollectionManager.Get(); if (collectionManager != null && collectionManager.IsFullyLoaded()) { Stopwatch stopwatch = this.method_9("CollectionManagerScene_COLLECTION"); if (stopwatch.IsRunning && stopwatch.ElapsedMilliseconds >= 1000L) { TraySection editingTraySection = collectionDeckTray.m_decksContent.m_editingTraySection; CollectionDeck taggedDeck = CollectionManager.Get().GetTaggedDeck(CollectionManager.DeckTag.Editing); if (taggedDeck != null && editingTraySection != null) { if (!collectionManager.GetDeck(taggedDeck.ID).NetworkContentsLoaded()) { DefaultBot.ilog_0.DebugFormat("[CollectionManagerScene_COLLECTION] !m_netContentsLoaded.", Array.Empty<object>()); await Coroutine.Sleep(Client.Random.Next(1000, 2000)); stopwatch.Reset(); } else { DefaultBot.ilog_0.InfoFormat("[CollectionManagerScene_COLLECTION] The contents of the deck have been obtained. Now clicking on the \"Done\" button.", Array.Empty<object>()); Client.LeftClickAt(collectionDeckTray.m_doneButton.m_ButtonText.Transform.Position); await Coroutine.Sleep(Client.Random.Next(1000, 2000)); stopwatch.Reset(); } } else if (!DefaultBotSettings.Instance.NeedsToCacheCustomDecks) { DefaultBot.ilog_0.InfoFormat("[CollectionManagerScene_COLLECTION] We do not need to cache custom decks. Now leaving the \"Collection Manager\".", Array.Empty<object>()); Client.LeftClickAt(collectionDeckTray.m_doneButton.m_ButtonText.Transform.Position); await Coroutine.Sleep(Client.Random.Next(1000, 2000)); stopwatch.Reset(); } else { try { Utility.smethod_4(); } catch (Exception arg) { DefaultBot.ilog_0.ErrorFormat("[CollectionManagerScene_COLLECTION] An exception occurred when calling CacheCustomDecks: {0}.", arg); BotManager.Stop(); return; } List<CollectionDeckBoxVisual> list = new List<CollectionDeckBoxVisual>(); foreach (TraySection traySection in collectionDeckTray.m_decksContent.m_traySections) { CollectionDeckBoxVisual deckBox = traySection.m_deckBox; long deckID = deckBox.GetDeckID(); string text = deckBox.m_deckName.Text; FormatType formatType = deckBox.m_formatType; if (deckID != -1L && !deckBox.IsLocked() && deckBox.IsEnabled()) { if (!collectionManager.GetDeck(deckID).m_netContentsLoaded) { if (Utility.smethod_2(deckID, text, formatType)) { DefaultBot.ilog_0.InfoFormat("[CollectionManagerScene_COLLECTION] We need the contents of this deck.", Array.Empty<object>()); list.Add(deckBox); } } else { DefaultBot.ilog_0.InfoFormat("[CollectionManagerScene_COLLECTION] We have the contents of this deck already.", Array.Empty<object>()); } } } if (list.Any<CollectionDeckBoxVisual>()) { DefaultBot.ilog_0.InfoFormat("[CollectionManagerScene_COLLECTION] Now choosing a random deck to load the contents of.", Array.Empty<object>()); list.ElementAt(Client.Random.Next(0, list.Count)).TriggerTap(); await Coroutine.Sleep(Client.Random.Next(1000, 2000)); stopwatch.Reset(); } else { List<CustomDeckCache> list2 = new List<CustomDeckCache>(); foreach (CustomDeckCache customDeckCache in MainSettings.Instance.CustomDecks) { if (collectionManager.GetDeck(customDeckCache.DeckId) == null) { list2.Add(customDeckCache); } } if (list2.Any<CustomDeckCache>()) { DefaultBot.ilog_0.DebugFormat("[CollectionManagerScene_COLLECTION] Now removing [{0}] decks that no longer exist.", list2.Count); foreach (CustomDeckCache customDeckCache2 in list2) { MainSettings.Instance.CustomDecks.Remove(customDeckCache2); try { File.Delete(CustomDeckCache.GetFileNameFor(customDeckCache2.DeckId)); } catch { } } } MainSettings.Instance.LastDeckCachePid = TritonHs.Memory.Process.Id; MainSettings.Instance.Save(); DefaultBotSettings.Instance.NeedsToCacheCustomDecks = false; GameEventManager.Instance.method_8(); stopwatch.Reset(); } } } else { if (!stopwatch.IsRunning) { stopwatch.Restart(); } DefaultBot.ilog_0.DebugFormat("[CollectionManagerScene_COLLECTION] Waiting to be in this state longer.", Array.Empty<object>()); await Coroutine.Sleep(Client.Random.Next(1000, 2000)); } } else { DefaultBot.ilog_0.DebugFormat("[CollectionManagerScene_COLLECTION] !IsFullyLoaded.", Array.Empty<object>()); await Coroutine.Sleep(Client.Random.Next(1000, 2000)); } } } } }
-
-
修復結果
第二個錯誤
錯誤提示
當模式不匹配的時候無限切換模式。
分析
在之前,切換模式只需要點擊一個按鈕就能切換狂野和標准,但是現在多加了一個模式,多了一個界面,所以無法選中模式,只會無限切換了。
解決思路
反編譯暴雪的官方文件,找到切換模式的方法,來代替切換模式。
根據經驗,此處在Triton.Bot.Logic.Bots.Defaultbot.Defaultbot.method_49。
同時,兄弟原來自帶的ConstructedMode與ConstructedGameRule用來選選擇狂野標准休閑天梯的也需要更改了,不需要再進行搭配,只要像爐石里面的4種模式:經典狂野標准休閑就可以了。
具體修復
-
首先把method_49清空,因為后面要先改的地方涉及到這里的引用,會編譯不成功報錯。
-
預添加兩個方法,來讀取當前模式。
-
找到Triton.Game.Mapping.Options,直接編輯類,添加兩個方法。
public FormatType GetFormatType() { return method_11<FormatType>("GetFormatType", Array.Empty<object>()); } public bool GetInRankedPlayMode() { return method_11<bool>("GetInRankedPlayMode", Array.Empty<object>()); }
-
-
通過反編譯爐石,找到官方通過使用VisualsFormatType來切換4種模式。
在Triton.Game.Mapping中添加類。
namespace Triton.Game.Mapping { public enum VisualsFormatType { Wild = 1, Standard = 2, Classic = 3, Casual = 4 } }
-
Triton.Bot.Logic.Bots.Defaultbot.DefaultBotSettings中用VisualsFormatType代替ConstructedMode與ConstructedGameRule。(思路是棄用其中一個,另一個改為VisualsFormatType的類型。因為直接編輯類不好編譯。)
-
找到ConstructedMode屬性,直接右鍵刪除。
-
找到ConstructedGameRule屬性,發現它是通過gameRule_0來存儲模式的。
-
類似第一個錯誤修復方法的5-1修改類型(VisualsFormatType)和名稱(visualsFormatType)
-
類似第一個錯誤修復方法的6-1-1-2修改ConstructedGameRule的返回類型
-
右鍵編輯方法,修改為
using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq.Expressions; using log4net; using Newtonsoft.Json; using Triton.Common; using Triton.Game.Mapping; namespace Triton.Bot.Logic.Bots.DefaultBot { public partial class DefaultBotSettings : JsonSettings { [DefaultValue(VisualsFormatType.Casual)] public VisualsFormatType ConstructedGameRule { get { return visualsFormatType; } set { if (!value.Equals(visualsFormatType)) { this.visualsFormatType = value; NotifyPropertyChanged(() => ConstructedGameRule); } DefaultBotSettings.ilog_0.InfoFormat("[DefaultBotSettings] ConstructedGameRule = {0}.", (GameRule)this.visualsFormatType); } } } }
-
-
如上2,依次修改observableCollection_3和AllConstructedRules
-
修改類型時,不是直接選擇類型,而是選擇泛型實例。
之后再選擇類型
然后添加再確定
-
修改完類型后如圖所示
-
右鍵編輯方法,修改為
using System; using System.Collections.ObjectModel; using System.ComponentModel; using log4net; using Newtonsoft.Json; using Triton.Common; using Triton.Game.Mapping; namespace Triton.Bot.Logic.Bots.DefaultBot { public partial class DefaultBotSettings : JsonSettings { [JsonIgnore] public ObservableCollection<VisualsFormatType> AllConstructedRules { get { ObservableCollection<VisualsFormatType> result; if ((result = this.observableCollection_3) == null) { ObservableCollection<VisualsFormatType> observableCollection = new ObservableCollection<VisualsFormatType>(); observableCollection.Add(VisualsFormatType.Casual); observableCollection.Add(VisualsFormatType.Classic); observableCollection.Add(VisualsFormatType.Standard); observableCollection.Add(VisualsFormatType.Wild); ObservableCollection<VisualsFormatType> observableCollection2 = observableCollection; this.observableCollection_3 = observableCollection; result = observableCollection2; } return result; } } } }
-
-
-
添加一個直接切換模式的方法
SwitchFormatTypeAndRankedPlayMode
-
找到Triton.Game.Mapping.DeckPickerTrayDisplay
-
直接右鍵編輯類,在最后添加上
public void SwitchFormatTypeAndRankedPlayMode(VisualsFormatType newVisualsFormatType) { method_8("SwitchFormatTypeAndRankedPlayMode", newVisualsFormatType); }
-
-
回到Triton.Bot.Logic.Bots.Defaultbot.Defaultbot.method_49
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using Buddy.Coroutines; using log4net; using Triton.Bot.Settings; using Triton.Common; using Triton.Game; using Triton.Game.Data; using Triton.Game.Mapping; namespace Triton.Bot.Logic.Bots.DefaultBot { public partial class DefaultBot : IRunnable, IAuthored, IBase, IBot, IConfigurable { private async Task method_49(TournamentScene tournamentScene_0) { if (DefaultBotSettings.Instance.ClientBackAttempts >= 5) { DefaultBot.ilog_0.ErrorFormat("[TournamentScene_DeckPicker] The client has been detected to be in a broken state. Please restart it as soon as possible as it cannot leave the current scene.", Array.Empty<object>()); DefaultBotSettings.Instance.ClientBroken = true; } GameEventManager.Instance.method_6(); await Coroutine.Yield(); if (!DefaultBotSettings.Instance.ClientBroken && (DefaultBotSettings.Instance.NeedsToCacheQuests || DefaultBotSettings.Instance.NeedsToCacheCustomDecks)) { DefaultBotSettings.Instance.ClientBackAttempts++; DefaultBot.ilog_0.InfoFormat("[TournamentScene_DeckPicker] Now returning to the Hub to cache quests/decks.", Array.Empty<object>()); await method_50(tournamentScene_0); } else { GameEventManager.Instance.method_7(); await Coroutine.Yield(); if (DefaultBotSettings.Instance.GameMode != GameMode.Constructed) { if (DefaultBotSettings.Instance.ClientBroken) { BotManager.Stop(); DefaultBot.ilog_0.ErrorFormat("[TournamentScene_DeckPicker] Your client needs to be restarted before continuing.", Array.Empty<object>()); await Coroutine.Yield(); } else { DefaultBot.ilog_0.InfoFormat("[TournamentScene_DeckPicker] Now returning to the Hub to change the game type.", Array.Empty<object>()); await method_50(tournamentScene_0); } } else { DeckPickerTrayDisplay deckPickerTrayDisplay = DeckPickerTrayDisplay.Get(); if (deckPickerTrayDisplay == null) { DefaultBot.ilog_0.DebugFormat("[TournamentScene_DeckPicker] DeckPickerTrayDisplay is null", Array.Empty<object>()); } else if (!deckPickerTrayDisplay.IsLoaded()) { DefaultBot.ilog_0.DebugFormat("[TournamentScene_DeckPicker] !DeckPickerTrayDisplay.IsLoaded", Array.Empty<object>()); } else if (string.IsNullOrEmpty(DefaultBotSettings.Instance.ConstructedCustomDeck)) { DefaultBot.ilog_0.ErrorFormat("[TournamentScene_DeckPicker] Please type a name for ConstructedCustomDeck!", Array.Empty<object>()); BotManager.Stop(); await Coroutine.Yield(); } else { FormatType formatType = Options.Get().GetFormatType(); bool inRankedPlayMode = Options.Get().GetInRankedPlayMode(); bool flag = false; if (DefaultBotSettings.Instance.ConstructedGameRule == VisualsFormatType.Casual) { if (inRankedPlayMode) { flag = true; DefaultBot.ilog_0.InfoFormat("[TournamentScene_DeckPicker] Now changing the game ruleset to 'Casual'.", Array.Empty<object>()); } } else if (DefaultBotSettings.Instance.ConstructedGameRule == VisualsFormatType.Wild) { if (formatType != FormatType.FT_WILD || !inRankedPlayMode) { flag = true; DefaultBot.ilog_0.InfoFormat("[TournamentScene_DeckPicker] Now changing the game ruleset to 'Wild'.", Array.Empty<object>()); } } else if (DefaultBotSettings.Instance.ConstructedGameRule == VisualsFormatType.Standard) { if (formatType != FormatType.FT_STANDARD) { flag = true; DefaultBot.ilog_0.InfoFormat("[TournamentScene_DeckPicker] Now changing the game ruleset to 'Standard'.", Array.Empty<object>()); } } else if (DefaultBotSettings.Instance.ConstructedGameRule == VisualsFormatType.Classic) { if (formatType != FormatType.FT_CLASSIC) { flag = true; DefaultBot.ilog_0.InfoFormat("[TournamentScene_DeckPicker] Now changing the game ruleset to 'Classic'.", Array.Empty<object>()); } } else { DefaultBot.ilog_0.ErrorFormat("[TournamentScene_DeckPicker] Unprocessed ConstructedGameRule: {0}.", DefaultBotSettings.Instance.ConstructedGameRule); BotManager.Stop(); await Coroutine.Yield(); return; } if (flag) { deckPickerTrayDisplay.SwitchFormatTypeAndRankedPlayMode(DefaultBotSettings.Instance.ConstructedGameRule); await Coroutine.Sleep(1500); } else { List<CustomDeckPage> customPages = deckPickerTrayDisplay.m_customPages; if (customPages != null) { foreach (CustomDeckPage item in customPages) { if (!item.AreAllCustomDecksReady()) { DefaultBot.ilog_0.DebugFormat("[TournamentScene_DeckPicker] !AreAllCustomDecksReady", Array.Empty<object>()); await Coroutine.Sleep(1000); return; } } //List<CustomDeckPage>.Enumerator enumerator = default(List<CustomDeckPage>.Enumerator); DefaultBot.ilog_0.InfoFormat("[TournamentScene_DeckPicker] Now attempting to select the desired custom deck.", Array.Empty<object>()); if (!(await DefaultBot.smethod_5("TournamentScene_DeckPicker", deckPickerTrayDisplay.m_selectedCustomDeckBox, customPages, deckPickerTrayDisplay, DefaultBotSettings.Instance.ConstructedCustomDeck))) { DefaultBotSettings.Instance.NeedsToCacheQuests = true; return; } } else { DefaultBot.ilog_0.InfoFormat("[TournamentScene_DeckPicker] Now attempting to select the desired basic deck.", Array.Empty<object>()); if (!await smethod_4("TournamentScene_DeckPicker", deckPickerTrayDisplay.m_selectedHeroButton, deckPickerTrayDisplay.m_heroButtons, deckPickerTrayDisplay, DefaultBotSettings.Instance.ConstructedCustomDeck)) { DefaultBotSettings.Instance.NeedsToCacheQuests = true; return; } } if (deckPickerTrayDisplay.m_rankedPlayButtons == null) { DefaultBot.ilog_0.DebugFormat("[TournamentScene_DeckPicker] DeckPickerTrayDisplay.m_rankedPlayButtons == null.", Array.Empty<object>()); } else { DefaultBotSettings.Instance.ClientBackAttempts = 0; TransitionPopup transitionPopup = GameMgr.Get().m_transitionPopup; if (transitionPopup != null && transitionPopup.IsShown()) { DefaultBot.ilog_0.InfoFormat("[TournamentScene_DeckPicker] The \"Matching\" popup is showing.", Array.Empty<object>()); await Coroutine.Sleep(1000); } else { PlayButton playButton = deckPickerTrayDisplay.m_playButton; UberText newPlayButtonText = playButton.m_newPlayButtonText; if (!playButton.IsEnabled()) { DefaultBot.ilog_0.InfoFormat("[TournamentScene_DeckPicker] The \"{0}\" is not enabled.", newPlayButtonText.Text); } else { UberText newPlayButtonText2 = playButton.m_newPlayButtonText; Vector3 center = newPlayButtonText2.m_TextMeshGameObject.Renderer.Bounds.m_Center; DefaultBot.ilog_0.InfoFormat("[TournamentScene_DeckPicker] Now clicking the \"{0}\" button.", newPlayButtonText2.Text); Client.LeftClickAt(center); await Coroutine.Sleep(3000); } } } } } } } } } }
修復結果
可以正常切換模式。但是切換之后會進行閃退,到下一個問題。
第三個錯誤
錯誤提示
無提示,直接閃退。
閃退之前的log:
2021-05-18 04:22:12,734 [5] INFO Logger (null) - [TournamentScene]
2021-05-18 04:22:12,836 [5] INFO Logger (null) - [TournamentScene_DeckPicker] Now changing the game ruleset to 'Wild'.
2021-05-18 04:22:14,401 [5] INFO Logger (null) - [TournamentScene]
2021-05-18 04:22:17,881 [26] INFO Logger (null) - [ProcessExited] The game process has closed. Hearthbuddy will now close.
分析
通過反編譯,定位到最后打印日志的位置,向下跟讀。
(沒有定位The game process has closed. Hearthbuddy will now close.,因為這個只是提示你遇到錯誤后將要關閉,並不能繼續追蹤找到錯誤觸發的地方)
通過經驗打上斷點,進行逐步調試,發現執行到這行再向下執行就會導致爐石閃退。
右鍵m_customPages在新標簽頁中打開,之后在新舊兩個版本的Assembly-CSharp.dll中搜索該字段。發現
原來:
現在:
由CustomDeckPage[]類型變為了List<CustomDeckPage>類型所以導致了錯誤。
解決思路
修改m_customPages的獲取方法。
具體修復
回到Hearthbuddy的m_customPages中,參考MonoClass method usage
集合的處理
public List Currencies
{
get
{
Class271 @Class = base.method_14("get_Currencies", Array.Empty());
if ( @Class != null)
{
return @class.method_25();
}
return null;
}
}
將原來的Class251修改為Class271
修復結果
start后不再閃退,而是瘋狂報”System.MissingFieldException: Field 'DeckPickerTrayDisplay.m_showingSecondPage' not found.“
進入下一個錯誤
第四個錯誤
錯誤提示
System.MissingFieldException: Field 'DeckPickerTrayDisplay.m_showingSecondPage' not found.
分析
本次更新將卡組頁面由2頁調整為3頁。故一些方法字段如m_showingSecondPage(展示第二頁)會被取消或者修改。
解決思路
先定位到Hearthbuddy中的m_showingSecondPage,然后在新舊兩個版本的Assembly-CSharp.dll中搜索該字段,定位到位置。
原來:
現在:
原來用bool類型就能代表2頁,現在多了1頁只能用int類型。
故思路為將現在的m_showingSecondPage修改為m_currentPageIndex,並修改其引用。
具體修復
-
修改m_showingSecondPage
-
右鍵m_showingSecondPage和get編輯屬性、簽名。。。
修改為mscorlib.System.Int32 ()類型
之后編輯方法為
using System; using System.Collections.Generic; using ns27; using Triton.Game.Mono; namespace Triton.Game.Mapping { [Attribute38("DeckPickerTrayDisplay")] public partial class DeckPickerTrayDisplay : MonoBehaviour { public int m_currentPageIndex { get { return base.method_2<int>("m_currentPageIndex"); } } } }
-
-
右鍵分析,回到原來引用m_showingSecondPage的地方
發現這個是控制選擇卡組的地方。通過翻頁等找到目標卡組。
原來是通過檢查當前頁是否有目標卡組,如果沒有的話進入下一頁(向左或者向右翻頁)來檢查第二頁是否有目標卡組。
現在改成了3頁,還用那樣的翻頁邏輯會處理的很麻煩,所以找到了一個更好的方法。
-
回到DeckPickerTrayDisplay,右鍵編輯類,在最下面加上一個方法
public void ShowPage(int pageNum, bool skipTraySlidingAnimation = false) { base.method_8("ShowPage", new object[] { pageNum, skipTraySlidingAnimation }); }
這個方法可以直接進行翻頁。
-
-
右鍵method_5,編輯方法修改為
(這部會影響下面一步的修改,所以建議先修改下一步,再回來修改這一步。當然,你可以先修改試試,但是之后還得回退回來。)
using System; using System.Linq; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using Buddy.Coroutines; using log4net; using Triton.Common; using Triton.Game; using Triton.Game.Data; using Triton.Game.Mapping; namespace Triton.Bot.Logic.Bots.DefaultBot { // Token: 0x02000256 RID: 598 public partial class DefaultBot : IRunnable, IAuthored, IBase, IBot, IConfigurable { // Token: 0x06001364 RID: 4964 RVA: 0x000BCCC0 File Offset: 0x000BAEC0 private static async Task<bool> smethod_5(string string_0, CollectionDeckBoxVisual collectionDeckBoxVisual_0, List<CustomDeckPage> list_1, DeckPickerTrayDisplay deckPickerTrayDisplay_0, string string_1) { CollectionDeckBoxVisual collectionDeckBoxVisual = null; if (list_1 != null && list_1.Count > 0) { for (int j = 0; j < list_1.Count; j++) { collectionDeckBoxVisual = list_1[j].m_customDecks.FirstOrDefault(x => x.m_deckName.Text.Equals(string_1)); if (collectionDeckBoxVisual != null) { DefaultBot.ilog_0.Info($"[{string_0}] Found the set deck on page {j + 1}, now turn to page {j + 1}."); deckPickerTrayDisplay_0.ShowPage(j); break; } } } bool result; if (collectionDeckBoxVisual == null) { DefaultBot.ilog_0.ErrorFormat("[{0}] The desired custom deck was not found.", string_0); BotManager.Stop(); await Coroutine.Yield(); result = false; } else if (collectionDeckBoxVisual.IsValid() && !collectionDeckBoxVisual.IsLocked()) { Vector3 position = collectionDeckBoxVisual.Transform.Position; Client.MouseOver(position); HighlightState highlightState = collectionDeckBoxVisual.m_highlightState; if (highlightState != null) { if (highlightState.m_CurrentState == ActorStateType.HIGHLIGHT_MOUSE_OVER || highlightState.m_CurrentState == ActorStateType.HIGHLIGHT_PRIMARY_ACTIVE) { Client.LeftClickAt(position); DefaultBotSettings.Instance.LastDeckId = collectionDeckBoxVisual.m_deckID; await Coroutine.Sleep(3000); return true; } } DefaultBot.ilog_0.ErrorFormat("[{1}] The \"{0}\" hero button was not highlighted.", collectionDeckBoxVisual.m_heroCardID, string_0); result = false; } else { DefaultBot.ilog_0.ErrorFormat("[{0}] The desired custom deck cannot be used because it is invalid or locked.", string_0); BotManager.Stop(); await Coroutine.Yield(); result = false; } return result; } } }
修復結果
可以翻頁並選中卡組。
報下一個錯誤:
Field 'DeckPickerTrayDisplay.m_rankedPlayButtons' not found.
進入下一個錯誤。
第五個錯誤
錯誤提示
Field 'DeckPickerTrayDisplay.m_rankedPlayButtons' not found.
分析
一般這種錯誤就是改名了。
解決思路
定位到m_rankedPlayButtons看一看。
原來:
現在:
進行修改。。
具體修復
不用修改返回類型,就修改一下方法名和里面的字符串就可以了。
(如果上一步修改了defaultbot,這部編譯會出現錯誤。)
修復結果
可以正常匹配了
第六個錯誤
錯誤提示
System.MissingMethodException: Method 'Entity.GetSpellPower' not found.
分析
同第五個錯誤
解決思路
原來:
現在:
法術新增了一個SPELL_SCHOOL類,而且取消了原來的GetSpellPower。
思路為在Hearthbuddy中添加TAG_SPELL_SCHOOL類,同時修改Entity.GetSpellPower及其引用。
具體修復
-
添加TAG_SPELL_SCHOOL類。
在Triton.Game.Mapping右鍵添加類。
using System; namespace Triton.Game.Mapping { public enum TAG_SPELL_SCHOOL { NONE, ARCANE,//奧數 FIRE,//火焰 FROST,//冰霜 NATURE,//自然 HOLY,//神聖 SHADOW,//暗影 FEL,//邪能 PHYSICAL_COMBAT } }
-
修改Triton.Game.Mapping.EntityBase.GetSpellPower()
-
修改字段
修改到這樣就可以了
修復結果
當前報錯消失,進入下一個報錯
第七個錯誤
錯誤提示
System.MissingMethodException: Method 'Entity.IsBasicCardUnlock' not found.
分析
同第六個錯誤
解決思路
同第六個錯誤
原來:
現在:
很簡單,類型都沒變,修改一下反射的方法名就可以了
修復結果
一切正常。
第八個錯誤
錯誤提示
[Tick] Exception during execution:Buddy.Coroutines.CoroutineUnhandledException:
Exception was thrown by coroutine ---> System.NullReferenceException: Object reference not set to an instance of an object.
at Triton.Game.HSCard.<Pickup>d__138.MoveNext()
具體修復
找到Triton.Game.HSCard類
-
修改
Pickup
方法為public async Task Pickup(int timeout = 500) { await Card.DoGrab(); Stopwatch stopwatch = Stopwatch.StartNew(); bool flag = false; while (stopwatch.ElapsedMilliseconds < timeout) { await Coroutine.Sleep(100); if (InputManager.Get().GetHeldCard() != null) { flag = true; break; } } if (flag) { ilog_0.InfoFormat("[Pickup] The card was picked up in {0} ms.", (object)stopwatch.ElapsedMilliseconds); } else { ilog_0.InfoFormat("[Pickup] The card was not able to be picked up in {0} ms.", (object)timeout); } }
-
修改
UseAt
方法為public async Task UseAt(int slot) { ilog_0.InfoFormat("[UseAt] {0}", (object)slot); ZonePlay battlefieldZone = GameState.Get().GetFriendlySidePlayer().GetBattlefieldZone(); int count = battlefieldZone.m_cards.Count; Vector3 cardPosition; if (count == 0) { cardPosition = battlefieldZone.GetCardPosition(0); await Client.MoveCursorHumanLike(cardPosition); Client.LeftClickAt(cardPosition); cardPosition = default(Vector3); cardPosition = default(Vector3); cardPosition = default(Vector3); } else if (slot > count) { cardPosition = battlefieldZone.GetCardPosition(count - 1); cardPosition.X += battlefieldZone.m_slotWidth / 2f; await Client.MoveCursorHumanLike(cardPosition); await Coroutine.Sleep(250); Client.LeftClickAt(cardPosition); cardPosition = default(Vector3); cardPosition = default(Vector3); cardPosition = default(Vector3); } else { cardPosition = battlefieldZone.GetCardPosition(slot - 1); cardPosition.X -= battlefieldZone.m_slotWidth / 2f; await Client.MoveCursorHumanLike(cardPosition); await Coroutine.Sleep(250); Client.LeftClickAt(cardPosition); cardPosition = default(Vector3); cardPosition = default(Vector3); cardPosition = default(Vector3); } }