Automating skill checks in Dead by Daylight (Part I)

Today we will take advantage over killer in Dead by Daylight by trying to godly time skill checks (most of the time). For those who are not familiar with the game, here is how skill check looks like.

It works similar to active reload in Gears of War. You need to hit SPACE when progress is anywhere withing success zone. There is a sound queue right before skill check begins. Our bot won't actually care about that, so you can listen your favorite tune and still pass skill checks like a pr0. If you hit the perfect zone you get more points (and in some cases avoid negative effects), so we will try our best to hit it every single time. This is going to be a challenge cause progress is moving pretty damn fast ~320°/s.

What I describe below is "naive" implementation. I was successfully using it some time ago (however isn't hitting perfects all the time). When I recently returned to Dead by Daylight it turned out there were new features added, making my helper "unhelpful" in some cases. It won't work when killer is using Hex: Ruin perk (changing colors of skill check elements) or "Order" - Class I addon (25% chance that a Skill Check appears in a random location). I'm currently working on improved version supporting all cases and hopefully will be able to share it soon, along with some neat technical details.

First things first

As usual, we have to find out whenever skill check is active and what's its state, so we can act appropriately. Overall idea of how helper is supposed to work is as follows:

  • grab screenshot of skill check area
  • analyze it to establish current progress position and success zone start
  • press space when progress will reach success zone "soon"

First need to capture rectangle on the screen containing skill check circle.

We capture only this rectangle on the screen

Now it is time for some image processing. The goal is to find last "red-ish" and first "white-ish"pixel on the circle. We will start from top of the circle and continue clockwise stepping 1° at a time. Simple as that. When there is no "red-ish" pixel, we will assume that there is no active skill check.

using (Bitmap img = AutoIt.CaptureScreen((int)CIRCLE_OFFSET.X, (int)CIRCLE_OFFSET.Y, (int)CIRCLE_OFFSET.X + CIRCLE_DIAMETER + 1, (int)CIRCLE_OFFSET.Y + CIRCLE_DIAMETER + 1))
{
    SkillCheckActive = false;

    int check_start_index = SkillCheckProgressIndex != -1 ? SkillCheckProgressIndex : 0;

    for (int i = check_start_index; i < 360; ++i)
    {
        Vec3 dir = Vec3.Rotate(INITIAL_DIR, i * 1, VEC_UP);

        Vec3 pixel = CIRCLE_CENTER + dir;
        Color pixel_color = img.GetPixel((int)pixel.X, (int)pixel.Y);

        if (pixel_color.R > 220 && pixel_color.G < 50 && pixel_color.B < 20)
        {
            SkillCheckActive = true;
            SkillCheckProgressIndex = i;
        }

        if ((SkillCheckPerfectZoneIndex == -1 || SkillCheckPerfectZoneIndex > i) &&
            pixel_color.R > 240 && pixel_color.G > 240 && pixel_color.B > 240)
        {
            SkillCheckPerfectZoneIndex = i;
        }

        // we found all we need, can break now
        if (SkillCheckProgressIndex != -1 && SkillCheckPerfectZoneIndex != -1)
        {
            break;
        }
    }

    if (!SkillCheckActive)
    {
        SkillCheckPerfectZoneIndex = -1;
        SkillCheckProgressIndex = -1;
    }
    
    //...
}

As you can see on the image below 1° precision will cover pretty much every pixel of the circle (I debug draw a green-ish pixel next to each tested pixel until I found progress).

Blue line shows detected progress position and white line success zone start

Hitting the success zone

As soon as we know what is where we can decide when to send input to the game window. However there a few factors which may cause a non-perfect hit and sometimes even a miss. First let's take a look at skill check frame by frame recorded @60 fps.

You can see that it is not progressing smoothly. That definitely is not going to help. What we need to consider is also a delay between time when game frame has been rendered (which is unknown) and time when we captured the screen and process it (screen capturing can take a considerate amount of time if we are using "managed" implementations). Ideally we would like to be able to assess the situation at least 60 times per second. That gives us a budget of ~16.6 ms per update. Image processing is really simple and practically adds no delay.

This however will change when we no longer know where skill check may appear. When game is running at lower fps, we will be dealing with old data for longer periods of time, leading to further inaccuracies. I will address most of those factors/issues in part 2, when I'm done with more sophisticated implementation. For now we have this really simple approach, where we look 40 ms into the future to check if progress will enter success zone.

if (SkillCheckPerfectZoneIndex != -1 &&
    SkillCheckProgressIndex != -1 &&
    SkillCheckProgressIndex + (PROGRESS_SPEED * 40) >= SkillCheckPerfectZoneIndex) // trigger space when we are supposed to reach perfect zone soon
{
    AutoIt.SendKey(Tools.HWND, Input.VirtualKeyCode.SPACE, 50);
    AutoIt.Sleep(600);
}

Why 40 ms?

40 ms is a rough estimate of a few factors:

  • how old was captured screen (when our bot is able to re-asses at 60Hz captured screen could be as much as 16 ms old)
  • screen capture time itself (vastly depending on a resolution and implementation ~30 ms)
  • image processing time (it takes ~1 ms)
  • input processing delay (time between sending and input and actual processing it by the game logic - unknown, if any)

It stopped working...

I had this helper working for quiet some time but one fine day after a new patch it stopped working. I was sending the input, but nothing would happened. Looked like game wasn't receiving it... WTF?!? So I browsed through patch notes and I have found following statement:

"Disabled the use of macros while the game is running"

I noticed that no matter how I try to emulate input (sending key, sending key to the window, using original AutoIt or my C# lib), it is completely ignored. I started to wonder how is it possible to disable fake inputs globally... Turns out that fake/injected inputs can be distinguished from real ones in LowLevelKeyboardProc callback registered by SetWindowsHookEx. Each process can add its own callback to the list and everyone will be notified when key state has been sent. Then, inside LowLevelKeyboardProc, you can check lParam flags against 0x10 to test event-injected flag and handle it as you see fit. That's what our Canadian friends from Behaviour Interactive did to stop us from cheating.

Fortunately there is a weakness in this hooks system which we can exploit. Procs are called in reversed order to the registration order (last registered proc will be called first). It also assumes that when you are done with processing a key state you will call next callback in a list via CallNextHookEx. So... what we are gonna do is to register our own proc after Dead by Daylight registered theirs and we won't call next hook at the end. Voilà,  inputs fixed.

private const int WH_KEYBOARD_LL = 13;

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelProc lpfn, IntPtr hMod, uint dwThreadId);

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);

private delegate IntPtr LowLevelProc(int nCode, IntPtr wParam, IntPtr lParam);

private static IntPtr HookKeyboardCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
    return IntPtr.Zero;// CallNextHookEx(_hookID, nCode, wParam, lParam);
}

private static LowLevelProc KeyboardProc = HookKeyboardCallback;
private static IntPtr KeyboardHookID = IntPtr.Zero;

private static IntPtr SetKeyboardHook(LowLevelProc proc)
{
    using (Process curProcess = Process.GetCurrentProcess())
    using (ProcessModule curModule = curProcess.MainModule)
    {
        return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
    }
}

static void SetHooks()
{
    KeyboardHookID = SetKeyboardHook(KeyboardProc);
}

What's next

Contents of this post should be treated as an introduction to part II. Our ultimate goal is to implement a helper capable of hitting 99% perfect skill checks @60 fps and not failing any of them. Some may say that this is far easier when reading game memory. Yes, it definitely is, but I like a challenge.

If you have any thoughts, suggestions or have been doing similar helper yourself and want to share some conclusions, please leave a comment. glhf!