📜 ⬆️ ⬇️

Writing a bot for the Stronghold Kingdoms

History of writing bot for Stronghold Kingdoms

For a long time I approached the question of writing a bot for this game, but I didn’t have enough experience, I was too lazy, I tried to come from the wrong side.
As a result, having gained experience in writing and reverse developing C # code, I decided to achieve my goal.

Yes, as you may have noticed, C # is not casual - the game is written on it, using .net 2.0, which later put some sticks in my wheels.


Initially, I thought of writing a socket bot, which would only emulate a network protocol (which is not encrypted in any way), and having “source codes” (the result of il-code decompilation) is easily restored in a third-party application.
')
But it seemed to me tedious and dreary, because why fence the bike, if there are those "source codes".

Armed with Reflector, I started to deal with the entry point of the game (the code of which has not been obfuscated at all for more than three years, I wonder developers) - nothing special.

Analysis and partly wrong decision.

Obviously, the game project was originally created as a console application:

private static void Main (string [] args) as an entry point and its Program class hint at this, a class, by the way, is also private.

First of all, I rushed to make the class and method public, again with Reflexor’s forces, with Reflexil added to it, without knowing what to expect.

But suddenly I ran into a launcher who was downloading the modified file.
Without briefly fighting with him with the same Reflector and carrying out an autopsy, I pulled out the installation code of the arguments passed to the executable game file:

if (DDText.getText(0x17) == "XX") parameters = new string[] { "-InstallerVersion", "", "", "st" }; // st == steam else parameters = new string[] { "-InstallerVersion", "", "" }; parameters[1] = SelfUpdater.CurrentBuildVersion.ToString(); parameters[2] = DDText.getText(0); // ,     ,   “ru”, “de”, “en”  ..    local.txt   . UpdateManager.SetCommandLineParameters(parameters); //        System.Diagnostics.Process UpdateManager.StartApplication(); 


Parse:

if (DDText.getText(0x17) == "XX") - A string from the local.txt file next to the launcher.
So they have a strange test on steam / no-steam version: X - not Steam, XX - Steam. : \
parameters[1] = SelfUpdater.CurrentBuildVersion - The launcher version is quietly twitching from it, although the check in the client is strange, as I learned later, and you can simply specify a number much larger than the current one, “in reserve”, since The check goes only to obsolescence, so skazat, versions through the comparison of numbers "less-more".
parameters[2] = DDText.getText(0) - Having forged the version, I learned that it is the language of the game, in the format of “ru”, “de”, “en”, etc.
It is also loaded from the local.txt file.

By the way, the version of the launcher looks something like this:

 static SelfUpdater() { currentBuildVersion = 0x75; // 117, .. 1.17   . } 


And he made a magic batch file:

StrongholdKingdoms.exe -InstallerVersion 117 ru

Although it is possible and so:

StrongholdKingdoms.exe -InstallerVersion 100500 ru

What I said a little higher.

So, what we have: a slightly modified client and launcher bypass system, if you can call it that.
Having tried to start it all, I see that the game works and my patches did not harm it (although why would they harm them).

After that, I tried to connect the executable file of the game to the project as a class library and connect the namespace of the game - Kingdoms.

Then I cracked a lot of code: I tried to call Main, and emulate the Programm class, but for some reason the game crashed with a runtime that wasn’t enough for the framework when I tried to make it work.
He referred to the fact that the game uses many non-C # libraries and a lot of unsafe code. I did not find any real reasons.

The right decision

Having tormented for a long time and not finding a solution, I already spat. But for some reason I remembered the fork of the Terraria server - TShock (yeah, fork, as well - the guys also had fun with the decompiler) and its loading of modules (mods \ plug-ins) from the DLL.

This idea seemed interesting to me. Googling and found a way and code.
Having slightly penetrated into it and having checked it in my own project, I was horrified to find that it works (suddenly!).
Actually, the code:

 System.Reflection.Assembly A = System.Reflection.Assembly.LoadFrom(System.Windows.Forms.Application.StartupPath + @"\BotDLL.dll"); Type ClassType = A.GetType("BotDLL.Main", true); object Obj = Activator.CreateInstance(ClassType); System.Reflection.MethodInfo MI = ClassType.GetMethod("Inject"); MI.Invoke(Obj, null); 


Let's sort the code:
System.Reflection.Assembly - This is the thing that is responsible for creating links to files when connecting them to the project, only from code. And it also stores information about the versions of your project and copyrights (yes, the same AssemblyInfo.cs in all your projects).
Assembly.LoadFrom(System.Windows.Forms.Application.StartupPath + @"\BotDLL.dll") - Load our library.
Then we call the function inside this class Inject (), which is essentially the beginning of the bot. =)
I tried the code that I sketched in a third-party application - inject worked.

Client patching

Now we come to the most interesting part - we introduce the calling code into the game.
Having tried to stick it into the impudent one into Main through the replacement of the code with the help of Reflexil, the patch that was not patched as a result of decompilation was successfully sent. Well, or I just was lazy, it does not matter.
I went to search in this very Main for guaranteed calling of third-party functions (outside the main if branches, etc.) rather quickly found a call to the MySettings.load () function, which loaded the settings of the game when it started.
But again, there was a mountain of code that did not want to compile without tambourines.
But by luck, next to it is the hasLoggedIn () boolean function which returns the only bool value just when the game is started:
return (this.HasLoggedIn || (this.Username.Length > 0));
I was immediately delighted and this function was immediately converted to this:

 if (!IsStarted) { System.Reflection.Assembly A = System.Reflection.Assembly.LoadFrom(System.Windows.Forms.Application.StartupPath + @"\BotDLL.dll"); Type ClassType = A.GetType("BotDLL.Main", true); object Obj = Activator.CreateInstance(ClassType); System.Reflection.MethodInfo MI = ClassType.GetMethod("Inject"); MI.Invoke(Obj, null); IsStarted = true; } return (this.HasLoggedIn || (this.Username.Length > 0)); 


We will deal with it.
if (! IsStarted) - we had to add this check, and for this we need to add an additional field to the MySettings class, since our function is called more than once, and we don’t really need several bot threads. This is done all the same Reflexil'om.
Well, the main code we have already disassembled a little higher.
And in the end we return what was there to be. =)

So - the bot itself

Inject function:

  public void Inject() { AllocConsole(); Console.Title = "SHKBot"; Console.WriteLine("DLL !"); Thread Th = new Thread(SHK); Th.Start(); BotForm FBot = new BotForm(); FBot.Show(); } … [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] static extern bool AllocConsole(); 


First we call the open console window function - it will be easier for debugging.
After we launch the stream with our main bot cycle - SHK ();
And at the same time we open the bot control form for convenience.

Next thing is small - to implement the functionality you need.
Here is the rest of my code - here I implemented an automated trading system.
In order for it to work, you first need to “cache” the villages in each session — open each of the villages you are going to trade.
This code helps doubtfully, and I haven’t got to the bottom of other ways of automatically downloading villages:

 InterfaceMgr.Instance.selectVillage(VillageID); GameEngine.Instance.downloadCurrentVillage(); 


Here is the SHK function code:
Hidden text
 public void SHK() { Console.WriteLine(" !"); while (!GameEngine.Instance.World.isDownloadComplete()) { Console.WriteLine("   !"); Thread.Sleep(5000); // 5 sec Console.Clear(); } Console.WriteLine(" !    ."); Console.WriteLine("\n======| DEBUG INFO |======"); Console.WriteLine(RemoteServices.Instance.UserID); Console.WriteLine(RemoteServices.Instance.UserName); List<int> VillageIDs = GameEngine.Instance.World.getListOfUserVillages(); foreach (int VillageID in VillageIDs) { WorldMap.VillageData Village = GameEngine.Instance.World.getVillageData(VillageID); Console.WriteLine("[] " + Village.m_villageName + " - " + VillageID); InterfaceMgr.Instance.selectVillage(VillageID); GameEngine.Instance.downloadCurrentVillage(); } Console.WriteLine("======| ========== |======\n"); while (true) { try { //   -   } catch (Exception ex) { Console.WriteLine("\n======| EX INFO |======"); Console.WriteLine(ex); Console.WriteLine("======| ======= |======\n"); } } } 


Code of control form:
Hidden text
 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Threading; using Kingdoms; using System.CodeDom.Compiler; using Microsoft.CSharp; using System.Reflection; namespace BotDLL { public partial class BotForm : Form { Thread TradeThread; bool IsTrading = false; public void Log(string Text) { Console.WriteLine(Text); richTextBox_Log.Text = Text + "\r\n" + richTextBox_Log.Text; } public BotForm() { CheckForIllegalCrossThreadCalls = false; InitializeComponent(); this.Show(); Log("  ."); listBox_ResList.SelectedIndex = 0; Log("  ..."); TradeThread = new Thread(Trade); TradeThread.Start(); } private void button_Trade_Click(object sender, EventArgs e) { //         if (GameEngine.Instance.World.isDownloadComplete() && textBox_TradeTargetID.Text.Length > 0) { try { if (!IsTrading) //    { button_Trade.Text = ""; IsTrading = true; //   } else //   { button_Trade.Text = ""; IsTrading = false; } } catch (Exception ex) { Console.WriteLine("\n======| EX INFO |======"); Log(ex.ToString()); Console.WriteLine("======| ======= |======\n"); } } } public void Trade() { Log("  !"); int Sleep = 0; while (true) //   { Sleep = 60 + new Random().Next(-5, 60); if (IsTrading) { Log("[" + DateTime.Now + "]   \"" + listBox_ResList.SelectedItem.ToString() + "\""); //  ID    int ResID = int.Parse(listBox_ResList.SelectedItem.ToString().Replace(" ", "").Split('-')[0]); int TargetID = int.Parse(textBox_TradeTargetID.Text); //  ID - List<int> VillageIDs = GameEngine.Instance.World.getListOfUserVillages(); //     foreach (int VillageID in VillageIDs) //   { //    (       ) if (GameEngine.Instance.getVillage(VillageID) != null) { //       WorldMap.VillageData Village = GameEngine.Instance.World.getVillageData(VillageID); VillageMap Map = GameEngine.Instance.getVillage(VillageID); //    int ResAmount = (int)Map.getResourceLevel(ResID); // -    int MerchantsCount = Map.calcTotalTradersAtHome(); // -    Log("  " + VillageID + "  " + MerchantsCount + " "); //  int SendWithOne = int.Parse(textBox_ResCount.Text); // -    int MaxAmount = MerchantsCount * SendWithOne; // -   if (ResAmount < MaxAmount) //        MerchantsCount = (int)(ResAmount / SendWithOne); //      if (MerchantsCount > 0) //     { TargetID = GameEngine.Instance.World.getRegionCapitalVillage(Village.regionID); //   ,  textBox_TradeTargetID.Text = TargetID.ToString(); //        GameEngine.Instance.getVillage(VillageID).stockExchangeTrade(TargetID, ResID, MerchantsCount * SendWithOne, false); AllVillagesPanel.travellersChanged(); //   ( )  GUI- } } } Log("    " + Sleep + "   " + DateTime.Now.AddSeconds(Sleep).ToString("HH:mm:ss")); Console.WriteLine(); } Thread.Sleep(Sleep * 1000); // ,   .   . } } private void BotForm_FormClosing(object sender, FormClosingEventArgs e) { try { TradeThread.Abort(); } catch {} } private void button_MapEditing_Click(object sender, EventArgs e) { button_MapEditing.Text = (!GameEngine.Instance.World.MapEditing).ToString(); GameEngine.Instance.World.MapEditing = !GameEngine.Instance.World.MapEditing; } private void button_Exec_Click(object sender, EventArgs e) { if (richTextBox_In.Text.Length == 0 || !GameEngine.Instance.World.isDownloadComplete()) return; richTextBox_Out.Text = ""; // *** Example form input has code in a text box string lcCode = richTextBox_In.Text; ICodeCompiler loCompiler = new CSharpCodeProvider().CreateCompiler(); CompilerParameters loParameters = new CompilerParameters(); // *** Start by adding any referenced assemblies loParameters.ReferencedAssemblies.Add("System.dll"); loParameters.ReferencedAssemblies.Add("System.Data.dll"); loParameters.ReferencedAssemblies.Add("System.Windows.Forms.dll"); loParameters.ReferencedAssemblies.Add("StrongholdKingdoms.exe"); // *** Must create a fully functional assembly as a string lcCode = @"using System; using System.IO; using System.Windows.Forms; using System.Collections.Generic; using System.Text; using Kingdoms; namespace NSpace { public class NClass { public object DynamicCode(params object[] Parameters) { " + lcCode + @" return null; } } }"; // *** Load the resulting assembly into memory loParameters.GenerateInMemory = false; // *** Now compile the whole thing CompilerResults loCompiled = loCompiler.CompileAssemblyFromSource(loParameters, lcCode); if (loCompiled.Errors.HasErrors) { string lcErrorMsg = ""; lcErrorMsg = loCompiled.Errors.Count.ToString() + " Errors:"; for (int x = 0; x < loCompiled.Errors.Count; x++) lcErrorMsg = lcErrorMsg + "\r\nLine: " + loCompiled.Errors[x].Line.ToString() + " - " + loCompiled.Errors[x].ErrorText; richTextBox_Out.Text = lcErrorMsg + "\r\n\r\n" + lcCode; return; } Assembly loAssembly = loCompiled.CompiledAssembly; // *** Retrieve an obj ref – generic type only object loObject = loAssembly.CreateInstance("NSpace.NClass"); if (loObject == null) { richTextBox_Out.Text = "Couldn't load class."; return; } object[] loCodeParms = new object[1]; loCodeParms[0] = "SHKBot"; try { object loResult = loObject.GetType().InvokeMember( "DynamicCode", BindingFlags.InvokeMethod, null, loObject, loCodeParms); //DateTime ltNow = (DateTime)loResult; if (loResult != null) richTextBox_Out.Text = "Method Call Result:\r\n\r\n" + loResult.ToString(); } catch (Exception ex) { Console.WriteLine("\n======| EX INFO |======"); Console.WriteLine(ex); Console.WriteLine("======| ======= |======\n"); } } } } 



Initially, I wanted to plug into the NLua bot (Lua library for C #), but since it supports only 3.5+ frameworks, I for some reason did not want to use the old versions so I did:
For convenience, I entered the execution of the code in real time at the Sharp itself - I was tired of restarting the game after recompiling time after time.
Used this tutorial .

Total

Advantages of such a decision:
  1. Access to all game code, as if you have its source code.
  2. You can make your own premium card system with a queue of buildings, studying research without restrictions and even more:
    • The algorithm for the resale of goods among the regions surrounding you.
    • Autobuilding the village "on the layout" removed from the already existing, as an example.
    • Auto-time of various units.
    • Avtopochinki castle while you are not.
    • Automatic collection of guaranteed cards for the time.

  3. And of course the dynamic execution of the code.
  4. Funny detection protection. Well, a couple of conditions in order not to send suspicious requests-dummy.

Minuses:
  1. We'll have to patch the client with each version handles. Or, you can write a patcher using Mono.Cecil or an analogue in the framework.
  2. Unlike premium cards, you have to keep the client always on and online.
  3. The game is rather big, so it’s definitely not an hour to learn the “API”. Although if desired, and tools versed in years - there would be a desire. And in any case, better than messing with packages.


Here is the whole thing:


Interested recommend to look at the following classes of the game:
Class list
  • Gameengine
  • GameEngine.Instance
  • GameEngine.Instance.World
  • Worldmap
  • WorldMap.VillageData
  • Remoteservice
  • RemoteServices.Instance
  • AllVillagesPanel
  • Villagemap



At the time of this writing, the game version was 2.0.18.6.
You can download this particular version with the executable file of the game and the bot here .
Do not worry, do not steal personal data. =) I'm tired of the game, so I share with the community experience.

Source codes are available here .
If you are thinking of using source codes - use a clean executable file (not patched by you) as a class library, and also disable copying this link to the final directory, so as not to accidentally replace the patched one.

I apologize for the writing style of the article - I am writing for the first time. Perhaps I’m jumping a lot from topic to topic, or I’m describing few technical aspects.
It may seem to you that there is a lot of water here, but this article was originally conceived as a small story - probably therefore.

Source: https://habr.com/ru/post/225663/


All Articles