Introduction

In this post we will go through the steps to create a simple trainer for the game Plague Inc: Evolved by Ndemic Creations.

Our goal with the trainer is to be able to add “Evolution Points”, which is used to evolve your disease, with a click of a button.

We will be using C# as the programming language for the trainer, and use the MInject library for DLL-injection and code loading in the mono-runtime.

Finding where the value is stored

The first thing we have to do is to locate where the game stores the evolution points value. To find that we have to decompile the game with a tool like dnSpy or dotPeek.

Locate the installed game files and open the file PlagueIncEvolved_Data\Managed\Assembly-CSharp.dll in your decompiler. Now locate where the evolution points are stored.

After some poking around I found that the evolution points value is stored at CGameManager.game.WorldInstance.diseases.First().evoPoints

Finding the process name

Using Process Explorer we can easily find the name of the process. Start process explorer and the game and look for something like this:

Plague Inc Process

Creating the loader

The first thing we need is a loader to inject our trainer code to the mono-process. Create a new .NET Framework console application called Loader and add a reference to the MInject DLL.

Then we need to get access to the running process. Open Program.cs and add a ProcessName const: private const string ProcessName = "PlagueIncEvolved";

In the Main method add the following to get a Process instance of the running process:

        var targetProcesses = Process.GetProcessesByName(ProcessName);
        if (targetProcesses.Length == 0)
        {
            Console.WriteLine($"Process {ProcessName} not found");
            Console.ReadKey();
            Environment.Exit(1);
        }

        var targetProcess = targetProcesses[0];

Then add the following to use MInject to inject the Trainer.dll we will create in the next step and invoke the Init method on the Trainer.Loader class.

            MonoProcess monoProcess;
            
            if (!MonoProcess.Attach(targetProcess, out monoProcess)) return;
            
            var assemblyBytes = File.ReadAllBytes("Trainer.dll");

            var monoDomain = monoProcess.GetRootDomain();
            monoProcess.ThreadAttach(monoDomain);
            monoProcess.SecuritySetMode(0);
    
            monoProcess.DisableAssemblyLoadCallback();

            var rawAssemblyImage = monoProcess.ImageOpenFromDataFull(assemblyBytes);
            var assemblyPointer = monoProcess.AssemblyLoadFromFull(rawAssemblyImage);
            var assemblyImage = monoProcess.AssemblyGetImage(assemblyPointer);
            var classPointer = monoProcess.ClassFromName(assemblyImage, "Trainer", "Loader");
            var methodPointer = monoProcess.ClassGetMethodFromName(classPointer, "Init");
        
            monoProcess.RuntimeInvoke(methodPointer);

            monoProcess.EnableAssemblyLoadCallback();    
    
            monoProcess.Dispose();

The complete Program.cs should look something like this

using System;
using System.Diagnostics;
using System.IO;
using MInject;

namespace Loader
{
    internal static class Program
    {
        private const string ProcessName = "PlagueIncEvolved";

        private static void Main()
        {
            var targetProcesses = Process.GetProcessesByName(ProcessName);
            if (targetProcesses.Length == 0)
            {
                Console.WriteLine($"Process {ProcessName} not found");
                Console.ReadKey();
                Environment.Exit(1);
            }
                
            var targetProcess = targetProcesses[0];
            
            MonoProcess monoProcess;
            if (!MonoProcess.Attach(targetProcess, out monoProcess)) return;
            
            var assemblyBytes = File.ReadAllBytes("Trainer.dll");

            var monoDomain = monoProcess.GetRootDomain();
            monoProcess.ThreadAttach(monoDomain);
            monoProcess.SecuritySetMode(0);
    
            monoProcess.DisableAssemblyLoadCallback();

            var rawAssemblyImage = monoProcess.ImageOpenFromDataFull(assemblyBytes);
            var assemblyPointer = monoProcess.AssemblyLoadFromFull(rawAssemblyImage);
            var assemblyImage = monoProcess.AssemblyGetImage(assemblyPointer);
            var classPointer = monoProcess.ClassFromName(assemblyImage, "Trainer", "Loader");
            var methodPointer = monoProcess.ClassGetMethodFromName(classPointer, "Init");
        
            monoProcess.RuntimeInvoke(methodPointer);

            monoProcess.EnableAssemblyLoadCallback();    
    
            monoProcess.Dispose();
        }
    }
}

Creating the trainer

Create a new project called Trainer and add a reference to the game Assembly-CSharp.dll that we decompiled in a previous step.

Add a new source file called Main.cs that will hold the trainer code.

Make sure that the class inherits from MonoBehaviour and add an Update method where we will add our trainer code.

using UnityEngine;

namespace Trainer
{
    internal class Main : MonoBehaviour
    {
        public void Update()
        {
        }
    }
}

To add some evolution points when a button is clicked, for example F1, update the Update method with the following.

        public void Update()
        {
            if (Input.GetKeyDown(KeyCode.F1))
            {
                CGameManager.game.WorldInstance.diseases.First().evoPoints += 100;
            }
        }

Now add a source file called Loader.cs and add the following code which will create a new game object and attach the Main-script to the newly created object.

using UnityEngine;

namespace Trainer
{
    public static class Loader
    {
        private static GameObject _load;
        
        public static void Init()
        {
            _load = new GameObject();
            _load.AddComponent<Main>();
            Object.DontDestroyOnLoad(_load);
        }
    }
}

Compile the trainer and loader and start the game and the trainer, and when we press F1 we will see that the evolution points is increased with 100 on each key press.

But we can make this a bit prettier by adding an overlay with the commands available and also a way to disable the trainer.

To disable the trainer all we need to do is adding functionality to destroy the GameObject stored in the Loader class. Add the following to the Loader.cs file.

        public static void Unload()
        {
            _Unload();
        }
        
        private static void _Unload()
        {
            Object.Destroy(_load);
        }

And to call the Unload method, update the Update method in the Main.cs file with the following.

        public void Update()
        {
            if (Input.GetKeyDown(KeyCode.F1))
            {
                CGameManager.game.WorldInstance.diseases.First().evoPoints += 100;
            }
            if(Input.GetKeyDown(KeyCode.Delete))
            {
                Loader.Unload();
            }
        }

To add an help overlay with the available commands, we need to add an OnGUI method to Main.cs with some labels to render.

        public void OnGUI()
        {
            GUI.Label(new Rect(0f, 0f, 250f, 25f), "F1 - Add 100 evolution points");
            GUI.Label(new Rect(0f, 25f, 250f, 25f), "DELETE - Unload trainer");
        }

The last thing we will add is a way to disable and enable the help overlay with a boolean field in the Main.cs, the complete Main.cs file should look something like the following.

using System.Linq;
using UnityEngine;

namespace Trainer
{
    internal class Main : MonoBehaviour
    {
        private bool _showHelp;
        
        public void Start()
        {
            _showHelp = true;
        }
        
        public void Update()
        {
            if (Input.GetKeyDown(KeyCode.F1))
            {
                CGameManager.game.WorldInstance.diseases.First().evoPoints += 100;
            }
            if (Input.GetKeyDown(KeyCode.F11))
            {
                _showHelp = !_showHelp;
            }
            if(Input.GetKeyDown(KeyCode.Delete))
            {
                Loader.Unload();
            }
        }
        
        public void OnGUI()
        {
            if (!_showHelp) return;
            
            GUI.Label(new Rect(0f, 0f, 250f, 25f), "F1 - Add 100 evolution points");
            GUI.Label(new Rect(0f, 25f, 250f, 25f), "F11 - Toggle help");
            GUI.Label(new Rect(0f, 50f, 250f, 25f), "DELETE - Unload trainer");
        }
    }
}

And the complete Loader.cs file should look something like the following.

using UnityEngine;

namespace Trainer
{
    public static class Loader
    {
        private static GameObject _load;
        
        public static void Init()
        {
            _load = new GameObject();
            _load.AddComponent<Main>();
            Object.DontDestroyOnLoad(_load);
        }
        
        public static void Unload()
        {
            _Unload();
        }
        
        private static void _Unload()
        {
            Object.Destroy(_load);
        }
    }
}

When we run the game and the trainer we will get an help overlay in the game.

Summary

In this tutorial we have learned how to inject code to a running Unity game with the use of MInject and how to alter values of the running process with the use of cusom code.

If you have any comments, feel free to post them below.

Complete code is available here: https://github.com/kza42/game-hacking/tree/main/PlagueIncTrainer