修复初始版本基于大神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); } }