using System.Collections.Generic;using System.Globalization;using System.Text.RegularExpressions; public static void Run(string[] args) CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; string dir = AppDomain.CurrentDomain.BaseDirectory; using (var one = OneOnly(dir)) if (one == null) { Console.WriteLine("The bot is already running here."); return; } string ini = Path.Combine(dir, "IdleMafiaBot.ini"); var settings = Settings.Load(ini); try { settings.Save(ini); } catch (Exception) { } if (args.Contains("--ini")) return; var preset = args.FirstOrDefault(a => a.StartsWith("--preset=", StringComparison.Ordinal)); if (preset != null && !LoadPreset(preset.Substring(9), settings, ini)) return; string log = Path.Combine(dir, "IdleMafiaBot.log"); var lastLine = File.Exists(log) ? File.GetLastWriteTime(log) : DateTime.MinValue; var bot = new Bot(settings, dir); bot.Logged += Console.WriteLine; var trust = Trust.Sync(); if (trust != null) bot.Log(trust); bot.StateChanged += st => Console.WriteLine(DateTime.Now.ToString("HH:mm:ss") + " [" + st + "]"); AppDomain.CurrentDomain.UnhandledException += (s, e) => var ex = e.ExceptionObject as Exception; bot.LogDetails(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " Idle Mafia Bot stopped because of an error: " + (ex != null ? ex.ToString() : "" + e.ExceptionObject)); SayLastRun(bot, dir, lastLine); RobloxWindow.Note = bot.Log; string problem = Ocr.Check(); if (problem != null) { bot.Log("Problem: " + problem); return; } bool once = args.Contains("--once"), scan = args.Contains("--scan"), check = args.Contains("--check"); exitAfter = args.Contains("--exit"); string commands = Path.Combine(dir, "IdleMafiaBot.cmd"); WatchSignals(bot, dir, commands); WireRemote(bot, settings, dir); LinuxPage.Wire(bot, settings, dir, on => Hold(dir, on)); string gone = Path.Combine(dir, "stats-removed"); var olds = File.ReadAllLines(gone).Select(l => l.Trim()).Where(l => l.Length > 0).ToList(); string wid = (settings.StatsMessage ?? "").Trim().Split('/')[0]; string old = olds.FirstOrDefault(l => wid.Length > 0 && StatsCard.WebhookOf(l) == wid) ?? olds.FirstOrDefault(); if (old != null) { bot.Stats.Removed(old); bot.SaveSettings(); } LinuxPages.Watch(bot, settings, dir); if (scan) bot.RequestScan(); else if (check) bot.RequestCheck(); else if (!args.Contains("--stopped")) bot.Start(); Loop(bot, settings, dir, commands, once || scan || check || exitAfter); static FileStream OneOnly(string dir) try { return new FileStream(Path.Combine(dir, ".running"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); } catch (IOException) { return null; } static bool LoadPreset(string file, Settings settings, string ini) var p = Preset.Parse(File.ReadAllText(file), out why); if (p == null) { Console.WriteLine("That preset can't be used: " + why); return false; } int skipped = p.ApplyTo(settings); Console.WriteLine("Preset \"" + p.Name + "\" is in the settings" + (skipped > 0 ? " (" + skipped + " values left as they were)" : "")); static void Loop(Bot bot, Settings settings, string dir, string commands, bool endWhenStopped) string state = Path.Combine(dir, "IdleMafiaBot.state"); try { Turns(bot, settings, dir, commands, endWhenStopped, state); } finally { try { File.Delete(state); } catch (Exception) { } } static void Turns(Bot bot, Settings settings, string dir, string commands, bool endWhenStopped, string state) bool seenRunning = false; try { File.WriteAllText(state, said == true ? "playing" : "stopped"); } catch (Exception) { } if (Thread.VolatileRead(ref closing) != 0) Thread.Sleep(Timeout.Infinite); Quiet(dir, !bot.Running || bot.GameLeftToYou); seenRunning |= bot.Running; if (endWhenStopped && seenRunning && !bot.Running) if (OneOffOver(bot, dir)) return; string changes = Path.Combine(dir, "IdleMafiaBot.set"); if (File.Exists(changes)) TakeSettings(bot, settings, dir, changes); LinuxPage.Turn(bot, settings, dir); if (File.Exists(commands)) { cmd = File.ReadAllText(commands).Trim().ToLowerInvariant(); File.Delete(commands); } if (cmd == "start" && !bot.Running) bot.Start(); else if (cmd == "stop" && bot.Running) bot.Stop(); else if (cmd == "report") MakeReport(bot, dir, null); else if (cmd == "exit" || cmd == "exit keep") CloseByYou(bot, dir, cmd == "exit keep"); static void TakeSettings(Bot bot, Settings s, string dir, string file) try { lines = File.ReadAllLines(file); } catch (Exception) { return; } string oldStats = (s.StatsLink ?? "").Trim(), oldLink = s.DiscordLink ?? ""; bool oldRemote = s.RemoteControl; var unread = new List<string>(); foreach (var line in lines) int eq = line.IndexOf('='); var field = typeof(Settings).GetField(line.Substring(0, eq).Trim()); if (field != null && !Settings.SetText(s, field, line.Substring(eq + 1).Trim())) unread.Add(field.Name); string stats = (s.StatsLink ?? "").Trim(); if (stats != oldStats && oldStats.Length > 0) bot.Stats.Removed(oldStats); if ((s.DiscordLink ?? "") != oldLink) Accounts.ForgetLink(dir, s.Account); if (stats != oldStats && stats.Length > 0) bot.Stats.Wake(); if (stats != oldStats || s.RemoteControl != oldRemote) bot.Remote.Wake(); try { File.Delete(file); } catch (Exception) { } bot.Log("Settings saved from the Tasks page" + (unread.Count > 0 ? " (" + string.Join(", ", unread) + " couldn't be read: left as they were)" : "")); static void WireRemote(Bot bot, Settings s, string dir) bot.Remote.PresetNames = () => { try { return Preset.Load(dir).Select(p => p.Name).ToList(); } catch (Exception) { return new List<string>(); } }; bot.Remote.Shot = (out string why) => Discord.ShotOfRoblox(bot.Pinned, out why); bot.Remote.ShareLost = text => bot.Log("Preset share: " + text); bot.Remote.Handler = (what, arg, reply) => try { RemoteDo(bot, s, dir, what, arg, reply); } catch (Exception e) { reply(false, "Something went wrong: " + e.Message); } static void RemoteDo(Bot bot, Settings s, string dir, string what, string arg, Action<bool, string> reply) if (bot.Running) { reply(true, "It's already playing."); return; } if (!new RobloxWindow().Find()) { reply(false, "Roblox isn't open on the PC, so the bot wasn't started. Open Idle Mafia Game there first, then send start again."); return; } reply(true, "Started. It plays now."); if (!bot.Running) { reply(true, "It's already stopped."); return; } string state = !bot.Running ? (new RobloxWindow().Find() ? "Stopped" : "Stopped (Roblox isn't open)") : bot.OutOfGame ? "Running, but the game is out of reach (disconnected or Roblox closed)" : "Playing"; var p = Path.Combine(dir, "problems", "active"); int n = Directory.Exists(p) ? Directory.GetFiles(p, "*.png").Length : 0; var now = Preset.Load(dir).FirstOrDefault(x => x.Matches(s)); string numbers = bot.Stats.NumbersLine(); reply(true, state + "." + (numbers.Length > 0 ? "\n" + char.ToUpper(numbers[0]) + numbers.Substring(1) + "." : "") + "\nPreset: " + (now != null ? now.Name : "none of yours") + "." + "\nProblems: " + (n == 0 ? "none" : n.ToString(CultureInfo.InvariantCulture)) + "." + "\nVersion " + Program.Version + "."); var presets = Preset.Load(dir); var p = presets.FirstOrDefault(x => Preset.SameName(x.Name, arg ?? "")); reply(false, "No preset called \"" + arg + "\"." + (presets.Count > 0 ? " Yours: " + string.Join(", ", presets.Select(x => x.Name)) + "." : " You have none yet: add one from a card in our Discord (Add to my bot).")); if (p.Matches(s, true)) { reply(true, "\"" + p.Name + "\" is already on."); return; } int skipped = p.ApplyTo(s); bot.Log("Preset: using " + p.Name + (skipped > 0 ? " (" + skipped + " settings left as they were)" : "")); reply(true, "Using \"" + p.Name + "\" now."); MakeReport(bot, dir, reply); var p = Preset.Parse(arg, out why); if (p == null) { reply(false, why); return; } var presets = Preset.Load(dir); int old = presets.FindIndex(o => Preset.SameName(o.Name, p.Name)); if (old >= 0) presets[old] = p; else presets.Add(p); try { Preset.Save(dir, presets); } catch (Exception) { reply(false, "The bot couldn't save its presets on the PC."); return; } bot.Log("Preset added from Discord: " + p.Name); reply(true, (old >= 0 ? "Replaced your \"" : "Added \"") + p.Name + "\" in your presets. Use it with /remote." + (p.Newer > 0 ? " " + p.Newer + " of its settings are from a newer bot: update yours to get them all." : "")); bool ok = arg.StartsWith("ok:", StringComparison.Ordinal); string text = arg.Length > 3 ? arg.Substring(3) : arg; bot.Log("Preset share: " + text); reply(false, "The bot doesn't know that command."); static void Hold(string dir, bool on) string up = Path.GetDirectoryName(dir.TrimEnd('/', '\\')); if (up == null || !File.Exists(Path.Combine(up, "keep.sh"))) return; string hold = Path.Combine(up, "hold"); if (on) File.WriteAllText(hold, ""); else if (File.Exists(hold)) File.Delete(hold); static void Quiet(string dir, bool on) if (quietWas == on) return; string quiet = Path.Combine(dir, "quiet"); if (on) File.WriteAllText(quiet, ""); else if (File.Exists(quiet)) File.Delete(quiet); static void Close(Bot bot, string dir, string by, bool keepRoll, int waitSeconds, bool tell) bot.Log("Closed by " + by); bool wasRunning = bot.Running; for (int i = 0; i < waitSeconds * 4 && bot.Running; i++) Thread.Sleep(250); if (bot.Running && waitSeconds > 0) bot.Log("The round didn't end in " + waitSeconds + " s - closing anyway"); try { bot.FoldTotals(); } catch (Exception) { } bool you = by.StartsWith("you", StringComparison.Ordinal) || by.StartsWith("--exit", StringComparison.Ordinal); if (tell && wasRunning && !you) bot.Discord.Problem("closed", "\U0001F6D1 The bot was closed", "By " + by + ".", Discord.ShotOfRoblox, false, 0); bot.Discord.Flush(you ? 2000 : 4000); try { File.Delete(OpenMarker(dir)); } catch (Exception) { } static void CloseByYou(Bot bot, string dir, bool keep) if (Interlocked.Exchange(ref closing, 1) != 0) Thread.Sleep(Timeout.Infinite); Close(bot, dir, "you (bot.sh stop)", keep, 30, false); static bool OneOffOver(Bot bot, string dir) bot.Log("The run is over: the bot stays open and stopped (Start on the Tasks page plays)"); if (Interlocked.Exchange(ref closing, 1) != 0) Thread.Sleep(Timeout.Infinite); Close(bot, dir, "--exit (the run is over)", false, 0, true); static void WatchSignals(Bot bot, string dir, string commands) var posix = System.Reflection.Assembly.Load("Mono.Posix, Version=4.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756"); term = (WaitHandle)Activator.CreateInstance(posix.GetType("Mono.Unix.UnixSignal", true), Enum.Parse(posix.GetType("Mono.Unix.Native.Signum", true), "SIGTERM")); bot.LogDetails(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " Linux's shutdown isn't watched (" + (e.InnerException ?? e).Message + "): a shutdown ends the bot without its closing lines"); try { got = term.WaitOne(); } if (got) Ended(bot, dir, commands); else try { term.Close(); } catch (Exception) { } }) { IsBackground = true, Name = "sigterm" }.Start(); static void Ended(Bot bot, string dir, string commands) if (Interlocked.Exchange(ref closing, 1) != 0) { EndNow(); return; } new Thread(() => { Thread.Sleep(20000); EndNow(); }) { IsBackground = true, Name = "sigterm limit" }.Start(); string by = SentBy(dir, commands, out keepRoll, out tell); try { Close(bot, dir, by, keepRoll, 5, tell); } try { File.Delete(Path.Combine(dir, "IdleMafiaBot.state")); } catch (Exception) { } static string SentBy(string dir, string commands, out bool keepRoll, out bool tell) try { if (File.Exists(commands)) cmd = File.ReadAllText(commands).Trim().ToLowerInvariant(); } if (cmd == "exit" || cmd == "exit keep") { keepRoll = cmd == "exit keep"; return "you (bot.sh stop)"; } string log = Path.Combine(dir, "IdleMafiaBot.log"); if (File.Exists(log) && !File.Exists(Path.Combine(dir, "quiet")) && DateTime.Now - File.GetLastWriteTime(log) > TimeSpan.FromMinutes(115)) return "keep.sh (no log line for 2 hours: it starts the game and the bot again)"; return "Linux (shutting down, restarting, signing out, or another program)"; try { System.Diagnostics.Process.GetCurrentProcess().Kill(); } catch (Exception) { Environment.Exit(1); } static string OpenMarker(string dir) { return Path.Combine(dir, "IdleMafiaBot.open"); } static void Opened(string dir) try { File.WriteAllText(OpenMarker(dir), DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); } static void SayLastRun(Bot bot, string dir, DateTime lastLine) string open = OpenMarker(dir); if (!File.Exists(open)) return; try { File.Delete(open); } catch (Exception) { } bot.Log("The last run ended without closing (the PC shut down or lost power, it was killed, or it stopped on an error)" + (lastLine > DateTime.MinValue ? " - its last log line was at " + lastLine.ToString("dd-MM HH:mm", CultureInfo.InvariantCulture) : "")); static void MakeReport(Bot bot, string dir, Action<bool, string> reply) try { desktop = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) ?? ""; } bool onDesktop = desktop.Length > 0 && Directory.Exists(desktop); var before = DateTime.Now; zip = ProblemReport.Write(dir, onDesktop ? desktop : dir, ProblemReport.RobloxWindowInfo(bot.Pinned)); moved = ProblemReport.MoveReported(dir, before); bot.Log("Couldn't make the report: " + e.Message); if (reply != null) reply(false, "Couldn't make the report: " + e.Message); long size = new FileInfo(zip).Length; string where = onDesktop ? "on your Desktop" : "in the bot's folder"; bot.Log(string.Format(CultureInfo.InvariantCulture, "Report saved {0}: {1} ({2}). {3}{4}", where + " (" + Home(Path.GetDirectoryName(zip)) + ")", Path.GetFileName(zip), size < 1048576 ? Math.Max(1, size / 1024) + " KB" : (size / 1048576.0).ToString("0.0", CultureInfo.InvariantCulture) + " MB", size > 10L * 1024 * 1024 ? "That's too big for Discord, so open an issue on the bot's GitHub page and add it there, with a few words about what went wrong." : "Post it in the Discord with a few words about what went wrong.", moved > 0 ? " Its " + moved + " problem picture" + (moved == 1 ? "" : "s") + " moved to problems/reported." : "")); if (reply != null) bot.Remote.ReportZip = zip; if (reply != null) reply(true, "Report saved " + where + ": " + Path.GetFileName(zip) + ". Post it in #bug-report when you're at your PC."); static string Home(string folder) string home = (Environment.GetEnvironmentVariable("HOME") ?? "").TrimEnd('/'); folder = (folder ?? "").TrimEnd('/'); return home.Length > 1 && (folder == home || folder.StartsWith(home + "/", StringComparison.Ordinal)) ? "~" + folder.Substring(home.Length) : folder;