How to farm XP in Friday the 13th while AFK

Today we will create our first bot for doing boring grinding for us, so we can do cool stuff in the meantime. We already have all tools for the job, now it's time to creatively use them. Let's get to it! And yeah, today's language of choice is C#... I will be relying on my C# port of AutoIt introduced in previous post Custom C# implementation of AutoIt

Update: Full source code is available for patrons on https://www.patreon.com/bottersgonnabot

Concept

In Friday the 13th you gain XP for performing various actions in game and additional 500XP for staying till the end of the match. Match ends when all counselors have died or escaped or at 20 minutes mark. When you die of escape and there are other players still fighting for survival, you enter spectate mode. If you leave before match ends you keep whatever XP you have gained before leaving. When you level up you gain CP (character points) which you can spend on rolling perks and unlocking Jason kills variants. Our goal is to effectively gain CP by leveling up while AFK.

Simplest bot

The most trivial bot would join a match and simply send random inputs to keep resetting idle kick timer (2 minutes) until match ends. Choosing very loud character and sprinting forward all the time would most definitely attract Jason attention resulting in a quick death, minimizing chance for other people to spectate your dumb behavior. You can earn some XP doing so, however there is a risk of being reported as others can see what your doing.

Farming together

If you have a botting friend who also has this game, you can farm XP a lot faster by playing private matches (PM). One player hosts the game and invites other players. Host can then decide who is going to be Jason for the next round.

So how can you earn XP the fastest way in private match? Obviously you can talk on a voice chat with your Jason friend and tell him where you have spawned so he can quickly kill you ending the match. Or you can kill yourself by jumping through broken window a couple of times.

How to automate that

Above methods are hard to automate as we do not know players' positions nor the coordinates of the nearest window :/ But fear not, there is much simpler way to end match: whoever is a guest just leaves the match. As a result host earns XP for completed match (in case of Jason there are additional points for no survivors netting 800XP). The issue is that only one player gets the XP, so we have to take turns. We also have to keep in mind that all sorts of fuckups can happen while playing (like network disconnects, game crashes/freezes, etc.). That's why we will restart the game after each match! Host can kill the game as soon as match outro begins (XP is being stored just before that) which is ~1 minute since map loading screen has been showed up. In my case single cycle (game restart, setting up private match, loading map and ending match) takes ~2 minutes. Assuming I'm hosting as Jason every second match I gain 800XP per 4 minutes making it 12kXP per hour. I would take that, especially when I sleep! Here is quick run down of all steps for both host and guest. On the following videos you can see an actual bot in action.

Host

  • start the game
  • enter Private Match
  • invite your friend
  • ready up and start the game
  • quit game after a minute since map loading started
Guest

  • start the game
  • wait for invite and accept it
  • ready up and start the game
  • leave match during initial cut scene
  • quit game after a minute since map loading started

Switch roles, rinse, repeat and profit!

Let's get coding started

First of all we need a some tool functions for managing game instance itself (mainly killing and restarting).

public const string WINDOW_NAME = "SummerCamp  - B6612";
public const string PROCESS_NAME = "SummerCamp";
public static IntPtr HWND; // handle to game window
public static string LAUNCHER_LOCATION = "steam://rungameid/438740";

public static void UpdateWindowHandle()
{
    HWND = AutoIt.WinGetHandle(WINDOW_NAME);
}

public static void KillGame()
{
    HWND = IntPtr.Zero;
    AutoIt.ProcessAndChildrenClose(PROCESS_NAME);
    LogLine("Game killed!");
}

public static bool StartGame()
{
    AutoIt.Run(LAUNCHER_LOCATION);

    if (WaitForGameStarted(60))
    {
        UpdateWindowHandle();
        LogLine("Game started!");
        return true;
    }
    else
    {
        LogLine("Game start failed!");
        return false;
    }
}

public static bool WaitForGameStarted(int max_seconds)
{
    for (int i = 0; i < max_seconds; ++i)
    {
        AutoIt.WinActivate(WINDOW_NAME);

        if (AutoIt.WinActive(WINDOW_NAME))
        {
            UpdateWindowHandle();
            return true;
        }

        AutoIt.Sleep(1000);
    }
    return false;
}

public static void RestartGame()
{
    // timeout is used in case something goes wrong during game restart
    Stopwatch timeout = new Stopwatch();

    while (true)
    {
        LogLine("Game restarting...");

        KillGame();
        AutoIt.Sleep(3000);
        StartGame();
        AutoIt.Sleep(1000);

        timeout.Restart();

        bool is_menu_main_visible = false;

        while (true)
        {
            // switch focus and inputs to game window
            AutoIt.ForceForegroundWindow(Tools.HWND, 1);
            AutoIt.Sleep(500);
            AutoIt.MouseClick("left", TestPositions.START_COORDS[0], TestPositions.START_COORDS[1]);
            AutoIt.Sleep(500);

            is_menu_main_visible = Tools.IsMainMenu();

            if (is_menu_main_visible || timeout.ElapsedMilliseconds > 2 * 60 * 1000)
            {
                break;
            }
        }

        if (is_menu_main_visible)
        {
            // restart is considered successful when we are in main menu
            break;
        }
    }

    LogLine("Game ready!");
}

A few functions for acquiring game state.

public static bool IsSteamGroupAvailable()
{
    int x = 0;
    int y = 0;
    // bitmap contains "..." group name in steam overlay showing up after clicking Invite
    return AutoIt.ImageSearchArea("steam_group_check.bmp", 0, TestPositions.STEAM_GROUP_RECT[0], TestPositions.STEAM_GROUP_RECT[1], TestPositions.STEAM_GROUP_RECT[2], TestPositions.STEAM_GROUP_RECT[3], ref x, ref y, 5);
}

public static bool IsPrivateMatch()
{
    Color p = AutoIt.PixelGetColor(TestPositions.PRIVATE_MATCH_CHECK_COORDS[0], TestPositions.PRIVATE_MATCH_CHECK_COORDS[1]);
    return p.R > 100 && p.R < 130 && p.G > 100 && p.G < 130 && p.B > 80 && p.B < 110;
}

public static bool IsMainMenu()
{
    Color p = AutoIt.PixelGetColor(TestPositions.MENU_MAIN_CHECK_COORDS[0], TestPositions.MENU_MAIN_CHECK_COORDS[1]);
    return p.R > 110 && p.R < 120 && p.G > 20 && p.G < 30 && p.B > 10 && p.B < 30;
}

public static bool IsAnotherPlayerInLobby()
{
    Color p = AutoIt.PixelGetColor(TestPositions.PLAYER_PRESENT_CHECK_COORDS[0], TestPositions.PLAYER_PRESENT_CHECK_COORDS[1]);
    return (p.R > 75 && p.G > 75 && p.B > 72 && p.R < 85 && p.G < 85 && p.B < 83) ||
            (p.R > 175 && p.G > 175 && p.B > 170 && p.R < 185 && p.G < 185 && p.B < 180);
}

public static bool IsInvitePending()
{
    Color p = AutoIt.PixelGetColor(TestPositions.PENDING_INVITE_CHECK_COORDS[0], TestPositions.PENDING_INVITE_CHECK_COORDS[1]);
    return p.R > 160 && p.G > 157 && p.B > 150 && p.R < 166 && p.G < 163 && p.B < 156;
}

public static bool IsSteamOverlayOpen()
{
    Color p = AutoIt.PixelGetColor(TestPositions.STEAM_OPEN_CHECK[0], TestPositions.STEAM_OPEN_CHECK[1]);
    return p.R > 225 && p.G > 225 && p.B > 225 && p.R < 235 && p.G < 235 && p.B < 235;
}

When checking if steam group is available I did a little trick and tagged my botting friend with "..." so he shows up in the uppermost group. That way when it comes to sending invites I'm not looking for his avatar or name on the screen, instead I invite first person from "..." group. If none is online I will just wait and try again in a couple of seconds, so I don't spam other friends with game invites.

We will need some functions for manipulating steam overlay and accepting invites.

public static void ToggleSteamOverlay()
{
    // I have remapped Steam overlay hotkey from Shift+Tab to NumLock for convenience
    AutoIt.Send(Input.VirtualKeyCode.NUMLOCK);
}

public static void OpenSteamOverlay()
{
    if (!IsSteamOverlayOpen())
        ToggleSteamOverlay();
}

public static void CloseSteamOverlay()
{
    if (IsSteamOverlayOpen())
        ToggleSteamOverlay();
}

public static void AcceptInvite()
{
    Tools.OpenSteamOverlay();
    AutoIt.Sleep(1000);

    if (Tools.IsInvitePending())
    {
        AutoIt.MouseClick("left", TestPositions.STEAM_ACCEPT_INVITE_COORDS[0], TestPositions.STEAM_ACCEPT_INVITE_COORDS[1]); // click accept invite
        AutoIt.Sleep(2000); // give Steam some time to process invite
        return;
    }

    AutoIt.Sleep(1000);
    Tools.CloseSteamOverlay();
    AutoIt.Sleep(1000);
}

Let's get into host and guest logic.

static void RunHost()
{
    while (!Tools.IsPrivateMatch())
    {
        AutoIt.MouseClick("left", TestPositions.PRIVATE_MATCH_COORDS[0], TestPositions.PRIVATE_MATCH_COORDS[1]); // Private match (for entering after restart)
        AutoIt.Sleep(500);
    }

    AutoIt.MouseClick("left", TestPositions.JASON_MASK_COORDS[0], TestPositions.JASON_MASK_COORDS[1]); // Jason mask
    AutoIt.Sleep(1000);

    while (!Tools.IsAnotherPlayerInLobby())
    {
        AutoIt.MouseClick("left", TestPositions.INVITE_COORDS[0], TestPositions.INVITE_COORDS[1]); // Invite button
        AutoIt.Sleep(1000);

        bool is_group_available = Tools.IsSteamGroupAvailable();

        if (is_group_available)
        {
            AutoIt.MouseClick("left", TestPositions.STEAM_INVITE_COORDS[0], TestPositions.STEAM_INVITE_COORDS[1]); // invite on steam friends list
        }

        AutoIt.Sleep(1000);
        Tools.CloseSteamOverlay();

        if (is_group_available)
        {
            Stopwatch timeout = new Stopwatch();
            timeout.Start();

            // send another invite after 15 seconds
            while (!Tools.IsAnotherPlayerInLobby() && timeout.ElapsedMilliseconds < 15000)
            {
                AutoIt.Sleep(500);
            }
        }
        else
        {
            AutoIt.Sleep(3000);
        }
    }

    // make "sure" that there will be no restart before clicking READY
    RestartTimer.Restart();

    AutoIt.MouseClick("left", TestPositions.READY_COORDS[0], TestPositions.READY_COORDS[1]); // READY

    // wait for loading
    while (Tools.IsPrivateMatch())
    {
        AutoIt.Sleep(200);
    }

    // this is synchronization point for both bots
    MatchTimer.Start();
}
static void RunGuest()
{
    while (!Tools.IsPrivateMatch())
    {
        Tools.AcceptInvite();
    }

    // make "sure" that there will be no restart before clicking READY
    RestartTimer.Restart();

    AutoIt.MouseClick("left", TestPositions.READY_COORDS[0], TestPositions.READY_COORDS[1]); // READY

    // wait for loading
    while (Tools.IsPrivateMatch())
    {
        AutoIt.Sleep(200);
    }
            
    // this is synchronization point for both bots
    MatchTimer.Start();

    AutoIt.Sleep(30000); // wait till map is loaded and intro is playing

    AutoIt.Send(Input.VirtualKeyCode.ESCAPE);
    AutoIt.Sleep(1000);
    AutoIt.MouseClick("left", TestPositions.LEAVE_MATCH_COORDS[0], TestPositions.LEAVE_MATCH_COORDS[1]); // click Leave Match
    AutoIt.Sleep(1000);
    AutoIt.MouseClick("left", TestPositions.CONFIRM_COORDS[0], TestPositions.CONFIRM_COORDS[1]); // click Confirm
}

Cool thing is that both host and guest bots MatchTimer gets kind of synchronized because they exit private match lobby and starts loading map at the same time.

As you noticed I use whole bunch of test positions. Those will be different for different resolutions. Here are the sample values:

//1680x1050
public static int[] PRIVATE_MATCH_COORDS = { 220, 374 }; // center of Private Match button in main menu
public static int[] PRIVATE_MATCH_CHECK_COORDS = { 92, 113 }; // upper left corner of "P" letter
public static int[] MENU_MAIN_CHECK_COORDS = { 186, 854 }; // point on red line between player profile name and XP bar
public static int[] PLAYER_PRESENT_CHECK_COORDS = { 106, 308 }; // upper left corner of player ready tick box
public static int[] PENDING_INVITE_CHECK_COORDS = { 1271, 129 }; // gray area of invite in chat window
public static int[] JASON_MASK_COORDS = { 698, 230 }; // center of jason mask
public static int[] INVITE_COORDS = { 545, 927 }; // invite button in private match menu
public static int[] STEAM_INVITE_COORDS = { 970, 226 }; // invite button in steam overlay
public static int[] READY_COORDS = { 1170, 524 }; // READY button in private match menu
public static int[] LEAVE_MATCH_COORDS = { 200, 515 }; // leave match button in in-game menu
public static int[] CONFIRM_COORDS = { 210, 650 }; // confirm leave button
public static int[] STEAM_ACCEPT_INVITE_COORDS = { 1157, 127 }; // invite accept "button" in chat area in steam overlay
public static int[] STEAM_GROUP_RECT = { 674, 203, 688, 209 }; // area for image search for "..." group
public static int[] STEAM_OPEN_CHECK = { 414, 996 }; // pixel on 't' letter in "Steam" word on the bottom of the overlay
public static int[] START_COORDS = { 840, 700 }; // anywhere near initial "press to start" message

Last missing part is handling game restarts. As I mentioned before we will restart the game after a minute after map loading started, however we need additional safety measure just in case something goes fubar somewhere in-between. My solution to all these problems is a separate "restarter" thread. This definitely is not the safest multi threading code out there but it gets the job done.

public static Stopwatch MatchTimer = new Stopwatch();
public static Stopwatch RestartTimer = new Stopwatch();

public static int MATCH_TIMEOUT = 1; // minutes
public static int RESTART_TIMEOUT = 5; // minutes
public static Thread BOT_THREAD = null;

static void RunRestarter()
{
    while (true)
    {
        AutoIt.Sleep(100);

        bool restart = false;
        bool switch_roles = false;

        // restart timer will not be running when starting bot so it will start the game
        if (!RestartTimer.IsRunning || RestartTimer.ElapsedMilliseconds > RESTART_TIMEOUT * 60 * 1000)
        {
            restart = true;
        }

        if (MatchTimer.ElapsedMilliseconds > MATCH_TIMEOUT * 60 * 1000)
        {
            restart = true;
            switch_roles = true;
        }

        if (restart)
        {
            if (BOT_THREAD != null)
            {
                BOT_THREAD.Abort();
            }

            Tools.RestartGame();

            // at this point we are sure that game has been properly started
            RestartTimer.Restart();
            MatchTimer.Reset();

            if (switch_roles)
            {
                if (MODE == EBotMode.Guest)
                {
                    MODE = EBotMode.Host;
                }
                else if (MODE == EBotMode.Host)
                {
                    MODE = EBotMode.Guest;
                }
            }

            if (MODE == EBotMode.Guest)
            {
                BOT_THREAD = new Thread(RunGuest);
            }
            else if (MODE == EBotMode.Host)
            {
                BOT_THREAD = new Thread(RunHost);
            }

            BOT_THREAD.Start();
        }
    }
}

All we need to do to get it running is start RunRestarter thread, it will handle everything. Simple as that! That would be about it, now let's harvest some XP. glhf!

Nothing makes a botter happier than double XP weekends.