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!