using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; namespace IdleMafiaBot { static class LinuxMain { public static void Run(string[] args) { CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; Regex.CacheSize = 512; 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); string said = null; bot.StateChanged += st => { if (st == said) return; said = 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"); bot.Once = once; bot.StartWatching(); string commands = Path.Combine(dir, "IdleMafiaBot.cmd"); Opened(dir); WatchSignals(bot, dir, commands); WireRemote(bot, settings, dir); LinuxPage.Wire(bot, settings, dir, on => Hold(dir, on)); string gone = Path.Combine(dir, "stats-removed"); if (File.Exists(gone)) { try { var olds = File.ReadAllLines(gone).Select(l => l.Trim()).Where(l => l.Length > 0).ToList(); File.Delete(gone); 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(); } } catch (Exception) { } } bot.Stats.Begin(); bot.Remote.Begin(); 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) { string why; 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); settings.Save(ini); Console.WriteLine("Preset \"" + p.Name + "\" is in the settings" + (skipped > 0 ? " (" + skipped + " values left as they were)" : "")); return true; } 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; bool? said = null; while (true) { if (bot.Running != said) { said = bot.Running; try { File.WriteAllText(state, said == true ? "playing" : "stopped"); } catch (Exception) { } } Thread.Sleep(1000); 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; endWhenStopped = false; } string changes = Path.Combine(dir, "IdleMafiaBot.set"); if (File.Exists(changes)) TakeSettings(bot, settings, dir, changes); LinuxPage.Turn(bot, settings, dir); string cmd = null; try { if (File.Exists(commands)) { cmd = File.ReadAllText(commands).Trim().ToLowerInvariant(); File.Delete(commands); } } catch (IOException) { } 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"); return; } } } static void TakeSettings(Bot bot, Settings s, string dir, string file) { string[] lines; try { lines = File.ReadAllLines(file); } catch (Exception) { return; } string oldStats = (s.StatsLink ?? "").Trim(), oldLink = s.DiscordLink ?? ""; bool oldRemote = s.RemoteControl; var unread = new List(); foreach (var line in lines) { int eq = line.IndexOf('='); if (eq <= 0) continue; 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); bot.SaveSettings(); 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(); } }; 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 reply) { switch (what) { case "start": 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; } Hold(dir, false); bot.Start(); reply(true, "Started. It plays now."); return; case "stop": if (!bot.Running) { reply(true, "It's already stopped."); return; } Hold(dir, true); bot.Stop(); reply(true, "Stopped."); return; case "status": { 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 + "."); return; } case "preset": { var presets = Preset.Load(dir); var p = presets.FirstOrDefault(x => Preset.SameName(x.Name, arg ?? "")); if (p == null) { 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).")); return; } if (p.Matches(s, true)) { reply(true, "\"" + p.Name + "\" is already on."); return; } int skipped = p.ApplyTo(s); s.PresetInUse = p.Name; bot.SaveSettings(); bot.Log("Preset: using " + p.Name + (skipped > 0 ? " (" + skipped + " settings left as they were)" : "")); reply(true, "Using \"" + p.Name + "\" now."); return; } case "report": MakeReport(bot, dir, reply); return; case "addpreset": { string why; 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.Remote.Wake(); 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." : "")); return; } case "shared": { bool ok = arg.StartsWith("ok:", StringComparison.Ordinal); string text = arg.Length > 3 ? arg.Substring(3) : arg; bot.Log("Preset share: " + text); reply(ok, text); return; } } reply(false, "The bot doesn't know that command."); } static void Hold(string dir, bool on) { try { 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); } catch (Exception) { } } static bool? quietWas; static void Quiet(string dir, bool on) { if (quietWas == on) return; try { string quiet = Path.Combine(dir, "quiet"); if (on) File.WriteAllText(quiet, ""); else if (File.Exists(quiet)) File.Delete(quiet); quietWas = on; } catch (Exception) { } } static bool exitAfter; static int closing; static void Close(Bot bot, string dir, string by, bool keepRoll, int waitSeconds, bool tell) { bot.Log("Closed by " + by); bool wasRunning = bot.Running; bot.Stop(keepRoll); 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); bot.Stats.Closing(by); 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) { Hold(dir, true); if (!exitAfter) { bot.Once = false; bot.Log("The run is over: the bot stays open and stopped (Start on the Tasks page plays)"); return false; } if (Interlocked.Exchange(ref closing, 1) != 0) Thread.Sleep(Timeout.Infinite); Close(bot, dir, "--exit (the run is over)", false, 0, true); return true; } static void WatchSignals(Bot bot, string dir, string commands) { WaitHandle term; try { 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")); } catch (Exception e) { 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"); return; } new Thread(() => { bool got = false; try { got = term.WaitOne(); } catch (Exception) { } 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(); bool keepRoll, tell; string by = SentBy(dir, commands, out keepRoll, out tell); try { Close(bot, dir, by, keepRoll, 5, tell); } catch (Exception) { } try { File.Delete(Path.Combine(dir, "IdleMafiaBot.state")); } catch (Exception) { } Environment.Exit(0); } static string SentBy(string dir, string commands, out bool keepRoll, out bool tell) { string cmd = ""; try { if (File.Exists(commands)) cmd = File.ReadAllText(commands).Trim().ToLowerInvariant(); } catch (Exception) { } keepRoll = true; tell = false; 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)"; tell = true; return "Linux (shutting down, restarting, signing out, or another program)"; } static void EndNow() { 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)); } catch (Exception) { } } 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 reply) { string desktop = ""; try { desktop = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) ?? ""; } catch (Exception) { } bool onDesktop = desktop.Length > 0 && Directory.Exists(desktop); var before = DateTime.Now; string zip; int moved; try { zip = ProblemReport.Write(dir, onDesktop ? desktop : dir, ProblemReport.RobloxWindowInfo(bot.Pinned)); moved = ProblemReport.MoveReported(dir, before); } catch (Exception e) { bot.Log("Couldn't make the report: " + e.Message); if (reply != null) reply(false, "Couldn't make the report: " + e.Message); return; } 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; } } }