// AFKinator - keeps Roblox from kicking you for being idle. // Every few minutes it flips to the Roblox window for a split second, taps a key, // and flips back to whatever you were doing. // Build with build.ps1 (uses the C# compiler that ships with Windows). using System; using System.Diagnostics; using System.Drawing; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Windows.Forms; [assembly: System.Reflection.AssemblyTitle("AFKinator")] [assembly: System.Reflection.AssemblyProduct("AFKinator")] [assembly: System.Reflection.AssemblyDescription("Prevents the Roblox 20-minute idle kick")] [assembly: System.Reflection.AssemblyVersion("1.0.0.0")] [assembly: System.Reflection.AssemblyFileVersion("1.0.0.0")] namespace AFKinator { static class Native { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd); [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h); [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr h); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, IntPtr pid); [DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool attach); [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId(); [DllImport("user32.dll")] public static extern uint SendInput(uint n, INPUT[] inputs, int size); [DllImport("user32.dll")] public static extern bool GetLastInputInfo(ref LASTINPUTINFO lii); [DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint flags); [DllImport("user32.dll")] public static extern bool RegisterHotKey(IntPtr h, int id, uint mods, uint vk); [DllImport("user32.dll")] public static extern bool UnregisterHotKey(IntPtr h, int id); [DllImport("user32.dll")] public static extern bool SetProcessDPIAware(); [StructLayout(LayoutKind.Sequential)] public struct LASTINPUTINFO { public uint cbSize; public uint dwTime; } [StructLayout(LayoutKind.Sequential)] public struct MOUSEINPUT { public int dx, dy; public uint mouseData, dwFlags, time; public IntPtr extra; } [StructLayout(LayoutKind.Sequential)] public struct KEYBDINPUT { public ushort wVk, wScan; public uint dwFlags, time; public IntPtr extra; } [StructLayout(LayoutKind.Explicit)] public struct InputUnion { [FieldOffset(0)] public MOUSEINPUT mi; [FieldOffset(0)] public KEYBDINPUT ki; } [StructLayout(LayoutKind.Sequential)] public struct INPUT { public uint type; public InputUnion u; } public static uint UserIdleMs() { LASTINPUTINFO lii = new LASTINPUTINFO(); lii.cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO)); GetLastInputInfo(ref lii); return unchecked((uint)Environment.TickCount - lii.dwTime); } public static void KeepAwake(bool on) { // ES_CONTINUOUS | ES_SYSTEM_REQUIRED, or just ES_CONTINUOUS to clear SetThreadExecutionState(on ? 0x80000001u : 0x80000000u); } static bool Focus(IntPtr h) { if (h == IntPtr.Zero || !IsWindow(h)) return false; IntPtr fg = GetForegroundWindow(); uint fgThread = GetWindowThreadProcessId(fg, IntPtr.Zero); uint me = GetCurrentThreadId(); bool attached = fgThread != 0 && fgThread != me && AttachThreadInput(me, fgThread, true); BringWindowToTop(h); bool ok = SetForegroundWindow(h); if (attached) AttachThreadInput(me, fgThread, false); return ok; } static void Key(ushort scan, bool up) { INPUT[] inp = new INPUT[1]; inp[0].type = 1; // keyboard inp[0].u.ki.wScan = scan; inp[0].u.ki.dwFlags = 0x8u | (up ? 0x2u : 0u); // SCANCODE (| KEYUP) SendInput(1, inp, Marshal.SizeOf(typeof(INPUT))); } // Briefly focus the Roblox window, tap a key, then give focus back. public static bool Nudge(IntPtr roblox, ushort scan) { IntPtr previous = GetForegroundWindow(); bool wasMinimized = IsIconic(roblox); if (wasMinimized) { ShowWindow(roblox, 9 /*RESTORE*/); Thread.Sleep(300); } if (!Focus(roblox)) { Thread.Sleep(100); Focus(roblox); } Thread.Sleep(150); bool focused = GetForegroundWindow() == roblox; if (focused) { Key(scan, false); Thread.Sleep(90); Key(scan, true); Thread.Sleep(100); } if (wasMinimized) ShowWindow(roblox, 6 /*MINIMIZE*/); if (previous != roblox) Focus(previous); return focused; } } class KeyChoice { public string Name; public string Label; public ushort Scan; public KeyChoice(string name, string label, ushort scan) { Name = name; Label = label; Scan = scan; } public override string ToString() { return Label; } } class MainForm : Form { const int HOTKEY_ID = 1; const int WM_HOTKEY = 0x0312; static readonly KeyChoice[] KeyChoices = { new KeyChoice("Space", "Space (jump)", 0x39), new KeyChoice("W", "W (forward)", 0x11), new KeyChoice("A", "A (left)", 0x1E), new KeyChoice("S", "S (back)", 0x1F), new KeyChoice("D", "D (right)", 0x20), new KeyChoice("E", "E", 0x12), new KeyChoice("Q", "Q", 0x10), }; NumericUpDown intervalBox; ComboBox keyBox; Button hotkeyButton, startButton, stopButton; Label statusLabel, lastLabel; CheckBox beepBox; System.Windows.Forms.Timer timer; Keys hotkey = Keys.F8; bool capturingHotkey; bool running; DateTime nextNudge; string settingsPath; public MainForm() { Text = "AFKinator"; Font = new Font("Segoe UI", 9F); AutoScaleMode = AutoScaleMode.Font; FormBorderStyle = FormBorderStyle.FixedSingle; MaximizeBox = false; StartPosition = FormStartPosition.CenterScreen; AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; KeyPreview = true; try { Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); } catch { } var grid = new TableLayoutPanel(); grid.AutoSize = true; grid.ColumnCount = 2; grid.Padding = new Padding(12); grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); Controls.Add(grid); // Row: interval intervalBox = new NumericUpDown(); intervalBox.Minimum = 1; intervalBox.Maximum = 19; intervalBox.Value = 10; intervalBox.Width = 55; var minutes = new Label(); minutes.Text = "minutes"; minutes.AutoSize = true; minutes.Margin = new Padding(3, 6, 3, 3); var intervalRow = new FlowLayoutPanel(); intervalRow.AutoSize = true; intervalRow.Margin = new Padding(0); intervalRow.Controls.Add(intervalBox); intervalRow.Controls.Add(minutes); AddRow(grid, "Press a key every:", intervalRow); // Row: key keyBox = new ComboBox(); keyBox.DropDownStyle = ComboBoxStyle.DropDownList; keyBox.Width = 140; keyBox.Items.AddRange(KeyChoices); keyBox.SelectedIndex = 0; AddRow(grid, "Key to press in Roblox:", keyBox); // Row: hotkey hotkeyButton = new Button(); hotkeyButton.Width = 140; hotkeyButton.Click += delegate { BeginHotkeyCapture(); }; AddRow(grid, "Start/stop hotkey:", hotkeyButton); new ToolTip().SetToolTip(hotkeyButton, "Click, then press the key or key combo you want (Esc to cancel)"); // Row: start / stop startButton = new Button(); startButton.Width = 120; startButton.Height = 34; stopButton = new Button(); stopButton.Width = 120; stopButton.Height = 34; startButton.Click += delegate { SetRunning(true); }; stopButton.Click += delegate { SetRunning(false); }; var buttons = new FlowLayoutPanel(); buttons.AutoSize = true; buttons.Margin = new Padding(0, 10, 0, 4); buttons.Controls.Add(startButton); buttons.Controls.Add(stopButton); grid.Controls.Add(buttons); grid.SetColumnSpan(buttons, 2); // Status statusLabel = new Label(); statusLabel.AutoSize = true; statusLabel.Font = new Font("Segoe UI", 10F, FontStyle.Bold); statusLabel.Margin = new Padding(3, 4, 3, 2); grid.Controls.Add(statusLabel); grid.SetColumnSpan(statusLabel, 2); lastLabel = new Label(); lastLabel.AutoSize = true; lastLabel.ForeColor = Color.DimGray; lastLabel.Text = "No key presses sent yet."; grid.Controls.Add(lastLabel); grid.SetColumnSpan(lastLabel, 2); beepBox = new CheckBox(); beepBox.Text = "Beep when starting/stopping"; beepBox.AutoSize = true; beepBox.Checked = true; beepBox.Margin = new Padding(3, 8, 3, 0); grid.Controls.Add(beepBox); grid.SetColumnSpan(beepBox, 2); LoadSettings(); intervalBox.ValueChanged += delegate { OnIntervalChanged(); SaveSettings(); }; keyBox.SelectedIndexChanged += delegate { SaveSettings(); }; beepBox.CheckedChanged += delegate { SaveSettings(); }; timer = new System.Windows.Forms.Timer(); timer.Interval = 250; timer.Tick += delegate { OnTick(); }; timer.Start(); UpdateUi(); } static void AddRow(TableLayoutPanel grid, string text, Control control) { var label = new Label(); label.Text = text; label.AutoSize = true; label.Margin = new Padding(3, 7, 10, 3); grid.Controls.Add(label); grid.Controls.Add(control); } // ---------- Hotkey ---------- static string HotkeyText(Keys k) { string s = ""; if ((k & Keys.Control) != 0) s += "Ctrl+"; if ((k & Keys.Shift) != 0) s += "Shift+"; if ((k & Keys.Alt) != 0) s += "Alt+"; Keys code = k & Keys.KeyCode; string name = code.ToString(); if (code >= Keys.D0 && code <= Keys.D9) name = ((int)(code - Keys.D0)).ToString(); return s + name; } bool RegisterCurrentHotkey() { uint mods = 0x4000; // MOD_NOREPEAT if ((hotkey & Keys.Alt) != 0) mods |= 1; if ((hotkey & Keys.Control) != 0) mods |= 2; if ((hotkey & Keys.Shift) != 0) mods |= 4; return Native.RegisterHotKey(Handle, HOTKEY_ID, mods, (uint)(hotkey & Keys.KeyCode)); } protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); if (!RegisterCurrentHotkey()) MessageBox.Show(this, "The hotkey " + HotkeyText(hotkey) + " is already used by another program.\nClick the hotkey button to pick a different one.", "AFKinator", MessageBoxButtons.OK, MessageBoxIcon.Warning); } protected override void WndProc(ref Message m) { if (m.Msg == WM_HOTKEY && m.WParam.ToInt32() == HOTKEY_ID) SetRunning(!running); base.WndProc(ref m); } void BeginHotkeyCapture() { capturingHotkey = true; Native.UnregisterHotKey(Handle, HOTKEY_ID); hotkeyButton.Text = "Press a key... (Esc)"; } protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { if (!capturingHotkey) return base.ProcessCmdKey(ref msg, keyData); Keys code = keyData & Keys.KeyCode; if (code == Keys.ControlKey || code == Keys.ShiftKey || code == Keys.Menu || code == Keys.LWin || code == Keys.RWin) return true; // wait for a real key Keys old = hotkey; if (code != Keys.Escape) hotkey = keyData; capturingHotkey = false; if (!RegisterCurrentHotkey()) { MessageBox.Show(this, HotkeyText(hotkey) + " is already used by another program (or Windows). Try a different key.", "AFKinator", MessageBoxButtons.OK, MessageBoxIcon.Warning); hotkey = old; RegisterCurrentHotkey(); } SaveSettings(); UpdateUi(); ActiveControl = startButton; return true; } // ---------- Running ---------- void SetRunning(bool on) { if (on == running) return; running = on; Native.KeepAwake(on); if (on) nextNudge = DateTime.Now.AddSeconds(3); // first press shortly after starting if (beepBox.Checked) ThreadPool.QueueUserWorkItem(delegate { Console.Beep(on ? 1200 : 500, on ? 150 : 250); }); UpdateUi(); } void OnIntervalChanged() { DateTime latest = DateTime.Now.AddMinutes((double)intervalBox.Value); if (running && nextNudge > latest) nextNudge = latest; } Process[] RobloxProcesses() { var list = new System.Collections.Generic.List(); foreach (Process p in Process.GetProcessesByName("RobloxPlayerBeta")) if (p.MainWindowHandle != IntPtr.Zero) list.Add(p); return list.ToArray(); } string waitingNote; void OnTick() { waitingNote = null; if (running && DateTime.Now >= nextNudge) { Process[] roblox = RobloxProcesses(); if (roblox.Length == 0) { waitingNote = "Roblox isn't open - waiting for it..."; nextNudge = DateTime.Now.AddSeconds(15); lastLabel.Text = "Roblox not found at " + DateTime.Now.ToString("h:mm tt") + ". Will keep checking."; } else if (Native.UserIdleMs() < 1500 && (DateTime.Now - nextNudge).TotalSeconds < 60) { // You're typing/moving the mouse - wait for a pause so we don't steal keystrokes. waitingNote = "Waiting for you to pause typing..."; } else { ushort scan = ((KeyChoice)keyBox.SelectedItem).Scan; int ok = 0; foreach (Process p in roblox) if (Native.Nudge(p.MainWindowHandle, scan)) ok++; lastLabel.Text = ok > 0 ? "Last key press: " + DateTime.Now.ToString("h:mm:ss tt") + (roblox.Length > 1 ? " (" + ok + " Roblox windows)" : "") : "Couldn't switch to Roblox at " + DateTime.Now.ToString("h:mm tt") + ". Will retry."; nextNudge = ok > 0 ? DateTime.Now.AddMinutes((double)intervalBox.Value) : DateTime.Now.AddSeconds(30); } } UpdateUi(); } void UpdateUi() { string hk = HotkeyText(hotkey); if (!capturingHotkey) hotkeyButton.Text = hk; startButton.Text = "Start (" + hk + ")"; stopButton.Text = "Stop (" + hk + ")"; startButton.Enabled = !running; stopButton.Enabled = running; if (!running) { statusLabel.Text = "Stopped"; statusLabel.ForeColor = Color.Firebrick; } else { statusLabel.ForeColor = Color.ForestGreen; if (waitingNote != null) statusLabel.Text = "Running - " + waitingNote; else { TimeSpan left = nextNudge - DateTime.Now; if (left < TimeSpan.Zero) left = TimeSpan.Zero; statusLabel.Text = string.Format("Running - next key press in {0}:{1:00}", (int)left.TotalMinutes, left.Seconds); } } } // ---------- Settings (saved in %APPDATA%\AFKinator\settings.ini) ---------- void LoadSettings() { settingsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AFKinator", "settings.ini"); try { if (!File.Exists(settingsPath)) return; foreach (string line in File.ReadAllLines(settingsPath)) { int eq = line.IndexOf('='); if (eq < 0) continue; string k = line.Substring(0, eq).Trim(), v = line.Substring(eq + 1).Trim(); int n; if (k == "interval" && int.TryParse(v, out n) && n >= 1 && n <= 19) intervalBox.Value = n; else if (k == "key") { for (int i = 0; i < KeyChoices.Length; i++) if (KeyChoices[i].Name == v) keyBox.SelectedIndex = i; } else if (k == "hotkey" && int.TryParse(v, out n) && n != 0) hotkey = (Keys)n; else if (k == "beep") beepBox.Checked = v != "0"; } } catch { } if (IsHandleCreated) { Native.UnregisterHotKey(Handle, HOTKEY_ID); RegisterCurrentHotkey(); } } void SaveSettings() { try { Directory.CreateDirectory(Path.GetDirectoryName(settingsPath)); File.WriteAllLines(settingsPath, new string[] { "interval=" + intervalBox.Value, "key=" + ((KeyChoice)keyBox.SelectedItem).Name, "hotkey=" + (int)hotkey, "beep=" + (beepBox.Checked ? "1" : "0"), }); } catch { } } protected override void OnFormClosing(FormClosingEventArgs e) { Native.UnregisterHotKey(Handle, HOTKEY_ID); Native.KeepAwake(false); SaveSettings(); base.OnFormClosing(e); } } static class Program { [STAThread] static void Main() { bool isNew; using (var mutex = new Mutex(true, "AFKinator_SingleInstance", out isNew)) { if (!isNew) { MessageBox.Show("AFKinator is already running.", "AFKinator", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } try { Native.SetProcessDPIAware(); } catch { } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); } } } }