using System.Collections.Generic;using System.Globalization;using System.Text.RegularExpressions; public int Jobs, Helps, Fights, Wins, Losses, Deposits, OpsCollected, OpsStarted, BossHits, Rewards, Crates; public int TakedownAttacks, StaminaGiven, Bought, Collected, Hired, Rerolls; public int BossesBeaten, Properties; public int HeistsStarted; public Counters Copy() { return (Counters)MemberwiseClone(); } return string.Join(";", typeof(Counters).GetFields().Select(f => f.Name + "=" + Convert.ToString(f.GetValue(this), CultureInfo.InvariantCulture))); public static Counters Parse(string text) foreach (var part in (text ?? "").Split(';')) int eq = part.IndexOf('='); var f = typeof(Counters).GetField(part.Substring(0, eq).Trim()); if (f == null || !double.TryParse(part.Substring(eq + 1), NumberStyles.Float, CultureInfo.InvariantCulture, out v) || v < 0) continue; if (f.FieldType == typeof(int)) f.SetValue(c, (int)Math.Min(int.MaxValue, v)); else if (f.FieldType == typeof(double)) f.SetValue(c, v); public void Add(Counters now, Counters since) foreach (var f in typeof(Counters).GetFields()) if (f.FieldType == typeof(int)) f.SetValue(this, (int)f.GetValue(this) + Math.Max(0, (int)f.GetValue(now) - (int)f.GetValue(since))); else if (f.FieldType == typeof(double)) f.SetValue(this, (double)f.GetValue(this) + Math.Max(0, (double)f.GetValue(now) - (double)f.GetValue(since))); public override string ToString() return string.Format("jobs {0} | heist helps {1} | fights {2} ({3} won, {4} lost) | boss hits {5} | deposits {6} | operations {7} collected, {8} started | rewards {9} | crates {10}", Jobs, Helps, Fights, Wins, Losses, BossHits, Deposits, OpsCollected, OpsStarted, Rewards, Crates) + (Bought > 0 || Collected > 0 ? string.Format(" | bought {0} | collected {1}", Bought, Collected) : "") + (Hired > 0 || Rerolls > 0 ? string.Format(" | crew hires {0} | rerolls {1}", Hired, Rerolls) : "") + (HeistsStarted > 0 ? " | heists started " + HeistsStarted : "") + (TakedownAttacks > 0 || StaminaGiven > 0 ? string.Format(" | Takedown attacks {0} | stamina given {1}", TakedownAttacks, StaminaGiven) : ""); public int Period, Max = -1; int prevTick = -1, value = -1, tick = -1; public bool HasValue { get { return value >= 0; } } public void Note(int v, int max, int t, DateTime now) if (v < 0 || max <= 0) return; if (prevTick > 0 && t > prevTick + 2) Period = Math.Min(900, Math.Max(Period, t)); value = v; Max = max; tick = v >= max ? 0 : t; at = now; public void Forget() { value = -1; prevTick = -1; } public int Guess(DateTime now) if (value < 0) return -1; if (value >= Max || Period <= 0) return value; int next = tick >= 0 ? tick : Period; double s = (now - at).TotalSeconds; int gained = s < next ? 0 : 1 + (int)((s - next) / Period); return Math.Min(Max, value + gained); public readonly Settings S; public readonly Counters Count = new Counters(); public readonly Discord Discord; public readonly StatsCard Stats; public readonly RemoteControl Remote; public List<JobInfo> Catalog; public volatile List<JobInfo> CatalogView; readonly string dir, logFile; static readonly object logLock = new object(); volatile bool scanRequested, checkRequested; volatile bool finishing, keepRoll, stopAgain; bool Halted { get { return HaltedNow(stop, finishing && !stopAgain, Environment.TickCount, finishEnd); } } internal static bool HaltedNow(bool stop, bool finishing, int now, int end) { return stop && !(finishing && end - now > 0); } public event Action<string> Logged; public event Action<string> StateChanged; public event Action<Header> HeaderRead; public event Action<string, string> InfoChanged; public event Action<List<OpSlot>> OpsRead; public event Action CatalogChanged; DateTime nextOps, nextBosses, nextPlaytime, nextContracts, nextCrates, nextHeists, nextFights, nextBank, nextPoints, nextJobs, nextLearn, nextGameStats, lastBank; int level = -1, repeats, frontFails, logWrites; int xpNextLast = -1, xpNextSeen = -1; bool learnedThisRun, waitingForUser, logBroken, catalogBuiltIn; volatile bool levelledStopped; readonly HashSet<string> firstSeen = new HashSet<string>(); void SnapshotOnce(string what) if (firstSeen.Add(what)) game.Snapshot(what, 0, true); public Bot(Settings s, string dir) catalogFile = Path.Combine(dir, "jobs.txt"); logFile = Path.Combine(dir, "IdleMafiaBot.log"); memory = new FightMemory(MemoryFile(s.Account)); catalogBuiltIn = !File.Exists(catalogFile); Catalog = GameData.LoadCatalog(catalogFile); CatalogView = Catalog.Select(j => j.Copy()).ToList(); crewUnequipped = s.CrewGearOff; ownOpenBefore = s.CrewOpenBefore; if (s.ScreenFirst) RobloxWindow.ScreenFirst = true; Discord = new Discord(s, Log); Stats = new StatsCard(this, s, Log, SaveSettings); Remote = new RemoteControl(s, Log, SaveSettings, Stats); public bool Running { get { return thread != null && thread.IsAlive; } } public void Start() { if (Program.Reroller) StartRerollOnly(); else Begin(false); } public void RequestScan() { Begin(true); } public void RequestCheck() { Begin(false, true); } public bool CheckOrScan { get { return scanRequested || checkRequested; } } void Begin(bool scan, bool check = false) finishing = keepRoll = stopAgain = false; thread = new Thread(Run) { IsBackground = true, Name = "bot" }; thread.SetApartmentState(ApartmentState.MTA); public void Stop() { Stop(false); } public void Stop(bool keep) if (keep) keepRoll = true; if (stop) { stopAgain = true; finishing = false; } stop = true; stopWhy = null; lock (partners) others = partners.ToList(); foreach (var p in others) p.Stop(keep); public bool StopPressed { get { return stop; } } if (r != null && r.Running) return true; lock (partners) others = partners.ToList(); return others.Any(p => p.OwnRollRuns); readonly List<Bot> partners = new List<Bot>(); bool Rejoins { get { return S.Rejoin && turns == null; } } static readonly TimeSpan TurnLength = TimeSpan.FromMinutes(3); DateTime turnStarted = DateTime.MaxValue; return turns != null && task != "bank" && DateTime.UtcNow - turnStarted >= TurnLength; void SayTurnUp(List<string> left) if (left.Count == 0) return; Log("Turn: its " + TurnLength.TotalMinutes + " minutes are up - " + string.Join(", ", left) + " wait for " + S.Account + "'s next turn"); public bool InTurns { get { return turns != null; } } public void StartAll(List<Instance> others) foreach (var i in others.Where(i => i.Name != null && i.Handle != pinned)) partners.Add(Partner(i)); made = partners.ToList(); if (made.Count == 0) { Start(); return; } var t = new Turns(new[] { this }.Concat(made)); foreach (var p in made) p.turns = t; Log("Playing " + (made.Count + 1) + " accounts in turns: " + string.Join(", ", new[] { S.Account }.Concat(made.Select(p => p.S.Account))) + " - one round each, the fullest bars first. Rejoining is off while it does."); foreach (var p in made) p.Begin(false); public void StartAllWhenSeen() for (var until = DateTime.UtcNow.AddMinutes(2); !Running && DateTime.UtcNow < until; Thread.Sleep(1000)) var seen = InstancesSeen; if (seen != null && seen.Count(i => i.Name != null) > 1 && seen.Any(i => i.Name != null && i.Handle == pinned)) { StartAll(seen); return; } Log("Turns: the other accounts' Roblox windows weren't read again - " + (S.Account.Length > 0 ? S.Account : "this account") + " plays alone"); }) { IsBackground = true, Name = "turns again" }.Start(); string ini = Accounts.Ini(dir, i.Name); bool copy = !File.Exists(ini) && S.Account.Length > 0; var theirs = new Settings(); theirs.TakeAccountFields(copy ? Accounts.NewFrom(S) : Settings.Load(ini)); var p = new Bot(theirs, dir) { lead = this, who = i.Name, pinned = i.Handle }; p.accountIniAt = IniTime(ini); p.Logged += line => { var h = Logged; if (h != null) h(line); }; p.Log("Account: " + i.Name + " is new here: it starts with " + S.Account + "'s settings and the game's job list."); static DateTime IniTime(string ini) try { return File.Exists(ini) ? File.GetLastWriteTimeUtc(ini) : DateTime.MinValue; } catch (Exception) { return DateTime.MinValue; } void SavedOwn() { if (lead != null) accountIniAt = IniTime(Accounts.Ini(dir, S.Account)); } string ini = Accounts.Ini(dir, S.Account), name = S.Account; if (at != DateTime.MinValue && at != accountIniAt) var theirs = Settings.Load(ini); if (theirs.Unreadable == null) S.TakeAccountFields(theirs); Log("Settings: " + name + "'s own settings were changed - they're used from this turn on"); bool screenFirst = S.ScreenFirst; S.ScreenFirst |= screenFirst; catalogFile = Accounts.Jobs(dir, S.Account); catalogBuiltIn = !File.Exists(catalogFile); Catalog = GameData.LoadCatalog(catalogFile); CatalogView = Catalog.Select(j => j.Copy()).ToList(); var h = Instances.HeaderIn(pinned, S); if (h == null || (h.Energy < 0 && h.Stamina < 0)) h = last; return h == null ? 0 : FullestBar(h); internal static int FullestBar(Header h) int e = h.EnergyMax > 0 && h.Energy >= 0 ? h.Energy * 100 / h.EnergyMax : 0; int s = h.StaminaMax > 0 && h.Stamina >= 0 ? h.Stamina * 100 / h.StaminaMax : 0; return Math.Min(100, Math.Max(e, s)); if (t == null) { Tick(); return; } if (DateTime.UtcNow < idleUntil) { Sleep(500); return; } if (WaitingInTurns()) return; if (!t.Take(this, need, () => stop, 0)) State("Another account is playing - " + (S.Account.Length > 0 ? S.Account : "this one") + " waits for its turn"); if (!t.Take(this, need, () => stop, 15000)) return; turnStarted = DateTime.UtcNow; if (WaitingInTurns()) return; if (game.Win.Find() && !game.Win.Minimized && !game.Win.IsForeground) State("Bringing " + S.Account + "'s Roblox window to the front for its turn"); finally { t.Pass(this); } if (!S.WaitWhileBusy || !game.Win.UserActiveWithin(S.IdleSeconds * 1000)) return false; State("Waiting until you stop using the mouse/keyboard (several accounts in turns)"); public event Action<List<Instance>> InstancesChanged; public List<Instance> InstancesSeen { get; private set; } public IntPtr Pinned { get { return pinned; } } public bool PlayOn(Instance which) if (Running || which == null || which.Name == null) return false; var seen = InstancesSeen; RobloxWindow.Taken = seen == null ? new IntPtr[0] : seen.Where(i => i.Handle != which.Handle).Select(i => i.Handle).ToArray(); if (S.Account.Length == 0) Adopt(which.Name); else if (!Accounts.Same(which.Name, S.Account) && !string.Equals(Profile.Key(which.Name), Profile.Key(S.Account), StringComparison.OrdinalIgnoreCase)) SwitchAccount(which.Name); var kept = KeptHeader(which.Name); if (hr != null) hr(kept); public bool ReadWindow(Instance which) if (Running || which == null) return false; if (which.Name != null) return true; var g = new Game(msg => { }, () => false); g.Win.Pinned = which.Handle; if (!g.Win.Find() || g.Win.Minimized || !g.Win.Front()) { Log("Account: couldn't bring that Roblox window to the front - is it minimized?"); return false; } for (int tries = 0; tries < 3 && head == null; tries++) Thread.Sleep(tries == 0 ? 400 : 800); using (var f = g.Win.Capture()) if (v.Problem != null) continue; var h = g.ReadHeader(f, v); if (h != null && Accounts.Plausible(h.Name)) head = h; finally { g.Win.EndAct(true, false); } if (head == null) { Log("Account: couldn't read the name in that Roblox window - is Idle Mafia Game open in it, with its top bar showing?"); return false; } string name = Accounts.Tidy(head.Name.Trim()); foreach (var i in (InstancesSeen ?? new List<Instance>()).Where(i => i.Handle == which.Handle).Concat(new[] { which })) var changed = InstancesChanged; if (changed != null && InstancesSeen != null) changed(InstancesSeen); readonly Dictionary<string, Header> headers = new Dictionary<string, Header>(StringComparer.OrdinalIgnoreCase); readonly AutoResetEvent lookNow = new AutoResetEvent(false); void KeepHeader(Header h) if (h == null || !Accounts.Plausible(h.Name)) return; lock (headers) headers[Profile.Key(h.Name.Trim())] = h; Header KeptHeader(string name) lock (headers) return headers.TryGetValue(Profile.Key(name.Trim()), out h) ? h : null; void LookForInstances(Game reader) var before = InstancesSeen; var now = Instances.Read(before, S, dir); if (Instances.SameWindows(before, now)) return; foreach (var i in now.Where(i => i.Name != null)) { KeepProfile(i.Name); KeepHeader(i.Head); } if (pinned == IntPtr.Zero || !now.Any(i => i.Handle == pinned)) var own = Instances.Pick(now, S.Account, pinned != IntPtr.Zero); if (own == null || !PlayOn(own)) if (pinned != IntPtr.Zero && !now.Any(i => i.Handle == pinned)) { pinned = IntPtr.Zero; RobloxWindow.Taken = new IntPtr[0]; } else RobloxWindow.Taken = now.Where(i => i.Handle != pinned).Select(i => i.Handle).ToArray(); else { pinned = IntPtr.Zero; RobloxWindow.Taken = new IntPtr[0]; } reader.Win.Pinned = pinned; var h = InstancesChanged; void KeepProfile(string name) if (Directory.Exists(Accounts.Folder(dir, name))) return; Directory.CreateDirectory(Accounts.Folder(dir, name)); Log("Account: " + name + " is open in a Roblox window (accounts\\" + Profile.Key(name) + ")"); catch (Exception e) { Log("Couldn't make " + name + "'s folder: " + e.Message); } public void StartWatching() var watcher = new Thread(() => var reader = new Game(msg => { }, () => false); LookForInstances(reader); string state = !win.Find() ? "not running" : win.Minimized ? "minimized" : RobloxWindow.ScreenFirst && !win.InSight() ? "covered" : null; using (var f = win.Capture()) var v = reader.ViewOf(f); if (v.Problem != null) state = "unreadable"; else { reader.SeedMax(S.BarMax); h = reader.ReadHeader(f, v); } if ((state ?? "found") != seen) bool other = !string.IsNullOrWhiteSpace(h.Name) && OtherAccount(h.Name); if (h.Level > 0 && !other && TakeLevel(h.Level)) { if (level > 0 && h.Level > level) levelledStopped = true; level = h.Level; S.LastLevel = level; } if (!GuessBar(energyRegen, DateTime.UtcNow, ref h.Energy, ref h.EnergyMax) && before != null) { h.Energy = before.Energy; h.EnergyMax = before.EnergyMax; } if (MemoryShort(e)) { SayMemory(); Thread.Sleep(60000); } lookNow.WaitOne(Running ? 5000 : 3000); }) { IsBackground = true, Name = "watch" }; public void Log(string msg) string line = DateTime.Now.ToString("HH:mm:ss") + " " + (w != null ? "[" + w + "] " : "") + msg, problem = null; if (logWrites++ % 200 == 0) var fi = new FileInfo(logFile); if (fi.Exists && fi.Length > 5 * 1024 * 1024) string old = Path.Combine(dir, "IdleMafiaBot.1.log"); try { File.AppendAllText(logFile, DateTime.Now.ToString("yyyy-MM-dd ") + line + Environment.NewLine); } catch (Exception e) { if (!logBroken) problem = DateTime.Now.ToString("HH:mm:ss") + " Can't write the log file: " + e.Message; logBroken = true; } if (h != null) { h(line); if (problem != null) h(problem); } public void LogDetails(string text) try { File.AppendAllText(logFile, text + Environment.NewLine); } void State(string s) { var h = StateChanged; if (h != null) h(s); } void Info(string key, string value) { var h = InfoChanged; if (h != null) h(key, value); } game = new Game(Log, () => Halted) { ProblemsDir = Path.Combine(dir, "problems"), CollectFights = CollectFights }; game.Win.Pinned = pinned; game.Win.NoFallback = turns != null; game.Tag = turns != null ? who : null; idleUntil = DateTime.MinValue; game.UserBack = () => acting && S.WaitWhileBusy && game.Win.UserInputSinceBot(); game.Saved = (reason, file) => Discord.Problem("picture " + reason, "⚠️ Problem: " + reason, file != null ? "The bot carries on. The picture is in its problems folder too." : "The bot carries on. The picture is in its problems folder (not sent: another window was over Roblox).", file != null ? (Func<byte[]>)(() => Discord.JpgOf(file)) : null, true, 180); game.Failed = TaskFailed; DateTime now = DateTime.UtcNow; nextOps = nextBosses = nextPlaytime = nextContracts = nextCrates = nextHeists = nextFights = nextBank = nextPoints = nextJobs = nextLearn = nextGameStats = now; nextBosses = BossesDoneUntil(now); nextEquip = nextShop = nextSafehouse = nextReroll = nextMarket = now; nextUpgrade = nextCrew = nextCrewSlot = now; if (levelledStopped) { levelledStopped = false; PropertiesLevelUp(); } nextBriefcases = nextEvents = nextGlobalBoss = nextKnock = now; ResetGlobalBoss(); badgeSig = eventsSig = null; eventsOn.Clear(); briefcasesSaid = null; rerollCrew = null; rerollSaid = null; lastBank = DateTime.MinValue; learnedThisRun = false; foldDue = true; rejoiner = new Rejoiner(); dialogUp = disconnectHint = robloxUsed = false; robloxUsedAt = DateTime.MinValue; string ocr = Ocr.Check(); Log((scanRequested ? "Scanning the game screens" : checkRequested ? "Checking your game" : "Bot started") + " - version " + Program.Version + (ocr == null ? ", reading text in " + Ocr.Language : "")); bool play = !scanRequested && !checkRequested, needUser = false; if (play && autoRun == null) RestoreRun(); if (ocr != null) { Log("Problem: " + ocr); Info("ocr", ocr); stopWhy = ocr; return; } try { Turn(); lastError = null; } catch (StopException) { break; } catch (NeedUserException e) { Log(e.Message); stop = true; stopWhy = e.Message; needUser = true; } catch (UserBusyException) if (!waitingForUser) Log(S.WaitWhileBusy ? "You started using the PC - stopped for now, waiting until you've been idle for " + S.IdleSeconds + " s" : "The mouse moved during a click - trying again in a moment"); State("Waiting until you stop using the mouse/keyboard"); if (S.WaitWhileBusy) Sleep(S.IdleSeconds * 1000); Native.GetCursorPos(out a); int since = Environment.TickCount, start = since; while (!stop && Environment.TickCount - since < 1500 && Environment.TickCount - start < 30000) Native.GetCursorPos(out b); if (Math.Abs(b.X - a.X) > 3 || Math.Abs(b.Y - a.Y) > 3) { a = b; since = Environment.TickCount; } catch (WindowChangedException e) repeats = e.Message == lastError ? repeats + 1 : 0; if (repeats == 0) Log("Stopped this round: " + e.Message); State("Stopped this round: " + e.Message + " - looking again shortly"); if (e.Message == Game.Disconnected) disconnectHint = true; Sleep(repeats > 2 ? 60000 : 10000); catch (OutOfMemoryPause e) MemoryShort(e.InnerException); WaitForMemory(SayMemory()); bool memory = MemoryShort(e); repeats = e.Message == lastError ? repeats + 1 : 0; if (repeats % 20 == 0) Log("Problem: " + e.Message + (repeats > 0 ? " (again, " + (repeats + 1) + " times)" : "")); if (!memory) SnapshotOnce("error " + e.Message); if (repeats == 3) Discord.Problem("error " + e.Message, "⚠️ The same problem keeps coming back", ProblemReport.NoUser(e.Message), Shot, true, 180); if (memory) WaitForMemory(SayMemory()); else Sleep(repeats > 5 ? 15000 : 3000); if (play && !needUser && !keepRoll) StopRollAtEnd(); try { FoldTotals(); } catch (Exception) { } lock (partners) others = partners.ToList(); foreach (var p in others) if (!p.stop) p.Stop(keepRoll || needUser); foreach (var p in others) { var th = p.thread; if (th != null) th.Join((RollStopSeconds + 5) * 1000); } lock (partners) partners.Clear(); if (lead == null) who = null; if (stopWhy != null) { var said = StoppedMessage(stopWhy); Discord.Problem("stopped", said[0], said[1], Shot, false, 0); } if (lead != null) Discord.Close(); internal string[] StoppedMessage(string why) if (lead == null) return new[] { "\U0001F6D1 The bot stopped", why + "\nPress Start in the bot's window to go on." }; return new[] { "\U0001F6D1 " + S.Account + " stopped", why + "\nThe other accounts play on. To play " + S.Account + " again: Stop, then Start in the bot's window." }; int end = Environment.TickCount + ms; while (!stop && end - Environment.TickCount > 0) Thread.Sleep(100); readonly object memoryLock = new object(); DateTime memoryAt = DateTime.MinValue, memorySaid = DateTime.MinValue; static Exception MemoryCause(Exception e) for (int depth = 0; e != null && depth < 8; depth++) if (e is OutOfMemoryException || e.HResult == unchecked((int)0x8007000E) || e.HResult == unchecked((int)0x80070008)) return e; var all = e as AggregateException; if (all != null && all.InnerExceptions.Count > 1) return all.InnerExceptions.Select(MemoryCause).FirstOrDefault(x => x != null); static bool PictureFailed(Exception e) for (int depth = 0; e != null && depth < 8; depth++, e = e.InnerException) if (e is ArgumentException && (e.StackTrace ?? "").Contains("System.Drawing.")) return true; internal bool MemoryShort(Exception e) if (MemoryCause(e) == null && !(PictureFailed(e) && DateTime.UtcNow < memoryAt.AddMinutes(5))) return false; memoryAt = DateTime.UtcNow; if (DateTime.UtcNow < memorySaid.AddMinutes(10)) return false; memorySaid = DateTime.UtcNow; try { text = Native.MemoryText(); } catch (Exception) { text = "not known"; } public void NoteMemory(Exception e) try { if (MemoryShort(e)) SayMemory(); } void WaitForMemory(bool say) System.Runtime.GCSettings.LargeObjectHeapCompactionMode = System.Runtime.GCLargeObjectHeapCompactionMode.CompactOnce; GC.WaitForPendingFinalizers(); if (say) Log("The PC ran out of memory - the bot waits a minute before going on"); State("The PC ran out of memory - waiting a minute before going on"); bool touched = rejoiner.Active ? Native.LastInputTick() != stepInput : game.Win.UserInputSinceBot(); var step = rejoiner.OnClosed(DateTime.UtcNow, touched, Rejoins, robloxUsed); Away("closed", "⚠️ Roblox isn't open", Rejoins && (step != Rejoiner.Step.Wait || rejoiner.Active) ? "The bot starts the game again." : "The bot waits until Roblox is open again."); if (TakeRejoinStep(step)) return; State(rejoiner.Status ?? (RobloxWindow.All().Count > 0 ? S.Account + "'s Roblox window is closed - open it again, or press Stop and pick another account" : "Waiting for Roblox to be opened...")); if (game.Win.IsForeground && game.Win.UserActiveWithin(5000)) { robloxUsed = true; robloxUsedAt = DateTime.UtcNow; } string unreadable = null; bool leftAnimals = false; Game.Disconnect dialog = null; using (var f = game.Capture()) string other = RobloxWindow.OtherWindow(); robloxCheckUp = other != null; unreadable = "Roblox opened its own \"" + other + "\" window beside the game - a Roblox check only you can do: " + "join the game once with this account on a phone or another PC, then restart Roblox here"; else if (v.Problem != null) unreadable = v.Language != null ? v.Problem : string.Format("Can't read the game in this {0}x{1} window: {2}", f.Width, f.Height, v.Problem); if (DateTime.UtcNow >= nextRulingLook && !(S.WaitWhileBusy && game.Win.UserActiveWithin(15000))) nextRulingLook = DateTime.UtcNow.AddMinutes(1); if (game.CloseRulingFamily(f)) game.ForgetView(); else if (game.ReadAnimalsWindow(f) != null) leftAnimals = true; h = game.ReadHeader(f, v); NoteRobloxBar(game.RobloxBar); if (S.Events && !game.RobloxBar) NoteBadges(f); looked = LookForDisconnect(f, h == null || (h.Energy < 0 && h.Stamina < 0 && h.Health < 0), ref dialog); Log("Closing the Animals window left open over the game's menu"); if (game.CloseAnimals()) { game.ForgetView(); return; } if (looked) dialogUp = dialog != null; if (unreadable != null || dialogUp) readOk = false; if (unreadable != null && !dialogUp && !rejoiner.Active) Away("unreadable", "⚠️ The bot can't read the game", unreadable); if (unreadable != null && !dialogUp && !robloxUsed && game.Win.Handle == readHandle) rejoiner.Playing(DateTime.UtcNow); if (unreadable != null || dialogUp || (rejoiner.Active && !looked)) if (!looked || !Offline(dialog)) { State(rejoiner.Status ?? unreadable ?? Game.Disconnected); WatchRoblox(4000); } if (awaySince != DateTime.MinValue) BackIn(); if (!string.IsNullOrWhiteSpace(h.Name) && NoteAccount(h.Name)) State("Another account is in Roblox: taking its own settings"); bool xpUp = h.Level <= 0 && h.XpNext > 0 && h.XpNext == xpNextLast && xpNextSeen > 0 && h.XpNext > xpNextSeen; if (h.XpNext > 0 && h.XpNext == xpNextLast) xpNextSeen = h.XpNext; if (xpUp) { game.MenuChanged(); PropertiesLevelUp(); BossesLevelUp(level > 0 ? level + 1 : 0); } if (h.Level > 0 && TakeLevel(h.Level)) if (h.Level > level) { game.MenuChanged(); PropertiesLevelUp(); if (level > 0) BossesLevelUp(h.Level); } LevelPing(h.Level, h.XpNext); else if (Game.LevelFitsXp(level, h.XpNext) == false && h.XpNext == xpNextSeen) Log("The level kept (" + level + ") doesn't fit the XP bar (" + h.XpNext + " to the next) - forgotten until it reads again"); S.BarMax = game.KnownMax; NoteBarsMoving(h, DateTime.UtcNow); NoteHealth(h.Health, h.HealthMax, DateTime.UtcNow); GuessBar(staminaRegen, DateTime.UtcNow, ref h.Stamina, ref h.StaminaMax); GuessBar(energyRegen, DateTime.UtcNow, ref h.Energy, ref h.EnergyMax); if (h.Energy < 0 && before != null) { h.Energy = before.Energy; h.EnergyMax = before.EnergyMax; } if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) State("Waiting until you stop using the mouse/keyboard (scan ready)"); if (!Act(delegate { try { Scan(); } catch (NeverPressException) { Log("--- Scan stopped: a press was refused ---"); } })) return; if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) State("Waiting until you stop using the mouse/keyboard (check ready)"); if (!Act(delegate { CheckGame(); })) return; if (game.Win.Handle == readHandle && readOk) rejoiner.Playing(DateTime.UtcNow); if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) State("Roblox is minimized - restoring it once you stop using the mouse/keyboard"); if (frontFails == 0) Log("Roblox is minimized - restoring it"); if (SweepRobloxBar()) return; if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) State("Waiting until you stop using the mouse/keyboard (only rerolling)"); bool goDue = DateTime.UtcNow >= nextReroll; if (!goDue && !RollStopDue(DateTime.UtcNow)) State("Only rerolling: the next go at " + nextReroll.ToLocalTime().ToString("HH:mm")); if (DisconnectBeforeRound()) return; State("Working: only rerolling the crew"); if (goDue) Log("Doing: only rerolling the crew" + (h.Cash >= 0 ? " (" + M(h.Cash) + " on hand)" : "")); if (GearBackDue(DateTime.UtcNow)) RunTask("gear back", h); if (RollStopDue(DateTime.UtcNow)) RunTask(RollStopTask, h); if (Program.Reroller && brk != null) foreach (var t in brk.Split(new[] { ", " }, StringSplitOptions.RemoveEmptyEntries)) if ((t == "jobs" && S.RerollerJobs) || (t == "give stamina" && S.RerollerGifts)) { Log("Doing: " + t + " (a full bar)"); RunTask(t, last ?? h); } failedNow = neutralNow = false; try { DoRerolls(true); if (!failedNow && !neutralNow) TaskWorked("rerolls"); } finally { taskNow = null; if (failedNow) StreakCheck("rerolls"); } if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) State("Waiting until you stop using the mouse/keyboard (only fighting)"); bool goDue = DateTime.UtcNow >= nextFights; if (!goDue && !RollStopDue(DateTime.UtcNow)) State("Only fighting: the next go at " + nextFights.ToLocalTime().ToString("HH:mm")); if (DisconnectBeforeRound()) return; State("Working: only fighting players"); if (goDue) Log(string.Format("Doing: only fighting players (stamina {0}/{1}, health {2}/{3})", h.Stamina, h.StaminaMax, h.Health, h.HealthMax)); if (GearBackDue(DateTime.UtcNow)) RunTask("gear back", h); if (RollStopDue(DateTime.UtcNow)) RunTask(RollStopTask, h); failedNow = neutralNow = false; try { fought = DoFights(100, S.OnlyFightsSpendAll ? 0 : AboveKept); if (!failedNow && !neutralNow) TaskWorked("fights"); } finally { taskNow = null; if (failedNow) StreakCheck("fights"); } if (due.Contains("learn jobs")) due = new List<string> { "learn jobs" }; Log(string.Format("Test round finished: nothing was due (energy {0}/{1}, stamina {2}/{3}, health {4}/{5})", h.Energy, h.EnergyMax, h.Stamina, h.StaminaMax, h.Health, h.HealthMax)); State("Running - nothing to do right now"); if (turns != null) { idleUntil = DateTime.UtcNow.AddSeconds(60); return; } if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) if (!waitingForUser) Log("You're using the PC - waiting until you've been idle for " + S.IdleSeconds + " s (" + string.Join(", ", due) + " ready)"); State("Waiting until you stop using the mouse/keyboard (" + string.Join(", ", due) + " ready)"); if (S.WaitWhileBusy) waitingForUser = false; if (DisconnectBeforeRound()) return; State("Working: " + string.Join(", ", due)); bool acted = Act(delegate Log(string.Format("Doing: {0} (energy {1}/{2}, stamina {3}/{4}, health {5}/{6})", string.Join(", ", due), h.Energy, h.EnergyMax, h.Stamina, h.StaminaMax, h.Health, h.HealthMax)); var left = new List<string>(); for (int i = 0; i < due.Count && !RerollOnly && !FightOnly; i++) if (i > 0 && GlobalBossCuts(due[i])) break; if (TurnUp(due[i])) { left.Add(due[i]); continue; } State("Working: " + Doing(due, i)); if (i > 0) TerritoryCheck(); if (!due.Contains("learn jobs") || RerollOnly || FightOnly) return; using (var f = game.Capture()) h2 = game.ReadHeader(f); if (h2.Energy < 0 && last != null) { h2.Energy = last.Energy; h2.EnergyMax = last.EnergyMax; } var more = WhatIsDue(h2); more.Remove("learn jobs"); if (more.Count == 0) return; Log(string.Format("Doing: {0} (energy {1}/{2}, stamina {3}/{4})", string.Join(", ", more), h2.Energy, h2.EnergyMax, h2.Stamina, h2.StaminaMax)); for (int i = 0; i < more.Count; i++) if (GlobalBossCuts(more[i])) break; if (TurnUp(more[i])) { left.Add(more[i]); continue; } State("Working: " + Doing(more, i)); if (acted) waitingForUser = false; if (Once && acted) { Log("Test round finished: " + Count); stop = true; } int loadingLookAt = Environment.TickCount - 60000; void NoteLoading(Frame f) if (Environment.TickCount - loadingLookAt < 30000) return; loadingLookAt = Environment.TickCount; if (!Game.GameLoading(f, out updated)) return; string v = Game.LoadingVersion(f); if (v == null || v == versionSaid) return; Log("The game is loading" + (updated ? " its new version " : ", version ") + v); static string Doing(List<string> tasks, int i) var rest = tasks.Skip(i + 1).ToList(); return tasks[i] + (rest.Count == 0 ? "" : ". Next: " + string.Join(", ", rest.Take(3)) + (rest.Count > 3 ? " and " + (rest.Count - 3) + " more" : "")); if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) throw new UserBusyException(); if (!game.Win.BeginAct()) if (frontFails++ == 0) Log("Couldn't bring Roblox to the front (PC locked, or an admin window in front?) - will keep trying"); State("Can't bring Roblox to the front - is the PC locked or an admin window in front?"); Sleep(Math.Min(3000 << Math.Min(frontFails - 1, 4), 30000)); if (frontFails > 0) { Log("Roblox is in front again (after " + frontFails + " tries)"); frontFails = 0; } catch (UserBusyException) { userBack = true; throw; } rejoiner.Playing(DateTime.UtcNow); game.Win.EndAct(S.GiveFocusBack && !stop, !stop && !userBack); bool Can(Tab t) { return game == null || !game.NotInMenu(t); } internal static void FullBarsFirst(List<string> due, Header h) bool energyFull = h.EnergyMax > 0 && h.Energy >= h.EnergyMax; bool staminaFull = h.StaminaMax > 0 && h.Stamina >= h.StaminaMax; if (!energyFull && !staminaFull) return; int at = due.TakeWhile(t => t == RollStopTask || t == "gear back" || t == "events" || t == "tutorial" || t == "operations").Count(); var first = due.Skip(at).Where(t => (energyFull && (t == "heists" || t == "jobs")) || (staminaFull && (t == "takedown" || t == "give stamina" || t == "bosses" || t == "fights"))).ToList(); if (first.Count == 0) return; foreach (var t in first) due.Remove(t); due.InsertRange(at, first); internal sealed class FullClock readonly List<KeyValuePair<DateTime, double>>[] hour = { new List<KeyValuePair<DateTime, double>>(), new List<KeyValuePair<DateTime, double>>() }; readonly double[] total = new double[2]; readonly bool[] was = new bool[2]; DateTime lastRead = DateTime.MinValue; public DateTime NextLog = DateTime.MinValue; public void Reset() { foreach (var l in hour) l.Clear(); total[0] = total[1] = 0; was[0] = was[1] = false; lastRead = NextLog = DateTime.MinValue; } public void Note(DateTime now, bool?[] full) double gap = lastRead == DateTime.MinValue ? 0 : (now - lastRead).TotalSeconds; for (int i = 0; i < 2; i++) bool isFull = full[i] == true; if (isFull && was[i] && gap > 0 && gap <= 300) { hour[i].Add(new KeyValuePair<DateTime, double>(now, gap)); total[i] += gap; } hour[i].RemoveAll(p => (now - p.Key).TotalMinutes > 60); public double HourMinutes(int bar) { return hour[bar].Sum(p => p.Value) / 60; } public double TotalMinutes(int bar) { return total[bar] / 60; } readonly FullClock fullClock = new FullClock(); DateTime fullShown = DateTime.MinValue; void NoteFullBars(Header h) var now = DateTime.UtcNow; fullClock.Note(now, new bool?[] { h.EnergyMax > 0 && h.Energy >= 0 ? (bool?)(h.Energy >= h.EnergyMax) : null, h.StaminaMax > 0 && h.Stamina >= 0 ? (bool?)(h.Stamina >= h.StaminaMax) : null }); if ((now - fullShown).TotalSeconds >= 20) Info("fullbars", string.Format("Bars sat full in the last hour: energy {0}, stamina {1}", FullMin(fullClock.HourMinutes(0)), FullMin(fullClock.HourMinutes(1)))); if (now - totalsFoldedAt >= TimeSpan.FromMinutes(10)) FoldTotals(); if (fullClock.NextLog == DateTime.MinValue) fullClock.NextLog = now.AddHours(1); else if (now >= fullClock.NextLog) fullClock.NextLog = now.AddHours(1); Log(string.Format("Bars: energy sat full {0} and stamina {1} in the last hour ({2} and {3} since Start)", FullMin(fullClock.HourMinutes(0)), FullMin(fullClock.HourMinutes(1)), FullMin(fullClock.TotalMinutes(0)), FullMin(fullClock.TotalMinutes(1)))); static string FullMin(double minutes) { return minutes < 0.5 ? "0 min" : Math.Round(minutes) + " min"; } bool GearBackDue(DateTime now) { return crewUnequipped && now >= gearBackHold && Can(Tab.Crew); } List<string> WhatIsDue(Header h, bool rerolls = true) if (Program.Whale) S.WhaleOnly(); else S.MacroOnly(); if (Program.Reroller) S.RerollerOnly(); var now = DateTime.UtcNow; var due = new List<string>(); bool rollStop = RollStopDue(now); if (rollStop) due.Add(RollStopTask); if (GearBackDue(now)) due.Add("gear back"); if (S.GlobalBoss && globalBossUp) if (now >= expClaimAgain && ExpClaimDue(now, h)) due.Add("2x exp"); if (S.Operations && Can(Tab.Operations) && ByNumber(Tab.Operations, nextOps, now)) due.Add("operations"); if (now >= nextGlobalBoss) due.Add("global boss"); if (rerolls && S.KeepAlive && due.Count == 0 && now >= keepAliveHold && Rejoiner.KeepAliveDue(now, GameInput)) due.Add("stay in the game"); if (!globalBossWaits || due.Count > 0) return due; bool bossKeeps = S.GlobalBoss && globalBossUp; if (S.Events && !string.IsNullOrEmpty(badgeSig) && (badgeSig != eventsSig || now >= nextEvents)) due.Add("events"); if (tutorialAction != null && (tutorialAction != "fight" || (h.Stamina > 0 && !bossKeeps && Can(Tab.Fight)))) due.Add("tutorial"); if (S.GlobalBoss && now >= nextGlobalBoss) due.Add("global boss"); if (S.Operations && Can(Tab.Operations) && ByNumber(Tab.Operations, nextOps, now)) due.Add("operations"); bool bare = crewUnequipped; if (GlobalBossHoldsExp(now)) { if (ExpPending(now)) ExpSay("gb", "2X EXP: waits for the Global Boss"); } if (now >= expClaimAgain && ExpClaimDue(now, h)) due.Add("2x exp"); bool expHolds = ExpHoldsHealth(now); if (expHolds) ExpSay("hold", ExpRunning(now) ? "2X EXP: running - player fights and the Takedown wait while a boss is left" : "2X EXP: saving health for it - player fights and the Takedown wait" + ExpWhen(now)); else expSaid.Remove("hold"); if (S.Takedown && !Lowering && !bare && !expHolds && !bossKeeps && Can(Tab.Family) && (now >= nextTakedown || (takedownWants > 0 && h.Stamina >= 0 && h.Stamina - Kept(h.StaminaMax) >= Math.Min(5, takedownWants)))) due.Add("takedown"); if (S.GiveStamina && !bossKeeps && now >= nextGive && WhyNotGive(h) == null && Can(Tab.Family) && !BossDamageHold()) due.Add("give stamina"); if (S.Bosses && !bare && !bossKeeps && now >= nextBosses && Can(Tab.Bosses)) due.Add("bosses"); if (S.Playtime && now >= nextPlaytime && Can(Tab.Safehouse)) due.Add("playtime rewards"); if (S.Contracts && Can(Tab.Contracts) && ByNumber(Tab.Contracts, nextContracts, now)) due.Add("contracts"); if (S.Briefcases && now >= nextBriefcases) due.Add("briefcases"); if (KnocksOn && S.TrickOrTreat && now >= nextKnock && Can(Tab.Territory)) due.Add("trick or treat"); double spend = Spendable(h); if (S.Properties && now >= nextProperties && (propLotsOpen != 0 || propReplace || propRecheck) && Can(Tab.Properties) && (propRecheck || propPrice <= 0 || (spend >= 0 && spend >= propPrice * 1.01))) due.Add("properties"); if (S.SafehouseUpgrade && (now >= nextUpgrade || (upgradeNeedsLevel > 0 && h.Level >= upgradeNeedsLevel)) && Can(Tab.Safehouse) && (upgradePrice < 0 || (upgradePrice > 0 && spend >= 0 && spend >= upgradePrice + PropertyKeep()))) due.Add("safehouse upgrade"); if (!Lowering && ((ShopOn() && now >= nextShop && Can(Tab.Shop)) || (S.Crates && now >= nextCrates && Can(Tab.Inventory) && !CratesHeld(now)) || (S.EquipBest && now >= nextEquip))) due.Add("gear"); if (!Program.Whale && now >= nextSafehouse && Can(Tab.Safehouse)) due.Add("read safehouse"); if (AnimalsDue(now)) due.Add("animals"); if (S.GameStats && now >= nextGameStats) due.Add("game stats"); if (S.SpendPoints && h.SkillPoints >= game.PointsCost && now >= nextPoints && pointsStopped != PointsStopKey()) due.Add("skill points"); if (S.Jobs && NeedLearn() && Can(Tab.Jobs)) due.Add("learn jobs"); bool startDue = S.StartHeists && now >= nextHeistStart; bool startSaved = startDue && heistEnergy > 0 && h.Energy >= heistEnergy; if (S.Heists && ((now >= nextHeists || startSaved) && (h.Energy >= helpEnergy || startDue)) && Can(Tab.Heists) && !(S.HeistsFamilyOnly && family.NotInFamily)) due.Add("heists"); if (S.Jobs && S.JobMode == JobMode.OneJob) NoteOneJob(h); else oneJobSaid = null; if (S.Jobs && now >= nextJobs && JobReady(h) && Can(Tab.Jobs) && !HeistEnergyHeld(h)) due.Add("jobs"); if (crewQuiet && (S.CrewFill || S.CrewReroll || S.TrainCrew)) { crewQuiet = false; nextCrew = now; } if (!Program.Whale && !Lowering && Can(Tab.Crew) && (now >= nextCrew || (BuySlots && crewSlotPrice > 0 && spend >= crewSlotPrice + SlotKeep() && now >= nextCrewSlot))) due.Add("crew"); if (S.Fights && !bare && !expHolds && !bossKeeps && now >= nextFights && (FightsMayUse(h) || FightItemsReady(now)) && Can(Tab.Fight)) due.Add("fights"); if (Lowering && !(S.RespectCycle && S.Fights) && now >= respectWait) due.Add("respect back"); if (S.FamilyWatch && now >= nextFamily && Can(Tab.Family)) due.Add("family"); if (Settings.MarketRelistShown && (S.MarketRelist || S.MarketPending.Length > 0) && now >= nextMarket && Can(Tab.BlackMarket)) due.Add("relist"); bool bankTime = S.BankAt > 0 && !cashOut ? h.Cash >= S.BankAt && now - lastBank >= TimeSpan.FromSeconds(30) : now >= nextBank || (due.Count > 0 && now >= lastBank.AddMinutes(1)); if (S.Bank && bankTime && Can(Tab.Bank) && (!AutoRollHoldsBank() || rollStop)) due.Add("bank"); bool lucky = RerollsLead(); if (rerolls && (due.Count == 0 || lucky) && S.CrewReroll && !RollUnwanted && now >= nextReroll && RerollCashOk(h, now) && Can(Tab.Crew)) due.Insert(lucky ? due.TakeWhile(t => t == RollStopTask || t == "gear back" || t == "events" || t == "tutorial").Count() : due.Count, "rerolls"); if (lucky && rerollsYield) foreach (var t in new[] { "gear", "bank" }) if (due.Remove(t)) due.Insert(due.IndexOf("rerolls"), t); else if (!Lowering && rerolls && autoRun != null && autoRun.Running && !RollUnwanted && now >= nextReroll && Can(Tab.Crew)) due.Add("rerolls"); if (rerolls && S.KeepAlive && due.Count == 0 && now >= keepAliveHold && Rejoiner.KeepAliveDue(now, GameInput)) due.Add("stay in the game"); if (cashOut && S.Bank && Can(Tab.Bank) && (!AutoRollHoldsBank() || rollStop)) int at = due.TakeWhile(t => t == RollStopTask || t == "gear back").Count(); if (BankFee() != 0) at = Math.Max(at, Math.Max(due.LastIndexOf("properties"), due.LastIndexOf("safehouse upgrade")) + 1); public DateTime Red = DateTime.MinValue, Plain = DateTime.MinValue; public DateTime Visited = DateTime.MinValue; public bool Anyway = true; public DateTime IgnoreUntil = DateTime.MinValue; public DateTime Safety = DateTime.MinValue; readonly Dictionary<Tab, MenuNumber> menuNumbers = new Dictionary<Tab, MenuNumber>(); menuNumbers[Tab.Operations] = new MenuNumber(); menuNumbers[Tab.Contracts] = new MenuNumber(); int menuNumbersAt = Environment.TickCount - 60000; void NoteMenuNumbers(Frame f) if (menuNumbers.Count == 0 || Environment.TickCount - menuNumbersAt < 20000) return; menuNumbersAt = Environment.TickCount; if (v == null || v.Problem != null) return; var now = DateTime.UtcNow; foreach (var kv in menuNumbers) var b = v.Badge(f, kv.Key); if (b == true) { if (++kv.Value.RedReads >= 2) kv.Value.Red = now; } else if (b == false) { kv.Value.RedReads = 0; kv.Value.Plain = now; } bool ByNumber(Tab t, DateTime next, DateTime now) if (!menuNumbers.TryGetValue(t, out n)) return now >= next; return NumberSays(n.Red, n.Plain, n.Visited, n.Anyway, n.IgnoreUntil, n.Safety, next, now); internal static bool NumberSays(DateTime red, DateTime plain, DateTime visited, bool anyway, DateTime ignoreUntil, DateTime safety, DateTime next, DateTime now) var seen = red > plain ? red : plain; if (now - seen > TimeSpan.FromMinutes(2)) return now >= next; if (red > plain && now >= ignoreUntil) return now >= visited.AddMinutes(1); if (safety == DateTime.MinValue || safety > visited.AddHours(3)) safety = visited.AddHours(3); return now >= next && (anyway || now >= safety); void Visited(Tab t, bool? worked, bool anyway, DateTime safety = default(DateTime)) if (!menuNumbers.TryGetValue(t, out n)) return; var now = DateTime.UtcNow; if (worked == false && n.Red > n.Plain && now - n.Red < TimeSpan.FromMinutes(2)) n.IgnoreUntil = now.AddMinutes(30); n.Red = DateTime.MinValue; sealed class Streak { public DateTime Since; public int Fails; public string Why = ""; public bool Said; } readonly Dictionary<string, Streak> streaks = new Dictionary<string, Streak>(); void Neutral() { neutralNow = true; } static string TaskTitle(string task) case "give stamina": return "Stamina gifts"; case "gear back": return "Putting the crew's gear back"; case "gear": return "Shop, gear and crates"; case "read safehouse": return "Reading the Safehouse"; case "learn jobs": return "Reading the job list"; case "stay in the game": return "Staying in the game"; case "relist": return "Black Market relisting"; default: return char.ToUpperInvariant(task[0]) + task.Substring(1); void TaskFailed(string why, bool own) if (task == null || string.IsNullOrWhiteSpace(why)) return; if (!streaks.TryGetValue(task, out s)) streaks[task] = s = new Streak(); var now = DateTime.UtcNow; if (own) { s.Why = why.Trim(); if (s.Said) ShowStuck(); } if (s.Fails == 0) s.Since = now; void StreakCheck(string task) if (!streaks.TryGetValue(task, out s) || s.Said || (s.Fails < 3 && DateTime.UtcNow - s.Since < TimeSpan.FromHours(1))) return; string line = StuckLine(task, s); Discord.Problem("stuck " + task, "⚠️ " + ProblemReport.NoUser(line), "The bot keeps trying. The pictures are in its problems folder.", Shot, false, 180); void TaskWorked(string task) if (!streaks.TryGetValue(task, out s)) return; Log(TaskTitle(task) + ": working again (stuck from " + s.Since.ToLocalTime().ToString("HH:mm") + ")"); static string StuckLine(string task, Streak s) return TaskTitle(task) + ": stuck since " + s.Since.ToLocalTime().ToString("HH:mm") + " - " + s.Why + (s.Fails > 1 ? " (" + s.Fails + " tries)" : ""); Info("stuck", string.Join("\n", streaks.Where(kv => kv.Value.Said).Select(kv => StuckLine(kv.Key, kv.Value)))); void RunTask(string task, Header h) failedNow = neutralNow = false; bool spends = task == "fights" || task == "takedown" || task == "tutorial"; if (spends) HealthSpent(); try { RunTaskWatched(task, h); if (!failedNow && !neutralNow) TaskWorked(task); } finally { taskNow = null; if (failedNow) StreakCheck(task); if (spends) HealthSpent(); } void RunTaskWatched(string task, Header h) try { RunTaskNow(task, h); redone.Remove(task); } catch (UserBusyException) { if (redone.Add(task)) DueAgain(task); throw; } catch (WindowChangedException e) if (redone.Add(task)) DueAgain(task); else if (e.Message != Game.Disconnected) Log(TaskTitle(task) + ": cut short twice in a row (" + e.Message + ") - it waits 10 minutes, the rest go on"); game.NoteFailure("cut short twice in a row: " + e.Message); catch (NeverPressException) { redone.Remove(task); Later(task, 30); } catch (StopException) { throw; } catch (NeedUserException) { throw; } var memory = MemoryShort(e) ? MemoryCause(e) ?? e : null; Later(task, memory != null ? 2 : 15); if (firstSeen.Add("task error " + task + " " + e.Message)) if (memory != null) Log(TaskTitle(task) + ": the PC ran out of memory (" + memory.GetType().Name + ": " + memory.Message + ") - it waits 2 minutes"); else Log(TaskTitle(task) + ": an error (" + e.GetType().Name + ": " + e.Message + ") - it waits 15 minutes, the rest go on"); LogDetails(e.ToString()); if (memory == null) SnapshotOnce("error in " + task); game.NoteFailure(memory != null ? "the PC ran out of memory" : "an error: " + e.Message); if (memory != null) throw new OutOfMemoryPause(e); rejoiner.Playing(DateTime.UtcNow); if (SpendsEnergy.Contains(task)) energyRegen.Forget(); if (SpendsStamina.Contains(task)) staminaRegen.Forget(); static readonly HashSet<string> SpendsEnergy = new HashSet<string> { "jobs", "heists", "tutorial", "skill points", "learn jobs", "animals" }; static readonly HashSet<string> SpendsStamina = new HashSet<string> { "bosses", "fights", "takedown", "give stamina", "tutorial", "skill points", "global boss" }; void Later(string task, int minutes) var when = DateTime.UtcNow.AddMinutes(minutes); case "operations": nextOps = when; break; case "bosses": nextBosses = when; break; case "playtime rewards": nextPlaytime = when; break; case "contracts": nextContracts = when; break; case "briefcases": nextBriefcases = when; break; case "global boss": nextGlobalBoss = when; break; case "trick or treat": nextKnock = when; break; case "events": nextEvents = when; eventsSig = badgeSig; break; case "properties": nextProperties = when; break; case "safehouse upgrade": nextUpgrade = when; break; case "crew": nextCrew = nextCrewSlot = when; break; case "rerolls": nextReroll = when; break; case RollStopTask: rollStopNext = when; break; case "gear back": gearBackHold = when; break; case "gear": nextShop = nextCrates = nextEquip = when; break; case "read safehouse": nextSafehouse = when; break; case "game stats": nextGameStats = when; break; case "skill points": nextPoints = when; break; case "learn jobs": nextLearn = when; break; case "heists": nextHeists = when; break; case "jobs": nextJobs = when; break; case "fights": nextFights = when; break; case "bank": nextBank = when; lastBank = when.AddMinutes(-1); break; case "takedown": nextTakedown = when; break; case "give stamina": nextGive = when; break; case "family": nextFamily = when; break; case "relist": nextMarket = when; break; case "animals": nextAnimals = when; break; case "stay in the game": keepAliveHold = when; break; case "tutorial": nextTutorialLook = when; tutorialAction = null; break; DateTime keepAliveHold = DateTime.MinValue; readonly HashSet<string> redone = new HashSet<string>(); void DueAgain(string task) var now = DateTime.UtcNow; case "operations": nextOps = now; break; case "bosses": nextBosses = now; break; case "playtime rewards": nextPlaytime = now; break; case "contracts": nextContracts = now; break; case "briefcases": nextBriefcases = now; break; case "global boss": nextGlobalBoss = now; break; case "trick or treat": nextKnock = now; break; case "events": nextEvents = now; break; case "properties": nextProperties = now; break; case "safehouse upgrade": nextUpgrade = now; break; case "crew": nextCrew = now; break; case "rerolls": nextReroll = now; break; case RollStopTask: rollStopNext = now; break; case "gear": nextCrates = nextEquip = now; break; case "read safehouse": nextSafehouse = now; break; case "game stats": nextGameStats = now; break; case "skill points": nextPoints = now; break; case "learn jobs": nextLearn = now.AddMinutes(5); break; case "heists": nextHeists = now; break; case "jobs": nextJobs = now; break; case "fights": nextFights = now; break; case "bank": nextBank = now; break; case "takedown": nextTakedown = now; break; case "give stamina": nextGive = now; break; case "family": nextFamily = now; break; case "relist": nextMarket = now; break; case "animals": nextAnimals = now; break; void RunTaskNow(string task, Header h) case "operations": DoOperations(); break; case "bosses": DoBosses(); break; if (ClaimHeldExp(S.Bosses && Can(Tab.Bosses) ? Tab.Bosses : Tab.Safehouse, "the bosses")) bossesNow = true; else { Neutral(); expClaimAgain = DateTime.UtcNow.AddMinutes(5); } int got = game.ClaimPlaytimeRewards(); Count.Rewards += Report(got, "Claimed {0} playtime reward(s)"); if (got > 0) playtimeClaimed = DateTime.UtcNow; if (game.PlaytimeExpHeld && !expHeld) Log("Playtime rewards: the 2X EXP one is ready and kept - claimed with health almost full, for the finished operations and the bosses"); expHeld = game.PlaytimeExpHeld; NoteExpCard(DateTime.UtcNow); int wait = game.PlaytimeNextSeconds; nextPlaytime = DateTime.UtcNow.AddSeconds(got > 0 || wait == 0 ? 600 : wait > 0 ? Math.Min(wait + 20, 3 * 3600) : wait == -2 ? 3 * 3600 : 1200); int got = game.ClaimContracts(); Count.Rewards += Report(Math.Max(0, got), "Claimed {0} contract task(s)"); nextContracts = DateTime.UtcNow.AddMinutes(got > 0 ? 15 : 30); Visited(Tab.Contracts, got < 0 ? (bool?)null : got > 0, got < 0); case "events": DoEvents(); break; case "briefcases": DoBriefcases(); break; case "global boss": DoGlobalBoss(); break; case "trick or treat": DoKnocks(); break; case "tutorial": DoTutorial(); break; case "properties": DoProperties(); break; case "safehouse upgrade": DoSafehouseUpgrade(); break; case "crew": DoCrew(); break; case "rerolls": DoRerolls(false); break; case RollStopTask: DoRollStop(); break; game.ScrollPage(0.62, 40); if (DoEquip() || !crewUnequipped) break; int wait = gearBackFails == 1 ? 1 : gearBackFails == 2 ? 5 : 15; gearBackHold = DateTime.UtcNow.AddMinutes(wait); Log("Crew: the gear isn't back on yet - bosses, player fights and the Takedown wait; AUTO EQUIP BEST again in " + wait + " minute" + (wait == 1 ? "" : "s")); case "gear": DoGear(); break; case "read safehouse": ReadSafehouse(); break; case "animals": DoAnimals(h); break; case "game stats": DoGameStats(); break; int ups = game.SpendPoints(S.PointsStat, h.SkillPoints, S.PointsStop(S.PointsStat)); int stat = game.SpentOn >= 0 ? game.SpentOn : S.PointsStat; Report(ups, "Skill points: " + Game.StatNames[stat] + " +{0}"); pointsStopped = PointsStopKey(); Log("Skill points: " + Game.StatNames[stat] + " is at your stop of " + S.PointsStop(stat) + " - the points stay saved until you change it"); if (ups > 0 && stat == 5) PropertiesLevelUp(); if (ups == 0 && game.SavingFor != null && pointsSaid != Game.StatNames[stat] + game.PointsCost) pointsSaid = Game.StatNames[stat] + game.PointsCost; Log("Skill points: saving up for " + game.SavingFor); nextPoints = DateTime.UtcNow.AddMinutes(game.PointsFailed ? 60 : 10); case "learn jobs": LearnJobs(); break; case "heists": DoHeists(); break; case "jobs": DoJobs(); break; case "fights": DoFights(); break; case "respect back": RespectBack("the respect cycle was switched off"); break; if (BankJustRan(DateTime.UtcNow)) if (nextBank < lastBank.AddSeconds(15)) nextBank = lastBank.AddSeconds(15); case "stay in the game": game.KeepAlive(); break; case "takedown": DoTakedown(); break; case "give stamina": DoGiveStamina(); break; case "family": DoFamilyWatch(); break; case "relist": DoRelist(); break; int Report(int n, string format) if (n > 0) Log(string.Format(format, n)); var now = DateTime.UtcNow; nextMarket = now.AddMinutes(Math.Max(Settings.MarketMinutesLeast, Math.Min(60, S.MarketMinutes))); if (!PendingBackUp()) return; if (!S.MarketRelist) { Neutral(); return; } var shop = game.OpenMyShop(); if (shop == null) return; if (shop.Rows.Count == 0) if (used == 0) SayOnce("market none", "Black Market: there are no listings in your shop - nothing to relist (the bot only puts your own back up)"); var done = new List<string>(); var turned = new HashSet<string>(); for (int go = 0; go < Math.Max(1, used) && go < 40; go++) if (go > 0) { shop = game.OpenMyShop(); if (shop == null) break; } shop = game.MyShopToEnd(shop); MarketListing last = null; for (int look = 0; look < 3; look++) if (look > 0) game.Wait(1000); using (var f = game.Capture()) last = game.ReadMyShop(f, Game.Prices.Last).Rows.LastOrDefault(); if (last != null && last.Animal) SayOnce("market animal", "Black Market: " + last.Name + " is an animal - the bot leaves it in your shop as it is and relists the others"); last = game.ReadMyShop(f, Game.Prices.All).Rows.LastOrDefault(r => !r.Animal); if (last == null || (last.Price > 0 && last.Left > 0 && last.Name.Length >= 3)) break; SayOnce("market closed", "Black Market: your listings don't say FOR SALE right now (is the shop closed?) - nothing relisted. The bot never pays rent"); game.Snapshot("market listing not for sale", 360); if (turned.Contains(Parse.Key(last.Name) + "|" + last.Left + "|" + last.Price)) break; if (last.Price <= 0 || last.Left <= 0 || last.Name.Length < 3) Log("Black Market: couldn't read \"" + last.Name + "\" (" + (last.Left > 0 ? last.Left + " left" : "how many are left unread") + ", " + (last.Price > 0 ? last.Price + " each" : "its price unread") + ") - left in your shop as it is"); game.Snapshot("market listing unread", 60); S.MarketPending = last.Name + "|" + last.Left + "|" + last.Price; var removed = game.RemoveListing(last, used); game.NoteFailure("Black Market: " + last.Name + " wasn't taken out"); Log("Black Market: couldn't tell whether " + last.Name + " was taken out - looking again in 2 minutes"); game.NoteFailure("Black Market: " + last.Name + " unclear"); nextMarket = DateTime.UtcNow.AddMinutes(2); string why = game.ListItem(last.Name, last.Left, last.Price, out listed); if (why == Game.NotSeenAfterPut && WaitForCheck(last.Name, listed, last.Price)) why = null; else if (why == Game.NotSeenAfterPut) { CheckNotDone(last.Name); break; } Log("Black Market: " + last.Name + " is in your inventory again but didn't go back up (" + why + ") - trying again in " + MarketRetryMinutes + " minutes, no collection deposit meanwhile"); game.NoteFailure("Black Market: " + last.Name + " didn't go back up"); MarketPing(last.Name, why); nextMarket = DateTime.UtcNow.AddMinutes(MarketRetryMinutes); turned.Add(Parse.Key(last.Name) + "|" + listed + "|" + last.Price); done.Add(last.Name + " (" + listed + (listed < last.Left ? " of " + last.Left : "") + " at " + last.Price + ")"); if (game.KeptSpotMinutes >= 0) Log("Black Market: the game kept " + last.Name + " in its old spot - it moves to the top only in " + game.KeptSpotMinutes + " min, so the next relist waits for that"); nextMarket = DateTime.UtcNow.AddMinutes(Math.Max(game.KeptSpotMinutes + 1, Settings.MarketMinutesLeast)); if (done.Count > 0) Log("Black Market: relisted " + string.Join(", ", done) + " - back on top"); if (S.MarketPending.Length == 0) return true; var p = S.MarketPending.Split('|'); if (p.Length != 3 || !int.TryParse(p[1], out qty) || !int.TryParse(p[2], out price) || qty <= 0 || price <= 0 || p[0].Length < 3) var shop = game.OpenMyShop(); if (shop == null) { nextMarket = DateTime.UtcNow.AddMinutes(2); return false; } var end = game.MyShopToEnd(shop); if (shop.Rows.Concat(end.Rows).Any(r => Game.SameItem(r.Name, name) && r.Left == qty)) Log("Black Market: " + name + " is up in your shop already"); checkUndoneAt = DateTime.MinValue; if (DateTime.UtcNow - checkUndoneAt < TimeSpan.FromHours(3)) SayOnce("market check wait " + checkUndoneAt.Ticks, "Black Market: " + name + " isn't up yet - the bot puts it up again 3 hours after the bot check, and looks every 30 minutes meanwhile"); nextMarket = DateTime.UtcNow.AddMinutes(MarketRetryMinutes); checkUndoneAt = DateTime.MinValue; string why = game.ListItem(name, qty, price, out listed); if (why == Game.NotSeenAfterPut && WaitForCheck(name, listed, price)) why = null; else if (why == Game.NotSeenAfterPut) { CheckNotDone(name); return false; } Log("Black Market: put " + name + " back up (" + listed + " at " + price + " each)"); if (why == Game.NotTradable) Log("Black Market: " + name + " isn't among your tradable items any more - nothing to put back up"); Log("Black Market: " + name + " still didn't go back up (" + why + ") - again in " + MarketRetryMinutes + " minutes, no collection deposit meanwhile"); game.NoteFailure("Black Market: " + name + " didn't go back up"); nextMarket = DateTime.UtcNow.AddMinutes(MarketRetryMinutes); void MarketPing(string name, string why) Discord.Problem("market relist", "⚠️ Black Market: " + name + " didn't go back up", "(" + why + ")\nIf the game shows a bot check, do it yourself " + CheckWhere + ". The item is in your inventory; the bot tries again in " + MarketRetryMinutes + " minutes.", Shot, false, 30, mention: true); const int CheckWaitMinutes = 2; const string CheckWhere = "on the Live page with the mouse on (http://127.0.0.1:8090/?control)"; const string CheckWhere = "in the game"; return DateTime.TryParse(S.MarketCheckUndone, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out at) ? at : DateTime.MinValue; set { S.MarketCheckUndone = value == DateTime.MinValue ? "" : value.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture); } bool WaitForCheck(string name, int qty, int price) Log("Black Market: " + name + " didn't show in your shop after PUT IN YOUR SHOP - if the game asks a bot check, do it " + CheckWhere + ": the bot leaves the game alone up to " + CheckWaitMinutes + " minutes for it"); Discord.Problem("market check", "⚠️ Black Market: the game may want a bot check", "The bot put " + name + " up, but it didn't show in your shop. If the game asks a bot check, do it yourself " + CheckWhere + ": the bot waits " + CheckWaitMinutes + " minutes without touching the game, then relists the rest. Not done by then: it plays on and puts the item up again in 3 hours.", Shot, false, 30, mention: true); var until = DateTime.UtcNow.AddMinutes(CheckWaitMinutes); while (!stop && DateTime.UtcNow < until) try { using (var f = game.Capture()) if (game.OnTop(f, name, qty, price)) { Log("Black Market: " + name + " is up - thanks, relisting goes on"); return true; } } void CheckNotDone(string name) Log("Black Market: " + name + " still isn't up after " + CheckWaitMinutes + " minutes - it's in your inventory; the bot plays on, looks every 30 minutes whether it's up and puts it up again in 3 hours, no collection deposit meanwhile"); game.NoteFailure("Black Market: " + name + " didn't go back up"); checkUndoneAt = DateTime.UtcNow; nextMarket = DateTime.UtcNow.AddMinutes(MarketRetryMinutes); const int MarketRetryMinutes = 30; if (DateTime.UtcNow < nextLearn || level <= 0) return false; int top = Catalog.Count == 0 ? -1 : Catalog.Max(j => GameData.CityIndex(j.City)); if (top + 1 < GameData.CityLevels.Length && level >= GameData.CityLevels[top + 1]) return true; if (learnedThisRun) return false; if (catalogBuiltIn) return true; if (S.JobMode == JobMode.Mastery && !JobsReadRecently()) return true; return Catalog.Count == 0; if (!DateTime.TryParse(S.JobsReadAt, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out at)) return false; var age = DateTime.UtcNow - at; return age >= TimeSpan.Zero && age < TimeSpan.FromHours(12); nextLearn = DateTime.UtcNow.AddMinutes(30); if (!game.OpenTab(Tab.Jobs)) { nextLearn = DateTime.UtcNow.AddMinutes(5); return; } Log("Reading every job and its mastery (takes a moment)..."); int before = Catalog.Count; var kept = Catalog.Select(j => j.Copy()).ToList(); string otherBefore = OtherOnTop(); int seen = otherBefore != null ? 0 : game.LearnJobs(Catalog, level, true, fold); string other = otherBefore ?? OtherOnTop(); CatalogView = Catalog.Select(j => j.Copy()).ToList(); nextLearn = DateTime.UtcNow.AddMinutes(10); Log("Jobs: the job read was left out - the game showed " + other + " (another Roblox window in front?); read again in 10 minutes"); GameData.Renumber(Catalog); if (FoldCities().Any(c => !fold.Contains(c))) foldDue = true; if (seen > 0) { S.JobsReadAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); catalogBuiltIn = false; } Log(string.Format("Read {0} jobs ({1} new).", seen, Catalog.Count - before)); using (var f = game.Capture()) h = game.ReadHeader(f); if (h == null) return null; if (S.Account.Length > 0 && !string.IsNullOrEmpty(h.Name) && Accounts.Plausible(h.Name)) lock (accountLock) if (IsOther(Accounts.Known(dir, h.Name.Trim()), Whose())) return "\"" + h.Name.Trim() + "\""; if (h.Level > 0 && level > 0 && Math.Abs(h.Level - level) > 5) return "level " + h.Level + " (this account is " + level + ")"; CatalogView = Catalog.Select(j => j.Copy()).ToList(); GameData.SaveCatalog(catalogFile, Catalog); if (S.Account.Length > 0) { Directory.CreateDirectory(Accounts.Folder(dir, S.Account)); GameData.SaveCatalog(Accounts.Jobs(dir, S.Account), Catalog); } catch (Exception e) { if (firstSeen.Add("jobs.txt not saved")) Log("Couldn't save jobs.txt: " + e.Message); } public JobInfo PickJob(Header h) { return PickJob(h, Catalog); } public JobInfo PickJob(Header h, List<JobInfo> catalog) if (S.JobMode == JobMode.OneJob) return OneJobWhy(h, catalog, out one) == null && !JobAside(one) ? one : null; int maxE = h != null && h.EnergyMax > 0 ? h.EnergyMax : int.MaxValue; var open = catalog.Where(j => j.TierLevel <= lv && j.Cost > 0 && j.Cost <= maxE && !j.Gone && !JobAside(j)).ToList(); if (S.JobMode == JobMode.Mastery) var next = open.OrderBy(j => j.Order).FirstOrDefault(j => j.MasteryRank < MasteryGold); if (next != null || open.Count == 0) return next; return open.OrderByDescending(j => j.XpPerEnergy).ThenByDescending(j => j.Xp).FirstOrDefault(); HashSet<int> FoldCities() var picked = PickJob(last); int keep = picked == null ? -1 : GameData.CityIndex(picked.City); return new HashSet<int>(Catalog.GroupBy(j => GameData.CityIndex(j.City)) .Where(g => g.Key >= 0 && g.Key != keep && g.All(j => j.MasteryRank >= MasteryGold)).Select(g => g.Key)); public bool MasteryDone(JobInfo picked) { return S.JobMode == JobMode.Mastery && picked != null && picked.MasteryRank >= MasteryGold; } const int MasteryGold = 3; public string OneJobWhy(Header h, List<JobInfo> catalog, out JobInfo job) return OneJobWhy(S.OneJob, catalog, JobLevel(h), h != null && h.EnergyMax > 0 ? h.EnergyMax : 0, out job); int levelBelow = int.MaxValue; DateTime levelBelowUntil; int jumpLevel = -1, statsLevel = -1; DateTime jumpSince, statsAt; if (level <= 0 || read <= level + 5) { jumpLevel = -1; return true; } var now = DateTime.UtcNow; jumpLevel = read; jumpSince = now; Log("The top bar reads level " + read + ", the bot had " + level + " - a jump that big is checked on the game's STATS first"); if (S.GameStats) nextGameStats = now; if (statsLevel > 0 && statsAt >= jumpSince) return Math.Abs(statsLevel - read) <= 1; return now - jumpSince >= TimeSpan.FromMinutes(30); int lv = level > 0 ? level : h != null && h.Level > 0 ? h.Level : int.MaxValue; return levelBelow < int.MaxValue && DateTime.UtcNow < levelBelowUntil ? Math.Min(lv, Math.Max(1, levelBelow - 1)) : lv; internal static string OneJobWhy(string name, List<JobInfo> catalog, int level, int energyMax, out JobInfo job) name = (name ?? "").Trim(); if (name.Length == 0) return OneJobNone; job = catalog.FirstOrDefault(j => j.Name == name) ?? catalog.FirstOrDefault(j => string.Equals(j.Name, name, StringComparison.OrdinalIgnoreCase)); if (job == null || job.Cost <= 0) return "isn't in the job list"; if (job.Gone) return "wasn't in the game's job list"; if (job.TierLevel > Math.Max(1, level)) return "opens at level " + job.TierLevel; if (energyMax > 0 && job.Cost > energyMax) return "needs " + job.Cost + " energy and your bar holds " + energyMax; public const string OneJobNone = "no job picked"; public DateTime JobAsideUntil(string name) lock (jobAside) return name != null && jobAside.TryGetValue(name, out until) && DateTime.UtcNow < until ? until : DateTime.MinValue; void NoteOneJob(Header h) string why = OneJobWhy(h, Catalog, out job); string name = (S.OneJob ?? "").Trim(); string said = why == null ? null : name + "|" + why; if (said == oneJobSaid) return; if (why == OneJobNone) Log("Jobs: no job picked - no jobs until you click one (One job, Energy page)"); Log("Jobs: \"" + name + "\" " + why + " - no jobs" + (why.StartsWith("opens at") ? " until then" : why.StartsWith("needs") ? " until it holds more" : "") + " (One job, Energy page)"); else if (oneJobSaid != null && oneJobSaid.StartsWith(name + "|")) Log("Jobs: \"" + job.Name + "\" can be done now - doing only that job (One job)"); Info("job", why ?? job.Name); readonly Dictionary<string, DateTime> jobAside = new Dictionary<string, DateTime>(); readonly Dictionary<string, int> jobMisses = new Dictionary<string, int>(); lock (jobAside) return jobAside.TryGetValue(j.Name, out until) && DateTime.UtcNow < until; void JobMissed(JobInfo job, string why) jobMisses.TryGetValue(job.Name, out n); jobMisses[job.Name] = ++n; game.NoteFailure("\"" + job.Name + "\" " + why); jobMisses.Remove(job.Name); lock (jobAside) jobAside[job.Name] = DateTime.UtcNow.AddHours(1); Log("Jobs: \"" + job.Name + "\" " + why + " twice in a row - it waits an hour, " + (S.JobMode == JobMode.OneJob ? "no other job meanwhile (One job)" : "the next best job meanwhile")); nextJobs = DateTime.UtcNow; if (job == null || h.Energy < job.Cost) return false; if (h.EnergyMax <= 0) return true; if (h.Energy >= h.EnergyMax - 2 * job.Cost) return true; if (ExpRunning(DateTime.UtcNow)) return true; return h.XpNext > 0 && h.Xp >= 0 && job.Xp > 0 && (long)(h.Energy / job.Cost) * job.Xp >= h.XpNext - h.Xp; if (S.Jobs && S.JobMode == JobMode.OneJob) NoteOneJob(last); Info("job", job == null ? (S.JobMode == JobMode.Mastery ? "all jobs have the chosen mastery" : S.JobMode == JobMode.OneJob ? "one job: not now" : "none") : string.Format("{0} ({1} energy, {2} XP, mastery {3}{4})", job.Name, job.Cost, job.Xp, job.MasteryLabel, MasteryDone(job) ? ", the best XP job now that every job is Gold" : "")); if (job == null) { Neutral(); return; } if (last != null && last.Energy >= 0 && last.Energy < job.Cost) { Neutral(); return; } if (MasteryDone(job) && firstSeen.Add("mastery done " + job.Name)) Log("Jobs: every job open has Gold mastery - doing the best XP job, \"" + job.Name + "\", until a new one opens"); if (!game.OpenTab(Tab.Jobs)) { nextJobs = DateTime.UtcNow.AddMinutes(2); return; } if (fold.Count > 0) game.SetCities(new int[0], fold); var card = game.FindJob(job, Catalog); int ci = GameData.CityIndex(job.City); if (ci >= 0 && game.ExpandCity(ci)) card = game.FindJob(job, Catalog); var saw = game.JobsSeen.Take(12).ToList(); Log("Couldn't find \"" + job.Name + "\" in the job list - trying again in 5 minutes" + (saw.Count > 0 ? " (it showed: " + string.Join(", ", saw) + (game.JobsSeen.Count > 12 ? "..." : "") + ")" : "")); game.Snapshot("job not found " + job.Name, 60); nextJobs = DateTime.UtcNow.AddMinutes(5); JobMissed(job, "wasn't found in the job list"); var lm = Regex.Match(card.ButtonText ?? "", @"LEVEL\s*([0-9OIlS]+)", RegexOptions.IgnoreCase); if (lm.Success && int.TryParse(Parse.Digits(lm.Groups[1].Value), out opensAt) && opensAt > job.TierLevel && GameData.InCityLevels(job.City, opensAt)) if (S.JobMode != JobMode.OneJob) Log(string.Format("\"{0}\" opens at level {1} - picking another job", job.Name, opensAt)); if (opensAt <= by && (opensAt < levelBelow || DateTime.UtcNow >= levelBelowUntil)) levelBelow = opensAt; levelBelowUntil = DateTime.UtcNow.AddHours(1); Log("Jobs: the bot went by level " + (by == int.MaxValue ? "none" : by.ToString()) + " (the top bar read " + (last != null && last.Level > 0 ? last.Level.ToString() : "no level") + ") - only jobs below level " + opensAt + " for an hour"); nextJobs = DateTime.UtcNow; if (card.Button == ButtonState.Locked) if (level <= 0 && (last == null || last.Level <= 0)) int below = lm.Success && int.TryParse(Parse.Digits(lm.Groups[1].Value), out opensAt) && opensAt > 1 ? Math.Min(opensAt, job.TierLevel) : job.TierLevel; if (below > 1 && (below < levelBelow || DateTime.UtcNow >= levelBelowUntil)) levelBelow = below; levelBelowUntil = DateTime.UtcNow.AddHours(1); Log("Jobs: your level didn't read, and \"" + job.Name + "\" isn't open yet - jobs below level " + below + " for now"); nextJobs = DateTime.UtcNow; int shown, by = JobLevel(last); if (lm.Success && int.TryParse(Parse.Digits(lm.Groups[1].Value), out shown) && shown > 1 && shown <= by && GameData.InCityLevels(job.City, shown) && (shown < levelBelow || DateTime.UtcNow >= levelBelowUntil)) levelBelow = shown; levelBelowUntil = DateTime.UtcNow.AddHours(1); Log("Jobs: \"" + job.Name + "\" shows LEVEL " + shown + " though the bot went by level " + (by == int.MaxValue ? "none" : by.ToString()) + " (the top bar read " + (last != null && last.Level > 0 ? last.Level.ToString() : "no level") + ") - only jobs below level " + shown + " for an hour"); nextJobs = DateTime.UtcNow; nextJobs = DateTime.UtcNow.AddMinutes(5); JobMissed(job, "shows \"" + (card.ButtonText ?? "").Trim() + "\" where DO JOB should be"); int energy = last != null ? last.Energy : job.Cost; int times = Math.Max(1, Math.Min(30, energy / Math.Max(1, job.Cost))); if (S.JobMode == JobMode.Mastery && job.MasteryRank >= 0 && job.MasteryRank < MasteryGold && job.MasteryCur >= 0) int left = 100 - job.MasteryCur; if (left > 0) times = Math.Min(times, left); int rankBefore = job.MasteryRank; int done = game.DoJob(card, times, Catalog); if (done > 0) jobMisses.Remove(job.Name); else if (card.Button != ButtonState.Dim) JobMissed(job, "couldn't be done (its DO JOB didn't take)"); if (done > 0 && last != null && last.Energy >= 0) last.Energy = Math.Max(0, last.Energy - done * job.Cost); if (done > 0) energyRegen.Forget(); string before = job.MasteryLabel, label = null; for (int look = 0; look < 3; look++) if (look > 0) Sleep(700); using (var f = game.Capture()) var fresh = game.ReadJobCards(f, Catalog, false).Where(c => c.Info == job).OrderBy(c => Math.Abs(c.NameY - card.NameY)).FirstOrDefault(); if (fresh != null) card = fresh; label = Game.ReadMasteryText(f, card); Game.ApplyMastery(job, label, f, card); if (done == 0 || job.MasteryLabel != before || job.MasteryRank >= 3) break; Log(string.Format("Did \"{0}\" x{1}, mastery now {2}", job.Name, done, job.MasteryLabel)); if (job.MasteryLabel == before && job.MasteryRank < 3) Log(" (mastery label read as \"" + label + "\")"); if (!Game.IsMasteryLabel(label)) SnapshotOnce("mastery label " + Game.MasteryWording(label)); if (S.JobMode == JobMode.Mastery && job.MasteryRank >= MasteryGold && rankBefore < MasteryGold) Log("Finished the mastery of \"" + job.Name + "\" - moving to the next job"); nextJobs = DateTime.UtcNow.AddSeconds(done > 0 ? 2 : 30); DateTime helpsAt = nextHeists; bool helpsDue = DateTime.UtcNow >= helpsAt; nextHeists = DateTime.UtcNow.AddMinutes(3); if (!game.OpenTab(Tab.Heists)) if (!helpsDue) { nextHeists = helpsAt; nextHeistStart = DateTime.UtcNow.AddMinutes(3); } if (S.StartHeists && DateTime.UtcNow >= nextHeistStart) StartHeist(); if (!helpsDue) { nextHeists = helpsAt; return; } bool lookedAgain = false, reopened = false; int energyLeft = int.MaxValue, favorsLeft = int.MaxValue; for (int i = 0; i < 30; i++) var h = ReadBarsSettled(5000, x => x.Energy >= 0); if (energyLeft != int.MaxValue) h.Energy = energyLeft; else { Log("Couldn't read energy - no heist helps this round"); game.NoteFailure("the energy bar didn't read"); SnapshotOnce("heists energy unreadable"); return; } if (Math.Min(h.Energy, energyLeft) < helpEnergy) return; using (var f = game.Capture()) { favors = game.ReadFavors(f); rows = game.ReadHeists(f); } for (int look = 0; look < 6 && favors < 0; look++) if (look > 0) game.Wait(800); using (var f = game.Capture()) { favors = game.ReadFavors(f); rows = game.ReadHeists(f); } if (favors > 0 && favorsLeft == int.MaxValue) using (var f = game.Capture()) again = game.ReadFavors(f, 1); if (again < 0) using (var f = game.Capture()) again = game.ReadFavors(f); using (var f = game.Capture()) third = game.ReadFavors(f); favors = third >= 0 && (third == favors || third == again) ? third : -1; int cost = rows.Where(r => r.Energy > 0).Select(r => r.Energy).DefaultIfEmpty(-1).Max(); if (cost > 0) helpEnergy = Math.Max(HelpEnergy, cost); else if (h.EnergyMax > 0) helpEnergy = Math.Max(helpEnergy, (int)Math.Ceiling(h.EnergyMax / 20.0)); if (Math.Min(h.Energy, energyLeft) < helpEnergy) return; if (favors >= 0) { favors = Math.Min(favors, favorsLeft); Info("favors", favors.ToString()); } var midnight = DateTime.UtcNow.Date.AddDays(1).AddMinutes(1); nextHeists = DateTime.UtcNow.AddHours(1) < midnight ? DateTime.UtcNow.AddHours(1) : midnight; Log("No heist favors left - checking again in " + (midnight < DateTime.UtcNow.AddHours(1) ? (int)Math.Ceiling((midnight - DateTime.UtcNow).TotalMinutes) + " minutes, after the reset" : "an hour")); using (var f = game.Capture()) on = game.SelectedTab(f); if (on != null && on != Tab.Heists && !reopened) Log("Heists: the " + View.TabLabels[on.Value] + " page was showing, not HEISTS - opening it again"); if (!game.OpenTab(Tab.Heists)) return; Log("Couldn't read how many favors are left - not helping this time"); game.NoteFailure("the favors left didn't read"); game.Snapshot("heist favors unread", 60); nextHeists = DateTime.UtcNow.AddMinutes(15); double perEnergy = JobXpPerEnergy(); var best = rows.Where(r => r.CanHelp && S.WouldHelp(r.YourFamily, r.XpPerHelp, perEnergy * HelpCost(r))) .OrderByDescending(r => r.YourFamily).ThenByDescending(r => r.XpPerHelp).FirstOrDefault(); if (i == 0) { nextHeists = DateTime.UtcNow.AddMinutes(10); return; } if (Math.Min(h.Energy, energyLeft) < Math.Max(helpEnergy, best.Energy)) return; string toast = game.ReadToast(900, t => Game.Refused(t) || Parse.Has(t, "HEIST") || Parse.Has(t, "HELP") || Parse.Has(t, "FAVOR")); Log("Heist help refused: " + toast); nextHeists = DateTime.UtcNow.AddMinutes(10); if (toast != null) SnapshotOnce("heist help notification"); energyLeft = Math.Min(h.Energy, energyLeft) - Math.Max(helpEnergy, best.Energy); if (last != null && last.Energy >= 0) last.Energy = Math.Max(0, Math.Min(last.Energy, energyLeft)); Log(string.Format("Helped \"{0}\" ({1}){2}", best.Name, best.XpPerHelp >= 0 ? "+" + best.XpPerHelp + " XP" : "XP not read", best.YourFamily ? " - a heist of your family" : "")); nextHeists = DateTime.UtcNow.AddSeconds(30); const int HelpEnergy = 6; int helpEnergy = HelpEnergy; const int HeistsPerDay = 2; static string HeistDay() { return DateTime.UtcNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } internal static bool HeistsUsedUp(string toast) toast = toast == null ? null : toast.Replace("alreadv", "already").Replace("todav", "today"); return toast != null && (Parse.Has(toast, "ALREADY STARTED") || Parse.Has(toast, "ALREADY ATTEMPTED")) && Parse.Has(toast, "HEISTS TODAY"); internal static TimeSpan? HeistsResetIn(string words) var m = System.Text.RegularExpressions.Regex.Match(words ?? "", @"RESET\s*IN\s*(\d{1,2})\s*[:.]\s*(\d{2})", System.Text.RegularExpressions.RegexOptions.IgnoreCase); if (!m.Success) return null; int hh = int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture), mm = int.Parse(m.Groups[2].Value, CultureInfo.InvariantCulture); if (hh > 24 || mm > 59) return null; return new TimeSpan(hh, mm, 0); bool HeistsUsedUpSaid(string words) if (!HeistsUsedUp(words)) return false; S.HeistsToday = HeistDay() + "|" + HeistsPerDay; var left = HeistsResetIn(words); nextHeistStart = left.HasValue ? DateTime.UtcNow.Add(left.Value).AddMinutes(2) : DateTime.UtcNow.Date.AddDays(1).AddMinutes(2); SayOnce("heists used " + HeistDay(), "Heists: the game says the day's " + HeistsPerDay + " heists are used (\"" + Cut(words.Trim(), 140) + "\") - the next one " + (left.HasValue ? "in " + (int)left.Value.TotalHours + "h" + left.Value.Minutes.ToString("00") : "after midnight UTC") + ", the energy goes to the jobs"); var p = (S.HeistsToday ?? "").Split('|'); return p.Length == 2 && p[0] == HeistDay() && int.TryParse(p[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out n) && n > 0 ? n : 0; bool HeistEnergyHeld(Header h) return S.Heists && S.StartHeists && heistEnergy > 0 && h.Energy >= 0 && h.Energy < heistEnergy && (h.EnergyMax <= 0 || heistEnergy <= h.EnergyMax); using (var f = game.Capture()) button = game.CollectTheTake(f, out heist, out haul); if (button == null) return; game.PressCollectTheTake(button); for (int look = 0; look < 6 && (look == 0 || still != null); look++) if (look > 0) game.Wait(look < 3 ? 1000 : 2000); using (var f = game.Capture()) still = game.CollectTheTake(f, out h2, out haul2); Log("Heists: COLLECT THE TAKE didn't take" + (heist.Length > 0 ? " on " + heist : "") + " - again next round"); game.Snapshot("heist take not collected", 60); Log("Heists: collected the take" + (heist.Length > 0 ? " of " + heist : "") + (haul.Length > 0 ? " (" + haul + ")" : "")); SnapshotOnce("heist take collected"); DateTime nextHeistDismiss = DateTime.MinValue; void DismissFailedHeist() if (DateTime.UtcNow < nextHeistDismiss) return; using (var f = game.Capture()) button = game.HeistFailedDismiss(f, out heist, out how); if (button == null) return; game.PressHeistDismiss(button); for (int look = 0; look < 3 && (look == 0 || still != null); look++) if (look > 0) game.Wait(1000); using (var f = game.Capture()) still = game.HeistFailedDismiss(f, out h2, out how2); nextHeistDismiss = DateTime.UtcNow.AddMinutes(30); Log("Heists: DISMISS didn't take" + (heist.Length > 0 ? " on " + heist : "") + " - again in 30 minutes"); game.Snapshot("heist dismiss not taken", 60); using (var f = game.Capture()) free = game.NoActiveHeist(f) != null; Log("Heists: dismissed " + (heist.Length > 0 ? heist : "your heist") + (how.Length > 0 ? " (" + how + ", the stake lost)" : "") + (free ? "" : " - the page doesn't say NO ACTIVE HEIST yet")); SnapshotOnce(free ? "heist failed dismissed" : "heist failed dismissed page unread"); internal static string OwnHeistKind(string words) if (string.IsNullOrWhiteSpace(words)) return null; string k = Parse.Key(words).Replace("SUCCESSREWARDS", ""); foreach (var w in new[] { Tuple.Create("CIAIM", "claim"), Tuple.Create("COIIECT", "collect"), Tuple.Create("DISMISS", "dismiss"), Tuple.Create("CIOSE", "close"), Tuple.Create("REMOVE", "remove"), Tuple.Create("RETRY", "retry") }) if (k.Contains(w.Item1)) return w.Item2; if (k.Contains("FAIIED") || k.Contains("SABOTAGED") || k.Contains("EXPIRED") || k.Contains("TIMEUP")) return "failed"; if (k.Contains("SUCCESS") || k.Contains("COMPIETE") || k.Contains("PAYOUT") || k.Contains("WON")) return "won"; if (k.Contains("ENDSIN") || k.Contains("HEIPS")) return "running"; nextHeistStart = DateTime.UtcNow.AddMinutes(10); if (HeistsToday() >= HeistsPerDay) nextHeistStart = DateTime.UtcNow.Date.AddDays(1).AddMinutes(2); using (var f = game.Capture()) { button = game.NoActiveHeist(f); own = game.OwnHeistWords(f); } using (var f = game.Capture()) { button = game.NoActiveHeist(f); own = game.OwnHeistWords(f); } if (HeistsUsedUpSaid(own)) return; string kind = OwnHeistKind(own); if (kind != null && saidOnce.Add("own heist " + kind + " " + DateTime.UtcNow.ToString("yyyyMMddHH"))) Log("Heists: your own heist shows \"" + Cut(own, 200) + "\""); if (kind != "running") SnapshotOnce("heist of yours " + kind); var h = ReadBarsSettled(5000, x => x.Energy >= 0 && x.Cash >= 0); Log("Heists: couldn't read the energy - no heist started (again in 10 minutes)"); SnapshotOnce("heist start energy unread"); if (heistEnergy > 0 && h.Energy < heistEnergy && (h.EnergyMax <= 0 || heistEnergy <= h.EnergyMax)) nextHeistStart = DateTime.UtcNow.AddMinutes(2); game.PressStartAHeist(button); List<HeistOffer> offers = null; for (int look = 0; look < 6 && pick == null; look++) if (look > 0) { game.ScrollPage(0.55, -2); game.Wait(500); } using (var f = game.Capture()) offers = game.ReadHeistOffers(f, out close); if (offers == null) { game.Wait(700); continue; } pick = offers.FirstOrDefault(o => Game.SameHeist(o.Name, S.StartHeist) && (o.Start != null || o.Short)); if (pick == null && offers.Any(o => Game.SameHeist(o.Name, S.StartHeist) && o.Start == null && !o.Short)) pick = offers.Where(o => o.Start != null || o.Short).OrderByDescending(o => o.Level).FirstOrDefault(); using (var f = game.Capture()) now = game.NoActiveHeist(f) == null ? game.OwnHeistWords(f) : ""; if (OwnHeistKind(now) == "running") Log("Heists: one of yours is running after all - no heist started"); Log("Heists: the START A HEIST window didn't show"); game.Snapshot("heist start window not read", 60); nextHeistStart = DateTime.UtcNow.AddMinutes(60); SayOnce("heist pick " + S.StartHeist, "Heists: \"" + S.StartHeist + "\" wasn't found in the START A HEIST window - no heist started (looking again in an hour)"); game.Snapshot("heist pick not found", 60); game.CloseHeistOffers(close); nextHeistStart = DateTime.UtcNow.AddMinutes(60); if (!Game.SameHeist(pick.Name, S.StartHeist)) SayOnce("heist lower " + pick.Name, "Heists: " + S.StartHeist + " is above your level - starting " + pick.Name + " instead"); SayOnce("heist energy unread " + pick.Name, "Heists: couldn't read " + pick.Name + "'s energy in the START A HEIST window - not started (again in 10 minutes)"); game.CloseHeistOffers(close); if (h.Energy < pick.Energy) heistEnergy = pick.Energy; SayOnce("heist energy " + pick.Name, "Heists: saving " + pick.Energy + " energy to start " + pick + " - the jobs wait for it"); game.CloseHeistOffers(close); nextHeistStart = DateTime.UtcNow.AddMinutes(h.EnergyMax > 0 && pick.Energy > h.EnergyMax ? 60 : 2); if (pick.Start == null || pick.Stake > 0 && (h.Cash < 0 || h.Cash < pick.Stake)) game.CloseHeistOffers(close); took = WithdrawFor(pick.Stake, false); SayOnce("heist cash " + pick.Name, "Heists: not enough cash on hand for " + pick + "'s stake - looking again in 30 minutes"); game.CloseHeistOffers(close); nextHeistStart = DateTime.UtcNow.AddMinutes(30); if (!game.OpenTab(Tab.Heists)) return; using (var f = game.Capture()) button = game.NoActiveHeist(f); if (button == null) return; game.PressStartAHeist(button); for (int look = 0; look < 6 && pick == null; look++) if (look > 0) { game.ScrollPage(0.55, -2); game.Wait(500); } using (var f = game.Capture()) offers = game.ReadHeistOffers(f, out close); if (offers != null) pick = offers.FirstOrDefault(o => o.Start != null && Game.SameHeist(o.Name, S.StartHeist)) ?? offers.Where(o => o.Start != null && o.Level <= Math.Max(1, level)).OrderByDescending(o => o.Level).FirstOrDefault(); SayOnce("heist needs " + S.StartHeist, "Heists: " + S.StartHeist + " still shows NEEDS (its stake) after the cash came out - again in 10 minutes"); game.CloseHeistOffers(close); using (var f = game.Capture()) var again = game.ReadHeistOffers(f, out close); var row = again == null ? null : again.FirstOrDefault(o => o.Start != null && Game.SameHeist(o.Name, pick.Name)); if (row == null) { Log("Heists: " + pick.Name + "'s START moved before it was pressed - again in 10 minutes"); game.CloseHeistOffers(close); return; } if (pick.Energy <= 0 || h.Energy < pick.Energy) Log("Heists: " + pick.Name + "'s energy " + (pick.Energy <= 0 ? "didn't read" : "(" + pick.Energy + ") is more than the bar's " + h.Energy) + " before START - again in 10 minutes"); game.CloseHeistOffers(close); game.PressHeistStart(pick); using (var f = game.Capture()) still = game.ReadHeistOffers(f, out close); string toast = still != null ? game.ReadToast(500, t => Parse.Has(t, "HEIST")) : null; for (int look = 0; look < 3 && still != null && !HeistsUsedUp(toast); look++) using (var f = game.Capture()) still = game.ReadHeistOffers(f, out close); using (var f = game.Capture()) none = game.NoActiveHeist(f); if (still == null && none == null) S.HeistsToday = HeistDay() + "|" + (HeistsToday() + 1); if (last != null && last.Energy >= 0) last.Energy = Math.Max(0, Math.Min(last.Energy, h.Energy) - pick.Energy); Log("Heists: started " + pick); SnapshotOnce("heist started"); nextHeistStart = DateTime.UtcNow.AddMinutes(Math.Max(10, pick.Minutes > 0 ? pick.Minutes : 30)); if (toast == null) toast = game.ReadToast(1000, t => Parse.Has(t, "HEIST")); if (still != null) game.CloseHeistOffers(close); Log("Heists: START on " + pick.Name + " didn't start it" + (still != null ? " (the window is still open - the day's 2 heists used?)" : "") + (!string.IsNullOrWhiteSpace(toast) ? " - the game said \"" + Cut(toast.Trim(), 120) + "\"" : "") + " - again in an hour"); game.Snapshot("heist start did nothing", 60); if (still != null) game.CloseHeistOffers(close); nextHeistStart = DateTime.UtcNow.AddMinutes(60); finally { if (took) DepositBack(); } public double JobXpPerEnergy() { return JobXpPerEnergy(last, Catalog); } public double JobXpPerEnergy(Header h, List<JobInfo> catalog) if (S.JobMode == JobMode.OneJob) return OneJobWhy(h, catalog, out one) == null ? one.XpPerEnergy : 0; int maxE = h != null && h.EnergyMax > 0 ? h.EnergyMax : int.MaxValue; foreach (var j in catalog) if (j.TierLevel <= lv && j.Cost > 0 && j.Cost <= maxE && j.Xp > 0) best = Math.Max(best, j.XpPerEnergy); int HelpCost(HeistRow r) { return r.Energy > 0 ? r.Energy : helpEnergy; } if (!FamilyTakes()) { DoFights(100, AboveKept); return; } if (DoFights(100, -1) > 0) givenSinceFights = 0; const int AboveKept = -2; int DoFights(int max, int reserve) nextFights = DateTime.UtcNow.AddSeconds(30); if (!game.OpenTab(Tab.Fight)) return 0; if (game.ProfileShowing() && !game.CloseProfile()) { Log("Fights: a player's profile is open and didn't close - fights wait 5 minutes"); nextFights = DateTime.UtcNow.AddMinutes(5); return 0; } int fought = 0, staminaLeft = -1; bool respectSaid = false; for (int i = 0; i < max; i++) var h = ReadBarsSettled(i == 0 ? 6000 : 5000); bool hospital = false, war; using (var f = game.Capture()) { rows = game.ReadFightRows(f, out war); if (i == 0) hospital = game.Hospitalized(f) && !h.HealthAtLeast(50); } if (hospital && !HealWithItem()) { Hospital(h, fought > 0); return fought; } if (S.RespectCycle && i % 3 == 0 && RespectTurn()) if (!game.OpenTab(Tab.Fight)) { nextFights = DateTime.UtcNow.AddMinutes(2); return fought; } bool scout = S.ScoutFights && !Lowering; Log("Fights: the cash on hand is " + M(h.Cash) + (S.BankAt > 0 ? ", over the " + M(S.BankAt) + " to bank at" : "") + " - banking between attacks"); if (!game.OpenTab(Tab.Fight)) { Log("Fights: the fight list didn't come back after the bank - fights wait 5 minutes"); nextFights = DateTime.UtcNow.AddMinutes(5); return fought; } if (rows.Count == 0 && game.ProfileShowing()) if (!game.CloseProfile()) { Log("Fights: a player's profile is open and didn't close - fights wait 5 minutes"); nextFights = DateTime.UtcNow.AddMinutes(5); return fought; } using (var f = game.Capture()) rows = game.ReadFightRows(f, out war); if (war && S.WarTargetsOnly && !rows.Any(r => r.War) && !rolledUp) game.ScrollPage(0.5, 10); using (var f = game.Capture()) rows = game.ReadFightRows(f, out war); if (war && S.WarTargetsOnly) rows = rows.Where(r => r.War).ToList(); if (h.Stamina < 0 && staminaLeft >= 0) h.Stamina = staminaLeft; if (h.Stamina < 0 && game.RobloxBar && barFixTries < 2) Log("Fights: the stamina bar didn't read and Roblox's own title bar is over the top bar - the bot clears it first"); if (FixRobloxBarNow()) continue; if (h.Stamina < 0) { Log("Couldn't read stamina (" + game.BarLook(1) + ") - no attacks this round"); game.Snapshot("stamina unreadable at fights", 60); game.NoteFailure("the stamina bar didn't read"); break; } if (reserve == AboveKept) reserve = Kept(h.StaminaMax); else if (reserve < 0) reserve = Math.Max(0, h.Stamina - Kept(h.StaminaMax)); if (h.Stamina <= reserve) if (S.FightItems && UseConsumable(true, "the fights") && game.OpenTab(Tab.Fight)) { staminaLeft = -1; continue; } int spare = h.Stamina - reserve; if (scout && (ownAttack < 0 || DateTime.UtcNow - ownPowerAt > TimeSpan.FromMinutes(30))) var own = game.ReadOwnProfile(); bool showing = game.ProfileShowing(); if (own == null && !showing && S.LastAttackPower > 0 && S.LastDefensePower > 0) SayOnce("own profile", "Fights: your profile's numbers didn't read - using the Safehouse's attack " + S.LastAttackPower + " and defense " + S.LastDefensePower); ownAttack = S.LastAttackPower; ownDefense = S.LastDefensePower; ownPowerAt = DateTime.UtcNow; else if (own == null || showing) SayOnce("own profile", "Fights: couldn't read your own attack and defense (your profile) - fights wait 5 minutes (\"Scout players first\" is on)"); nextFights = DateTime.UtcNow.AddMinutes(5); else { ownAttack = own.Attack; ownDefense = own.Defense; ownPowerAt = DateTime.UtcNow; } var now = DateTime.UtcNow; var ready = rows.Where(r => r.Ready && r.StaminaCost <= spare && !(fightSkip.ContainsKey(FightKey(r)) && fightSkip[FightKey(r)] > now) && !Spared(r) && !(scout && BadOdds(ScoutedNow(r))) && (Lowering || Wanted(r) != null || !LeftAlone(r, now))).ToList(); var notHit = ready.Where(r => (r.War && !Program.Whale) || !HitOnList(r)).ToList(); if (notHit.Count == 0 && rows.Count > 0 && !refreshed && !(war && S.WarTargetsOnly)) if (NewFightList(rows, out pressed) || pressed) { i--; continue; } if (notHit.Count > 0) ready = notHit; if (fought == 0 && scout && rows.Any(r => r.Ready) && rows.Where(r => r.Ready).All(r => BadOdds(ScoutedNow(r)))) nextFights = now.AddMinutes(15); SayOnce("all below the odds", "Fights: every player on the list is too strong for \"" + Settings.WinChanceName(S.FightWinChance) + "\" (scouted) - looking again in 15 minutes"); var free = rows.Select(r => FightKey(r)).Where(k => fightSkip.ContainsKey(k) && fightSkip[k] > now).Select(k => fightSkip[k]).ToList(); var wait = rows.Count == 0 ? TimeSpan.FromMinutes(2) : free.Count > 0 ? free.Min() - now : TimeSpan.FromMinutes(5); nextFights = now + TimeSpan.FromSeconds(Math.Max(30, Math.Min(300, wait.TotalSeconds))); SayOnce("no player to attack", "Fights: no player to attack right now - looking again in a few minutes"); var cheap = ready.Where(r => r.StaminaCost == 1).ToList(); if (cheap.Count > 0) ready = cheap; if (scout && scoutsPaid >= 3) var known = ready.Where(r => ScoutedNow(r) != null).ToList(); if (fought == 0) nextFights = now.AddMinutes(5); Log("Fights: 3 players scouted this round and none worth attacking - looking again " + (fought == 0 ? "in 5 minutes" : "next round")); var target = Program.Whale ? TopDown(ready, Lowering).First() : ready.OrderBy(r => Wanted(r) != null ? 0 : 1).ThenByDescending(r => BigPayer(r, now)).ThenByDescending(r => ExpectedRespect(r, now)) .ThenBy(r => scout && ScoutedNow(r) != null ? 0 : 1).ThenBy(r => r.Level < 0 ? 999 : r.Level).First(); else if (Wanted(target) != null) Log("Fights: " + target.Name + (target.Family.Length > 0 ? " (" + target.Family + ")" : "") + " is on your attack-first list (\"" + Wanted(target) + "\") - attacked first"); else if (BigPayer(target, now) > 0) Log("Fights: " + target.Name + " paid " + M(BigPayer(target, now)) + " in the last day (" + memory.Record(target.Name) + ") - attacked first"); else if (!respectSaid && ExpectedRespect(target, now) > 0) Log(string.Format(CultureInfo.InvariantCulture, "Fights: {0} is the best respect bet right now (+{1:0.0} expected)", target.Name, ExpectedRespect(target, now))); if (scout && ScoutedNow(target) == null) bool may = spare >= target.StaminaCost + 1, paid; var p = game.ScoutPlayer(target, may, out paid, out why); if (paid) { scoutsPaid++; staminaLeft = h.Stamina - 1; } if (game.ProfileShowing() && !game.CloseProfile()) Log("Fights: a player's profile stayed open - fights wait 5 minutes"); nextFights = DateTime.UtcNow.AddMinutes(5); if (!may && !paid && why == Game.NoStaminaToScout) { if (fought == 0) nextFights = DateTime.UtcNow.AddMinutes(1); break; } fightSkip[FightKey(target)] = DateTime.UtcNow.AddMinutes(why == Game.NoProfile ? 6 * 60 : 30); Log("Fights: couldn't scout a " + (target.Level > 0 ? "level " + target.Level + " " : "") + "player (" + why + ") - another one"); var s = scouted[FightKey(target)] = new Scouted { Attack = p.Attack, Defense = p.Defense, At = DateTime.UtcNow }; Log(string.Format(CultureInfo.InvariantCulture, "Fights: scouted a {0}player{1} - attack {2:N0}, defense {3:N0} (yours {4:N0} attack, {5:N0} defense): {6}% to win, {7}", target.Level > 0 ? "level " + target.Level + " " : "", paid ? "" : " (scouted before)", s.Attack, s.Defense, ownAttack, ownDefense, WinChance(ownAttack, s.Defense), BadOdds(s) ? "left alone, you asked for \"" + Settings.WinChanceName(S.FightWinChance) + "\"" : "attacking")); if (!game.Attack(target)) Log("Fights: the list kept changing under the ATTACK buttons - fights wait 2 minutes"); game.Snapshot("fight attack not there", 60); nextFights = DateTime.UtcNow.AddMinutes(2); Log("Fights: the list changed (no ATTACK where it was) - reading it again"); string result = game.FinishFight(15000); using (var f = game.Capture()) { banner = game.Hospitalized(f); inHospital = Game.TopRightYouHospitalized(f); } if (!inHospital) inHospital = Game.YouHospitalized(game.LastFightRefusal) || Game.YouHospitalized(game.LastFightToast); if (!inHospital && game.AfterAttack != null) using (var after = new Frame((System.Drawing.Bitmap)game.AfterAttack.Clone())) inHospital = Game.TopRightYouHospitalized(after); if (!inHospital && banner && !h.HealthAtLeast(50)) inHospital = true; else if (!inHospital && banner) game.Snapshot("hospital banner at good health", 60, failure: false); if (HealWithItem() && game.OpenTab(Tab.Fight)) continue; string why = game.LastFightRefusal; if (why == null && game.AfterAttack != null) using (var after = new Frame((System.Drawing.Bitmap)game.AfterAttack.Clone())) why = Game.TopRightHospitalized(after); if (Game.TargetHospitalized(why)) fightSkip[FightKey(target)] = DateTime.UtcNow.AddMinutes(10); SayOnce("target in hospital", "Fights: a player you beat is in the hospital - the bot attacks another one"); if (Game.DailyLimit(why)) { DailyLimit(target); continue; } int shield = Game.ShieldSeconds(why); if (shield == 0) shield = 3600; fightSkip[FightKey(target)] = DateTime.UtcNow.AddSeconds(Math.Min(shield, 24 * 3600) + 60); Log("Fights: " + (target.Name.Length > 0 ? target.Name : "a level " + target.Level + " player") + " is shielded for another " + SpanText(shield) + " - left alone until then"); fightSkip[FightKey(target)] = DateTime.UtcNow.AddMinutes(30); string said = game.LastFightToast != null ? " (the game said: " + game.LastFightToast + ")" : ""; Log("Fights: the game closed the fight window with " + (target.Name.Length > 0 ? target.Name : "a level " + target.Level + " player") + " without a fight" + said + " - that player waits 30 minutes"); if (firstSeen.Add("fight called off")) game.Snapshot("fight called off", 0, true, picture: game.FightGone); Log("Fights: the game called off 3 fights in a row - fights wait 5 minutes"); game.Snapshot("fights called off", 60, failure: false, picture: game.FightGone); nextFights = DateTime.UtcNow.AddMinutes(5); if (game.FightWindowStuck) Log("Fights: the fight window stayed open with no result - fights wait 2 minutes"); game.Snapshot("fight window stuck", 60); nextFights = DateTime.UtcNow.AddMinutes(2); why = why ?? game.LastFightToast; fightSkip[FightKey(target)] = DateTime.UtcNow.AddMinutes(2); game.Snapshot("attack did not start", 60, failure: false, picture: game.AfterAttack); Log("The attack didn't start twice - fights paused for 5 minutes" + (why != null ? " (the game said: " + why + ")" : "")); game.Snapshot("attack did not start", 60); nextFights = DateTime.UtcNow.AddMinutes(5); game.ForgetAfterAttack(); hitOnList.Add(FightKey(target)); staminaLeft = h.Stamina - target.StaminaCost; if (result.StartsWith("VICTORY")) Count.Wins++; else if (result.StartsWith("DEFEAT")) Count.Losses++; if (target.War) Log(string.Format("Fight vs war target {0}: {1}", target.Name.Length > 0 ? target.Name : "#" + (target.Index + 1), result)); else Log(string.Format("Fight vs level {0} player: {1}", target.Level, result)); bool won = result.StartsWith("VICTORY"); double took = won && target.Name.Length > 0 && (S.WhaleHunt || S.RememberPlayers) ? WinTook(h.Cash) : -1; if (Lowering) Won(target, result, took); else Remember(target, won, result, took); if (Lowering && won && !(S.WhaleHunt && took >= S.WhaleCash)) fightSkip[FightKey(target)] = DateTime.UtcNow.AddHours(6); string summary = memory.RespectSummary(DateTime.UtcNow); if (!summary.StartsWith("no ")) SayOnce("respect " + DateTime.UtcNow.ToString("yyyyMMddHH"), "Fights: " + summary); if (S.WhaleHunt && S.WhaleHitsNow > 1 && won && target.Name.Length > 0 && max > 1 && took >= S.WhaleCash) fought += HitAgain(target, took, reserve, ref staminaLeft); double WinTook(double cashBefore) if (game.LastFightCash >= 0) return game.LastFightCash; if (game.LastFightPending) return -1; var after = ReadBarsSettled(2000, x => x.Cash >= 0); return cashBefore >= 0 && after.Cash >= 0 ? Math.Max(0, after.Cash - cashBefore) : -1; int HitAgain(FightRow target, double took, int reserve, ref int staminaLeft) string name = target.Name; int hits = S.WhaleHitsNow; Log("Fights: the win on " + name + " took " + M(took) + " - banking it and hitting again (up to " + hits + " hits, while each takes " + M(S.WhaleCash) + " or more)"); for (int hit = 2; hit <= hits; hit++) if (S.Bank && Can(Tab.Bank)) DoBank(); if (!game.OpenTab(Tab.Fight)) return more; Game.AttackEntry e = null; game.AttackHistoryToTop(); int end = Environment.TickCount + 60000, missing = Environment.TickCount + 25000; List<Game.AttackEntry> read = null; using (var f = game.Capture()) read = game.ReadAttackHistory(f); e = read.FirstOrDefault(x => Game.SamePlayer(x.Name, name)); if (e == null && read.Count > 0 && Game.CloseName(read[0].Name, name)) e = read[0]; if ((e != null && (e.Again != null || e.Limit)) || end - Environment.TickCount <= 0) break; if (e == null && missing - Environment.TickCount <= 0) break; if (e == null) game.AttackHistoryToTop(); if (e != null && e.Limit && e.Again == null) { DailyLimit(target); break; } Log("Fights: " + name + " isn't in the attack history (" + (read == null || read.Count == 0 ? "no entry of yours read" : "read: " + string.Join(", ", read.Select(x => "'" + x.Name + "'"))) + ") - on with the list"); game.Snapshot("attack history entry not found", 60); if (e.Again == null) { Log("Fights: no ATTACK AGAIN for " + name + " within a minute - on with the list"); break; } var h = ReadBarsSettled(5000); if (h.Stamina < 0 || h.Stamina - reserve < target.StaminaCost) break; string result = game.FinishFight(15000); using (var f = game.Capture()) inHospital = game.Hospitalized(f) && !h.HealthAtLeast(50); if (inHospital) { Hospital(h, true); break; } if (Game.DailyLimit(game.LastFightRefusal)) { DailyLimit(target); break; } Log("Fights: hitting " + name + " again didn't start" + (game.FightCalledOff ? " (the game closed the fight window without a fight)" : "") + (game.LastFightRefusal != null ? " (the game said: " + game.LastFightRefusal + ")" : "") + " - on with the list"); if (game.FightWindowStuck) { game.Snapshot("fight window stuck", 60); nextFights = DateTime.UtcNow.AddMinutes(2); } staminaLeft = h.Stamina - target.StaminaCost; if (result.StartsWith("VICTORY")) Count.Wins++; else if (result.StartsWith("DEFEAT")) Count.Losses++; double got = result.StartsWith("VICTORY") ? WinTook(h.Cash) : -1; if (Lowering) Won(target, result, got); else Remember(target, result.StartsWith("VICTORY"), result, got); Log(string.Format("Fight vs {0} again (hit {1} of {2}): {3}{4}", name, hit, hits, result, got >= 0 ? " - took " + M(got) : "")); if (got < S.WhaleCash) break; if (S.Bank && Can(Tab.Bank)) DoBank(); void DailyLimit(FightRow target) fightSkip[FightKey(target)] = DateTime.UtcNow.AddHours(3); SayOnce("daily limit " + FightKey(target) + DateTime.UtcNow.ToString("yyyyMMdd"), "Fights: the game's daily limit on " + (target.Name.Length > 0 ? target.Name : "a player") + " is reached - another one"); public FightMemory Memory { get { return memory; } } void Won(FightRow r, string result, double took) if (result.StartsWith("VICTORY")) if (took < 0) took = game.LastFightCash >= 0 ? game.LastFightCash : game.LastFightPending ? -1 : Game.LastMoney(result); if (took > 0) Count.FightCash += took; void Remember(FightRow r, bool won, string result, double took) var roll = new FightMemory.Rolls { OwnAttack = game.LastOwnAttack, TheirDefense = game.LastOtherDefense, OwnRoll = game.LastOwnRoll, TheirRoll = game.LastOtherRoll }; if (roll.OwnAttack > 0 && roll.TheirDefense > 0) Log(string.Format(CultureInfo.InvariantCulture, "Fights: the fight window shows your attack was {0:N0} and {1}'s defense {2:N0}{3}", roll.OwnAttack, r.Name.Length > 0 ? r.Name : "the player", roll.TheirDefense, roll.OwnRoll > 0 && roll.TheirRoll > 0 ? string.Format(CultureInfo.InvariantCulture, " - rolled {0:N0} against {1:N0}", roll.OwnRoll, roll.TheirRoll) : "")); if (!S.RememberPlayers || r.Name.Length < 3 || r.War || !(won || result.StartsWith("DEFEAT"))) return; memory.Add(r.Name, won, took, Parse.Signed(result, "Respect"), sc != null ? sc.Defense : -1, roll, DateTime.UtcNow); double ExpectedRespect(FightRow r, DateTime now) if (!S.ScoutFights || ownAttack <= 0 || memory == null) return 0; double p = WinChance(ownAttack, s.Defense) / 100.0; return p * AverageRespect(r.Name, now, true, 25) + (1 - p) * AverageRespect(r.Name, now, false, -15); double AverageRespect(string name, DateTime now, bool won, double fallback) double v = memory.AverageRespect(name, now, won); if (!double.IsNaN(v)) return v; v = memory.GlobalAverageRespect(now, won); return double.IsNaN(v) ? fallback : v; string MemoryFile(string account) string shared = Path.Combine(dir, "fight memory.txt"); if (string.IsNullOrEmpty(account)) return shared; string mine = Accounts.Fights(dir, account); Directory.CreateDirectory(Accounts.Folder(dir, account)); if (!File.Exists(mine) && File.Exists(shared)) File.Copy(shared, mine); catch (Exception) { return shared; } double BigPayer(FightRow r, DateTime now) if (!S.RememberPlayers || r.Name.Length < 3 || r.War) return -1; double big = memory.BigWin(r.Name, now); return big >= S.WhaleCash ? big : -1; bool LeftAlone(FightRow r, DateTime now) if (!S.RememberPlayers || r.Name.Length < 3 || r.War) return false; var until = memory.LeftAloneUntil(r.Name, now); if (until <= now) return false; SayOnce("left alone " + FightKey(r) + until.ToString("yyyyMMddHH"), "Fights: " + r.Name + " beat you twice in a row - left alone until " + until.ToLocalTime().ToString("ddd HH:mm", CultureInfo.InvariantCulture) + ", then tried again"); string Wanted(FightRow r) { return Program.Whale ? null : AttackFirst(S.AttackFirst, r); } internal static string AttackFirst(string list, FightRow r) string e = NeverAttack(list, r); return string.IsNullOrEmpty(e) ? null : e; string e = NeverAttack(S.NeverAttack, r); if (e == null) return false; SayOnce("never attack " + FightKey(r), "Fights: " + (r.Name.Length > 0 ? r.Name : "a player whose name didn't read") + (r.Family.Length > 0 ? " (" + r.Family + ")" : "") + " left alone - " + (r.Name.Length > 0 ? "\"" + e + "\" is on your never-attack list" : "your never-attack list can't be checked without the name")); internal static string NeverAttack(string list, FightRow r) var entries = NeverAttackEntries(list); if (entries.Count == 0) return null; string name = Parse.Key(Game.SplitPrestige(r.Name, out badge)), fam = Parse.Key(r.Family); foreach (var e in entries) if (k.Length == 0) continue; if (name.Length > 0 && (name == k || (k.Length >= 6 && View.Distance(name, k) <= 1))) return e; if (fam.Length > 0 && FamilyHolds(fam, k)) return e; if (name.Length > 0) return null; if (fam.Length > 0 && entries.All(e => !RobloxNameLike(e))) return null; static bool FamilyHolds(string fam, string k) if (fam == k || (k.Length >= 4 && fam.Contains(k))) return true; for (int i = 0; i + k.Length - 1 <= fam.Length; i++) for (int n = k.Length - 1; n <= k.Length + 1 && i + n <= fam.Length; n++) if (View.Distance(fam.Substring(i, n), k) <= 1) return true; static bool RobloxNameLike(string e) { return Regex.IsMatch((e ?? "").Trim(), @"^[A-Za-z0-9_]{3,20}$"); } internal static List<string> NeverAttackEntries(string list) return (list ?? "").Split(new[] { ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()) .Where(x => x.Length > 0).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); void SayWar(bool war, int rows) if (war == atWar || (!war && rows == 0)) return; if (!war) { Log("Fights: no war on the Fight page any more - the whole list again"); return; } Log(S.WarTargetsOnly ? "Fights: your family is at war - only war targets are attacked" : "Fights: your family is at war (\"Only attack war targets\" is off: the whole list)"); SnapshotOnce("fights at war"); readonly Dictionary<string, DateTime> fightSkip = new Dictionary<string, DateTime>(); readonly HashSet<string> hitOnList = new HashSet<string>(); DateTime refreshBrokenUntil = DateTime.MinValue; bool HitOnList(FightRow r) { return HitBefore(hitOnList, r); } internal static IEnumerable<FightRow> TopDown(IEnumerable<FightRow> rows, bool lowering) if (!lowering) return rows.OrderBy(r => r.Index); return rows.OrderBy(r => r.NoFamily ? 1 : 0).ThenBy(r => r.Level < 0 ? 1 : 0).ThenByDescending(r => r.Level).ThenBy(r => r.Index); internal static bool HitBefore(ICollection<string> hit, FightRow r) if (hit.Contains(k)) return true; return r.Name.Length > 0 && k.Length >= 6 && hit.Any(x => !x.StartsWith("#") && x.Length >= 6 && View.Distance(x, k) <= 1); DateTime lastRefreshPress = DateTime.MinValue; bool NewFightList(List<FightRow> before, out bool pressed) if (DateTime.UtcNow < refreshBrokenUntil) return false; var was = new HashSet<string>(before.Where(r => r.Name.Length > 0).Select(FightKey)); if (Program.Whale) was.UnionWith(hitOnList); var since = DateTime.UtcNow - lastRefreshPress; if (since < TimeSpan.FromSeconds(10.5)) game.Wait((int)(TimeSpan.FromSeconds(10.5) - since).TotalMilliseconds + 1); if (!game.RefreshFightList()) SayOnce("refresh not found", "Fights: couldn't find the list's round arrow (refresh) - the same players are attacked again for now"); game.Snapshot("fight refresh not found", 60, failure: false); lastRefreshPress = DateTime.UtcNow; int start = Environment.TickCount; using (var f = game.Capture()) { rows = game.ReadFightRows(f); loading = rows.Count == 0 && game.FightListLoading(f); } int fresh = rows.Count(r => r.Name.Length > 0 && !HitBefore(was, r)); if (!Program.Whale || hitOnList.Count >= 64) hitOnList.Clear(); Log("Fights: everyone on the list had a hit - pressed refresh, " + fresh + " new players"); SnapshotOnce("fight list refreshed"); int ms = Environment.TickCount - start; if (ms >= 30000 || (!loading && ms >= 3000)) break; SayOnce("refresh changed nothing", "Fights: refresh brought no new players - the same ones are attacked again for now"); game.Snapshot("fight refresh changed nothing", 60, failure: false); if (Program.Whale) hitOnList.Clear(); else refreshBrokenUntil = DateTime.UtcNow.AddMinutes(2); sealed class Scouted { public int Attack, Defense; public DateTime At; } readonly Dictionary<string, Scouted> scouted = new Dictionary<string, Scouted>(); int ownAttack = -1, ownDefense = -1; DateTime ownPowerAt = DateTime.MinValue; Scouted ScoutedNow(FightRow r) return scouted.TryGetValue(FightKey(r), out s) && DateTime.UtcNow - s.At < TimeSpan.FromHours(1) ? s : null; return s != null && ownAttack > 0 && WinChance(ownAttack, s.Defense) < Settings.WinChanceStep(S.FightWinChance); internal const double FightRoll = 0.20; internal static int WinChance(int attack, int defense) if (attack <= 0 || defense <= 0) return 0; double x1 = attack * (1 - FightRoll), x2 = attack * (1 + FightRoll); double y1 = defense * (1 - FightRoll), y2 = defense * (1 + FightRoll); double lo = Math.Max(x1, y1), hi = Math.Min(x2, y2); double wins = Math.Max(0, x2 - Math.Max(x1, y2)); if (hi > lo) wins += ((hi - y1) * (hi - y1) - (lo - y1) * (lo - y1)) / (2 * (y2 - y1)); return (int)Math.Round(100 * Math.Max(0, Math.Min(1, wins / (x2 - x1)))); public bool CollectFights; static string FightKey(FightRow r) { return r.Name.Length > 0 ? Parse.Key(r.Name) : "#" + r.Index + " L" + r.Level; } void Hospital(Header h = null, bool afterFights = false) string health = h != null && h.Health >= 0 && h.HealthMax > 0 ? " (health " + h.Health + "/" + h.HealthMax + ")" : ""; if (afterFights || DateTime.UtcNow - hospitalSaid > TimeSpan.FromMinutes(30)) Log("You're in the hospital" + health + " - player fights wait until your health is back, next look in 10 minutes"); hospitalSaid = DateTime.UtcNow; nextFights = DateTime.UtcNow.AddMinutes(10); DateTime hospitalSaid = DateTime.MinValue; string PointsStopKey() { return S.Account + "|" + S.PointsStat + "|" + S.PointsStop(S.PointsStat); } if (bankFee >= 0) return bankFee; string name = S.Account.Length > 0 ? S.Account : last != null ? (last.Name ?? "").Trim() : ""; if (name.Length == 0 || feeLookedUp == name) return bankFee; var p = Profile.Load(Profile.FileFor(dir, name)); if (p != null && p.BankFee >= 0) bankFee = p.BankFee; bool CanWithdraw() { return S.Bank && Can(Tab.Bank); } double Spendable(Header h) { return h.Cash < 0 ? h.Cash : h.Cash + (CanWithdraw() && h.Banked > 0 ? h.Banked : 0); } bool WithdrawFor(double need, bool bigBuy) { double floor; return WithdrawKeeping(need, bigBuy, need, 0, out floor); } bool WithdrawUpTo(double need, bool bigBuy, double upTo) { double floor; return WithdrawKeeping(need, bigBuy, upTo, 0, out floor); } bool WithdrawKeeping(double need, bool bigBuy, double upTo, double keep, out double floor) floor = Math.Max(0, keep); outOnHand = outRead = -1; if (!CanWithdraw() || need <= 0) return false; if (!bigBuy && BankFee() != 0) return false; if (cash < 0 || cash >= need) return false; using (var f = game.Capture()) banked = game.ReadHeader(f).Banked; if (!WithdrawPlan(need, upTo, cash, banked, keep, out amount, out all, out after)) return false; nextBank = DateTime.UtcNow; string typed = game.WithdrawAmount(amount, out why, out seen); if (typed != null && seen) Log("Bank: took " + M(amount) + " out for a purchase with the typed amount (" + typed + ")"); SnapshotOnce("bank withdraw amount"); outOnHand = cash + amount; Log("Bank: WITHDRAW pressed for " + M(amount) + ", but " + (typed == Game.BankUnread ? "the top bar didn't read" : "the top bar shows no cash coming out") + " - no purchase this time"); game.Snapshot("bank withdraw amount unconfirmed", 60); SayOnce("bank amount " + why.Split('(', '"')[0], "Bank: the typed amount wasn't used (" + why + ") - no purchase this time (the bank is never emptied for one)"); game.Snapshot("bank amount not used", 60); string msg = game.WithdrawAll(); if (msg == null) return false; if (msg == Game.BankUnchanged) Log("Bank: WITHDRAW ALL pressed, but the top bar shows no cash coming out"); game.Snapshot("bank withdraw did nothing", 60); Log("Bank: took the cash out for a purchase (" + msg.Trim() + ")"); SnapshotOnce("bank withdraw"); outOnHand = outRead = cash + banked; internal static bool WithdrawPlan(double need, double upTo, double cash, double banked, double keep, out long amount, out bool all, out double floor) floor = Math.Max(0, keep); if (need <= 0 || cash < 0 || cash >= need) return false; double target = WithdrawTarget(need, upTo, cash, banked, keep); if (target <= 0) return false; amount = AmountToWithdraw(target, cash, banked); if (amount <= 0 && upTo > need) amount = AmountToWithdraw(need, cash, banked); floor = KeepOnHand(keep, banked - amount); if (floor > 0 && FreeCash(cash, banked, keep) < need * 1.01) { amount = 0; floor = Math.Max(0, keep); return false; } if (cash + banked < (need + Math.Max(0, keep)) * 1.005) return false; internal static double KeepOnHand(double keep, double banked) { return Math.Max(0, keep - Math.Max(0, banked)); } internal static double FreeCash(double cash, double banked, double keep) { return cash < 0 ? -1 : cash + Math.Max(0, banked) - Math.Max(0, keep); } internal static double WithdrawTarget(double need, double upTo, double cash, double banked, double keep) if (need <= 0 || cash < 0 || banked < 0) return 0; double free = FreeCash(cash, banked, keep); if (free < need) return 0; return Math.Min(Math.Max(need, upTo), free); double KeepFloor(double keep, bool anyFee) { double floor = KeepFloorRead(keep, anyFee); return floor < 0 ? keep : floor; } double KeepFloorRead(double keep, bool anyFee) if (!CanWithdraw() || (!anyFee && BankFee() != 0)) return keep; using (var f = game.Capture()) banked = game.ReadHeader(f).Banked; return banked < 0 ? -1 : KeepOnHand(keep, banked); internal static long AmountToWithdraw(double target, double cash, double banked) if (target <= 0 || cash < 0 || banked <= 0) return 0; double x = target - cash + target * 0.03; double step = Math.Pow(10, Math.Max(0, Math.Floor(Math.Log10(x)) - 2)); x = Math.Ceiling(x / step) * step; if (x >= banked * 0.95 || x > 9e14) return 0; void DepositBack(double spent = -1) double onHand = outOnHand, read = outRead; outOnHand = outRead = -1; if (BankFee() != 0) { nextBank = DateTime.UtcNow; return; } double back = game.DepositCashBefore, gone = lastBank != bankedAt ? OnHandGone(onHand, read, spent, back) : 0; Log("Bank: " + M(gone) + " of the cash taken out is gone - " + M(back) + " went back in, " + M(onHand - spent) + " was left after the purchase (an attack while it was on hand?)"); double outOnHand = -1, outRead = -1; internal static double OnHandGone(double onHand, double read, double spent, double back) if (onHand <= 0 || spent < 0 || back < 0) return 0; double expect = onHand - spent; if (expect <= 0) return 0; double gone = expect - back; return gone > Math.Max(expect * 0.01, (Math.Max(0, read) + back) * 0.01) ? gone : 0; static bool BigBuy(string task) { return task == "properties" || task == "safehouse upgrade"; } void BankNext(List<string> due, int i) if (!cashOut || !S.Bank || !Can(Tab.Bank)) return; if (i + 1 < due.Count && (due[i + 1] == "bank" || (BankFee() != 0 && BigBuy(due[i + 1])))) return; int at = due.IndexOf("bank", i + 1); if (at < 0 && due.IndexOf("bank") >= 0) return; if (at >= 0) due.RemoveAt(at); due.Insert(i + 1, "bank"); const int BankMinutes = 1; bool BankAtMidFights(Header h) if (!S.Bank || h.Cash <= 0 || !Can(Tab.Bank) || AutoRollHoldsBank()) return false; var since = DateTime.UtcNow - lastBank; return S.BankAt > 0 ? h.Cash >= S.BankAt && since >= TimeSpan.FromSeconds(30) : since >= TimeSpan.FromMinutes(BankMinutes); internal bool BankJustRan(DateTime now) { return !cashOut && now - lastBank < TimeSpan.FromSeconds(15); } SayOnce("bank auto roll " + autoRun.StartedAt.Ticks, "Bank: waits while the crew's Auto Roll rolls on the cash on hand"); nextBank = DateTime.UtcNow.AddMinutes(1); string msg = game.DepositAll(out fee); if (fee >= 0) bankFee = fee; else if (msg != null && Game.FeeIn(msg) >= 0) bankFee = Game.FeeIn(msg); lastBank = DateTime.UtcNow; nextBank = DateTime.UtcNow.AddMinutes(BankMinutes); Log("Bank: each deposit costs " + fee + "% without the No Bank Fees pass"); if (msg == null) { if (game.DepositCashBefore == 0) cashOut = false; return; } if (msg == Game.BankHidden) if (!bankHiddenSaid) Log("Bank: DEPOSIT ALL pressed " + msg); if (msg == Game.BankUnchanged || msg == Game.BankUnread) Log("Bank: DEPOSIT ALL pressed " + msg); if (++bankUnconfirmed >= 2) game.Snapshot("bank deposit unconfirmed", 60); else nextBank = DateTime.UtcNow; if (NothingToDeposit(msg)) { Log("Bank: nothing to deposit"); return; } bool toast = !msg.StartsWith("the notification didn't show"); string line = toast ? TidyDeposit(msg) : "deposited (" + msg + ")"; if (toast && !line.StartsWith("deposited $") && game.DepositCashBefore > 0) line += " - about " + M(game.DepositCashBefore) + " (the cash on hand before it)"; public static bool NothingToDeposit(string msg) return Regex.IsMatch(msg ?? "", @"n\s?o\s?t\s?h\s?i\s?n\s?g\s*t\s?o\s*d\s?e\s?p\s?o\s?s", RegexOptions.IgnoreCase); public static string TidyDeposit(string msg) var fee = Regex.Match(msg, @"\(\s*(no\s*fee|\$\s?[\d,.]+\s?[KMBT]?\s*fee)\s*\)", RegexOptions.IgnoreCase); string feeText = fee.Success ? " (" + Regex.Replace(fee.Groups[1].Value, @"\s+", " ").ToLowerInvariant().Replace("$ ", "$") + ")" : ""; var m = Regex.Match(fee.Success ? msg.Remove(fee.Index, fee.Length) : msg, @"\$\s?\d(?:[\d,.]|\s(?=[\d,.]*\d))*(?:\s?[KMBT]\b)?", RegexOptions.IgnoreCase); string amount = m.Success ? m.Value.Replace(" ", "") : ""; amount = amount.TrimEnd('.'); if (amount.EndsWith(",")) amount = ""; if (amount.Length == 0) return Regex.IsMatch(msg, @"dep\w*s\w*ted|ba\s?n\s?k\s?e\s?d", RegexOptions.IgnoreCase) ? "deposited" + feeText : msg.Trim(); return "deposited " + amount + feeText; void NoteBankFee(string msg) string name = last != null ? (last.Name ?? "").Trim() : ""; int fee = Game.FeeIn(msg); if (name.Length == 0 || fee < 0 || feeNotedFor == name) return; string file = Profile.FileFor(dir, name); var p = Profile.Load(file); if (p == null || p.BankFee >= 0) return; p.BankFeeFrom = "a deposit"; try { p.Save(file); } catch (Exception) { return; } Log(fee == 0 ? "(The deposit says there's no bank fee: noted in your game check)" : "(The deposit says each one costs " + fee + "%: noted in your game check)"); public event Action<string> AccountChanged; DateTime accountFirstVote; readonly object accountLock = new object(); bool NoteAccount(string name) if (!Accounts.Plausible(name)) return false; name = Accounts.Known(dir, name.Trim()); if (!IsOther(name, whose)) accountSeen = null; accountVotes = 0; if (S.Account.Length == 0) Adopt(name); var now = DateTime.UtcNow; bool known = Directory.Exists(Accounts.Folder(dir, name)); bool strict = !known || Accounts.Same(name, whose); bool same = accountSeen != null && (strict ? name == accountSeen : Accounts.Same(name, accountSeen)); if (!same) { accountSeen = name; accountVotes = 1; accountFirstVote = now; return true; } if (accountVotes < (strict ? 8 : 3) || now - accountFirstVote < TimeSpan.FromSeconds(strict ? 60 : 10)) return true; accountSeen = null; accountVotes = 0; string again = Accounts.Known(dir, (NameAgain ?? ReadNameAgain)()); if (!Accounts.Alike(again, name)) SayOnce("account again " + name + "|" + again, "Account: the top bar read \"" + name + "\" for a minute, but once more at another zoom \"" + (again ?? "nothing") + "\" - not taken as another account"); if (S.Account.Length == 0) S.Account = whose; if (!known && Accounts.Part(name, S.Account)) RenameAccount(name); else SwitchAccount(name); bool OtherAccount(string name) if (!Accounts.Plausible(name)) return false; lock (accountLock) return IsOther(Accounts.Known(dir, name.Trim()), Whose()); if (!ownerLooked && S.Account.Length == 0) { ownerLooked = true; ownerHint = Accounts.OwnerHint(dir); } return S.Account.Length > 0 ? S.Account : ownerHint ?? ""; bool IsOther(string name, string whose) if (whose.Length > 0 && Accounts.Alike(name, whose)) return false; bool own = whose.Length > 0 && !string.Equals(Profile.Key(name), Profile.Key(whose), StringComparison.OrdinalIgnoreCase) && Directory.Exists(Accounts.Folder(dir, name)); return whose.Length > 0 && (own || !Accounts.Same(name, whose)); internal Func<string> NameAgain = null; if (game == null) return null; using (var f = game.Capture()) return game.NameAgain(f); string ini = Accounts.Ini(dir, name), jobs = Accounts.Jobs(dir, name); bool back = File.Exists(ini); var o = Settings.Load(ini); if (o.Unreadable != null) S.Unreadable = o.Unreadable; crewUnequipped = S.CrewGearOff; ownOpenBefore = S.CrewOpenBefore; if (catalogBuiltIn && File.Exists(jobs)) Catalog = GameData.LoadCatalog(jobs); CatalogView = Catalog.Select(j => j.Copy()).ToList(); Log(back ? "Account: " + name + ". Its own settings are back (accounts\\" + Profile.Key(name) + ")." : "Account: " + name + ". Its settings and job list are kept for it from now on (accounts\\" + Profile.Key(name) + ")."); if (S.Account.Length == 0) return; Directory.CreateDirectory(Accounts.Folder(dir, S.Account)); S.Save(Accounts.Ini(dir, S.Account), true); if (lead == null) S.Save(Path.Combine(dir, "IdleMafiaBot.ini")); GameData.SaveCatalog(Accounts.Jobs(dir, S.Account), Catalog); GameData.SaveCatalog(catalogFile, Catalog); catch (Exception e) { Log("Couldn't save " + S.Account + "'s settings: " + e.Message); } void SwitchAccount(string name) string before = S.Account; string ini = Accounts.Ini(dir, name), jobs = Accounts.Jobs(dir, name); bool known = File.Exists(ini); var theirs = known ? Settings.Load(ini) : Accounts.NewFrom(S); S.TakeAccountFields(theirs); if (theirs.Unreadable != null) S.Unreadable = theirs.Unreadable; Log("Couldn't read " + name + "'s settings (" + theirs.Unreadable + "). It plays with a new player's settings and nothing is saved this time. Start the bot again to get them back."); lock (totalsLock) totalsFolded = Count.Copy(); catalogBuiltIn = !File.Exists(jobs); Catalog = GameData.LoadCatalog(jobs); CatalogView = Catalog.Select(j => j.Copy()).ToList(); ResetAccountState(before); Log(known ? "Account: " + name + " is playing now. Its own settings and job list are back (" + before + "'s are kept)." : "Account: " + name + " is new here: it starts with " + before + "'s settings and the game's job list."); void RenameAccount(string name) string before = S.Account, from = Accounts.Folder(dir, before), to = Accounts.Folder(dir, name); try { if (Directory.Exists(from) && !Directory.Exists(to)) Directory.Move(from, to); } catch (Exception e) { Log("Couldn't move " + before + "'s folder to " + name + "'s: " + e.Message); } memory = new FightMemory(MemoryFile(S.Account)); Log("Account: " + name + " - the same account as \"" + before + "\" (a part of its name was read before). Its settings and job list go on under the whole name (accounts\\" + Profile.Key(name) + ")."); void ResetAccountState(string before) DateTime now = DateTime.UtcNow; nextOps = nextBosses = nextPlaytime = nextContracts = nextCrates = nextHeists = nextFights = nextBank = nextPoints = nextJobs = nextLearn = nextGameStats = now; nextBosses = BossesDoneUntil(now); nextEquip = nextShop = nextSafehouse = nextProperties = nextTutorialLook = nextMarket = now; nextBriefcases = nextEvents = nextGlobalBoss = nextKnock = now; ResetGlobalBoss(); badgeSig = eventsSig = null; eventsOn.Clear(); briefcasesSaid = null; briefcasesStopped = false; nextHeistStart = now; heistEnergy = 0; lastBank = DateTime.MinValue; learnedThisRun = false; foldDue = true; propPrice = 0; propLotsOpen = -1; propReplace = false; replaceSaid = null; propWontBuild.Clear(); upgradePrice = -1; nextUpgrade = now; upgradeSaid = ""; upgradeNeedsLevel = -1; levelBelow = int.MaxValue; nextCrew = nextCrewSlot = now; crewSlotPrice = -1; crewSaid = ""; bankFee = -1; feeLookedUp = null; crewInfo = null; nextReroll = now; rerollCrew = null; rerollEmpty = false; emptiedRarity = -1; emptiedByStats = false; rerollCash = -1; rerollSaid = null; rerollShort = false; listShortFails = 0; hiredRead = -1; rerollSkip.Clear(); sortStuck.Clear(); sortStuckRounds = 0; autoRun = null; autoRollOff = false; autoRollGrey = 0; autoRollNext = autoRollRest = rollStopNext = DateTime.MinValue; rollStopTries = 0; if (Running && !CheckOrScan) RestoreRun(); lock (jobAside) jobAside.Clear(); jobMisses.Clear(); ownStats.Clear(); ownReadAt = ownConfirmAt = DateTime.MinValue; crewUnequipped = S.CrewGearOff; ownOpenBefore = S.CrewOpenBefore; gearBackFails = 0; gearBackHold = DateTime.MinValue; ownUnknownLeft = -1; ownJustRead = false; energyRegen.Forget(); staminaRegen.Forget(); ownAttack = ownDefense = -1; ownPowerAt = DateTime.MinValue; scouted.Clear(); fightSkip.Clear(); hitOnList.Clear(); refreshBrokenUntil = DateTime.MinValue; memory = new FightMemory(MemoryFile(S.Account)); crewHirePrice = new double[] { 10000, 10e6, 10e9 }; tutorialHint = tutorialItem = tutorialAction = tutorialLogged = null; tutorialOver = tutorialFought = false; feeSaid = saidBestGear = saidDeposit = false; levelSaid = levelSeen = levelSaidXp = -1; summaryLevel = -1; streaks.Clear(); ShowStuck(); if (game != null) { game.ResetAccount(before, S.Account); game.SeedMax(S.BarMax); } bool TutorialOn { get { return tutorialHint != null; } } string tutorialHint, tutorialItem, tutorialAction; DateTime tutorialSince, nextTutorialLook; bool tutorialOver, tutorialFought; if (tutorialOver || game == null || (level > 8 && tutorialHint == null) || DateTime.UtcNow < nextTutorialLook) return; nextTutorialLook = DateTime.UtcNow.AddSeconds(15); using (var f = game.Capture()) box = game.ReadTutorial(f); if (tutorialHint != null && tutorialFought) { tutorialOver = true; Log("Tutorial: finished"); } tutorialHint = null; tutorialAction = null; tutorialItem = null; bool changed = box.Hint != tutorialHint; if (changed && box.Hint != tutorialLogged) tutorialLogged = box.Hint; tutorialSince = DateTime.UtcNow; Log("Tutorial: \"" + box.Hint + "\""); bool stuck = DateTime.UtcNow - tutorialSince > TimeSpan.FromMinutes(30); if (box.Done != null) { tutorialAction = "done"; return; } if (stuck && box.Skip != null) { tutorialAction = "skip"; return; } string u = box.Hint.ToUpperInvariant(); var buy = Regex.Match(box.Hint, @"Buy the (.+?)\s*[.!]", RegexOptions.IgnoreCase); bool fightStep = !buy.Success && !u.Contains("PROPERT") && (u.Contains("FIGHT PAGE") || u.Contains("REAL PLAYER") || u.Contains("TO ATTACK")); if (fightStep) { tutorialAction = tutorialFought ? null : "fight"; return; } tutorialItem = buy.Success ? buy.Groups[1].Value.Trim() : null; if (u.Contains("PROPERT")) { propPrice = 0; nextProperties = DateTime.UtcNow; } else if (tutorialItem != null || u.Contains("SHOP")) { nextShop = DateTime.UtcNow; game.MenuChanged(Tab.Shop); } else if (u.Contains("EQUIP") || u.Contains("CREW")) nextEquip = DateTime.UtcNow; else if (!u.Contains("JOB") && !u.Contains("EXP")) SnapshotOnce("tutorial step " + Regex.Replace(box.Hint, "[^A-Za-z ]", "").Trim()); string what = tutorialAction; nextTutorialLook = DateTime.UtcNow; if (!tutorialFought) nextTutorialLook = DateTime.UtcNow.AddMinutes(2); using (var f = game.Capture()) box = game.ReadTutorial(f); if (what == "done" && box.Done != null) { game.PressTutorial(box.Done); Log("Tutorial: DONE pressed"); } else if (what == "skip" && box.Skip != null) { game.PressTutorial(box.Skip); Log("Tutorial: skipped \"" + box.Hint + "\" (stuck on it for 30 minutes)"); } readonly Dictionary<string, DateTime> propWontBuild = new Dictionary<string, DateTime>(); bool WontBuild(Game.PropertyCard c) return c != null && propWontBuild.TryGetValue(Parse.Key(c.Name), out until) && DateTime.UtcNow < until; nextProperties = DateTime.UtcNow.AddMinutes(2); var cards = game.ReadBuildCards(); if (cards == null || cards.Count == 0) SayReplace("the BUILD page's cards didn't read. The bot looks again in 30 minutes."); propPrice = 0; propRecheck = true; nextProperties = DateTime.UtcNow.AddMinutes(30); string seen = string.Join("|", cards.Where(c => (c.Gold > 0 || c.Income > 0) && c.Name.Trim().Length > 0) .Select(c => (c.Gold > 0 ? "g:" : "") + c.Name.Trim()).Distinct().ToArray()); if (seen.Length > 0 && seen != S.PropertySeen) { S.PropertySeen = seen; SaveSettings(); } var lots = game.ReadBlock(cards.Where(c => c.Gold > 0).ToList(), out open); if (lots == null || open < 0) SayReplace("YOUR BLOCK didn't read. The bot looks again in 30 minutes."); game.Snapshot("property lots not read", 60); propPrice = 0; propRecheck = true; nextProperties = DateTime.UtcNow.AddMinutes(30); double spend = SpendNow(); var planCards = cards.Where(c => !WontBuild(c)).ToList(); var plan = PlanProperty(planCards, lots, open, S.GoldLots, () => game.GoldLotsOwned(lots), spend, S.PropertyOnly); propPrice = 0; propRecheck = true; nextProperties = DateTime.UtcNow.AddMinutes(plan.Why != null && plan.Why.Contains("didn't read") ? 30 : 360); SayReplace(plan.Why ?? "every lot holds the best property you can build. The bot looks again in 6 hours or after a level-up."); propPrice = plan.Target.Price; propReplace = plan.Replace != null; string what = plan.Replace != null ? plan.Target + " in place of a " + plan.Replace : plan.Target + " on a free lot"; if (spend < 0 || spend < plan.Target.Price * 1.01) SayReplace("saving up for " + what); var worst = plan.Replace; if (worst != null && (worst = SecondBlockRead(worst, lots)) == null) return; double need = plan.Target.Price * 1.01; took = WithdrawFor(need, true); double cash = plan.Replace != null ? CashSure() : CashNow(); if (cash < 0 || cash < need) { SayReplace("saving up for " + what); return; } if (worst != null && !Replace(worst, plan.Target, ref took)) return; if (!game.BuildCard(plan.Target, out why, out grey)) if (grey && plan.Target.Name.Trim().Length > 0) propWontBuild[Parse.Key(plan.Target.Name)] = DateTime.UtcNow.AddHours(12); Log("Properties: " + plan.Target.Name + " won't build (" + why + ") - left out for 12 hours, the next best goes on the lot"); if (plan.Replace != null) propLotsOpen = Math.Max(1, propLotsOpen); nextProperties = DateTime.UtcNow; if (plan.Replace != null) { Log("Properties: the lot is free but nothing was built yet (" + why + ") - the next look builds on it"); propLotsOpen = Math.Max(1, propLotsOpen); } else Log("Properties: didn't build " + plan.Target + " - " + why); nextProperties = DateTime.UtcNow.AddMinutes(plan.Replace != null ? 1 : 10); PropertyBuilt(plan.Target, worst); if (propLotsOpen > 0) propLotsOpen--; nextProperties = DateTime.UtcNow; catch (Exception) { propRecheck = true; throw; } finally { if (took) DepositBack(); } if (!CanWithdraw()) return cash; using (var f = game.Capture()) banked = game.ReadHeader(f).Banked; return cash + Math.Max(0, banked); internal sealed class PropertyPlan public Game.PropertyCard Target; public Game.BlockLot Replace; internal static PropertyPlan PlanProperty(List<Game.PropertyCard> cards, List<Game.BlockLot> lots, int lotsOpen, int goldLots, Func<int> goldOwned, double spend, string only = "") foreach (var c in cards.Where(c => c.Income > 0 && c.Gold <= 0)) var same = lots.Where(l => l.Gold <= 0 && l.Income > 0 && SameProperty(l.Name, c.Name)).Select(l => l.Income).OrderBy(x => x).ToList(); if (same.Count == 0) continue; double mid = same[same.Count / 2]; if (Math.Abs(c.Income - mid) > mid * 0.1) c.Income = mid; var cash = cards.Where(c => c.Income > 0 && c.Gold <= 0 && c.Price > 0).ToList(); if (!string.IsNullOrEmpty(only) && only.Trim().Length > 0) var pickedGold = cards.Where(c => c.Gold > 0 && c.Price > 0 && SameProperty(c.Name, only)).OrderBy(c => c.Price).FirstOrDefault(); if (lotsOpen > 0) return new PropertyPlan { Target = pickedGold }; var weakCash = Game.WorstLot(lots); if (weakCash != null) return new PropertyPlan { Target = pickedGold, Replace = weakCash }; var weakGold = Game.WorstGoldLot(lots); if (weakGold != null && weakGold.Gold < pickedGold.Gold) return new PropertyPlan { Target = pickedGold, Replace = weakGold }; if (!lots.Any()) return new PropertyPlan { Why = "every lot is built on, and YOUR BLOCK's lots didn't read. The bot looks again in 30 minutes." }; return new PropertyPlan(); cash = cash.Where(c => SameProperty(c.Name, only)).ToList(); return new PropertyPlan { Why = only.Trim() + " (your pick) isn't on the BUILD page to build: not open yet, or its name didn't read. The bot looks again in 30 minutes." }; var gold = cards.Where(c => c.Gold > 0 && c.Price > 0).ToList(); Func<Game.PropertyCard, bool> covered = c => spend >= 0 && c.Price * 1.01 <= spend; Func<List<Game.PropertyCard>, Func<Game.PropertyCard, double>, Game.PropertyCard> pick = (from, worth) => from.Where(covered).OrderByDescending(worth).ThenBy(c => c.Price).FirstOrDefault() ?? from.OrderBy(c => c.Price).FirstOrDefault(); var worstCash = Game.WorstLot(lots); var better = worstCash == null ? new List<Game.PropertyCard>() : cash.Where(c => c.Income > worstCash.Income && !SameProperty(c.Name, worstCash.Name)).ToList(); Func<List<Game.PropertyCard>, Game.PropertyCard> pickCash = from => var fair = from.Where(c => !from.Any(d => d.Price <= c.Price && d.Income >= c.Income && (d.Price < c.Price || d.Income > c.Income))) .OrderBy(c => c.Price).ThenByDescending(c => c.Income).ToList(); return fair.Where(covered).OrderByDescending(c => c.Income).FirstOrDefault() ?? fair.FirstOrDefault(); bool cashDone = cash.Count == 0 || (worstCash != null && better.Count == 0); int owned = int.MinValue; Func<int> ownedNow = () => { if (owned == int.MinValue) owned = goldLots > 0 && gold.Count > 0 ? goldOwned() : 0; return owned; }; if (cashDone && goldLots > 0 && gold.Count > 0) if (n < 0) return new PropertyPlan { Why = "the gold lots' count didn't read. The bot looks again in 30 minutes." }; if (n < goldLots) return new PropertyPlan { Target = pick(gold, c => c.Gold) }; if (cash.Count == 0) return new PropertyPlan { Why = "a lot is free, but no property to build on it read. The bot looks again in 30 minutes (didn't read)." }; return new PropertyPlan { Target = pickCash(cash) }; if (better.Count > 0) return new PropertyPlan { Target = pickCash(better), Replace = worstCash }; if (worstCash == null && !lots.Any(l => l.Gold > 0)) return new PropertyPlan { Why = "every lot is built on, and YOUR BLOCK's lots didn't read. The bot looks again in 30 minutes." }; if (goldLots > 0 && gold.Count > 0) if (n < 0) return new PropertyPlan { Why = "the gold lots' count didn't read. The bot looks again in 30 minutes." }; if (n < goldLots && worstCash != null) return new PropertyPlan { Target = pick(gold, c => c.Gold), Replace = worstCash }; var worstGold = Game.WorstGoldLot(lots); if (worstGold != null && goldLots > 0) var more = gold.Where(c => c.Gold > worstGold.Gold).ToList(); if (more.Count > 0) return new PropertyPlan { Target = pick(more, c => c.Gold), Replace = worstGold }; return new PropertyPlan(); internal static bool SameProperty(string a, string b) Func<string, string> key = t => new string((t ?? "").Where(char.IsLetterOrDigit).Select(char.ToLowerInvariant).ToArray()); string x = key(a), y = key(b); if (x.Length == 0 || y.Length == 0) return false; if (Math.Min(x.Length, y.Length) < 10 || Math.Abs(x.Length - y.Length) > 1) return false; int i = 0, j = 0, diff = 0; while (i < x.Length && j < y.Length) if (x[i] == y[j]) { i++; j++; continue; } if (++diff > 1) return false; if (x.Length > y.Length) i++; else if (y.Length > x.Length) j++; else { i++; j++; } return diff + (x.Length - i) + (y.Length - j) <= 1; void PropertiesLevelUp() { nextProperties = DateTime.UtcNow; propPrice = 0; propLotsOpen = -1; propReplace = false; propRecheck = false; } Game.BlockLot SecondBlockRead(Game.BlockLot worst, List<Game.BlockLot> lots) var again = game.ReadBlock(null, out open, 2); if (again == null || again.Count == 0) Log("Properties: didn't replace a " + worst + " - YOUR BLOCK didn't read a second time"); nextProperties = DateTime.UtcNow.AddMinutes(30); var lower = Game.WorstLot(lots, again); if (lower != null && lower.Income < worst.Income) Log("Properties: the second read of YOUR BLOCK found a " + lower + ", worse than the " + worst + " the first read picked - that one goes"); bool Replace(Game.BlockLot worst, Game.PropertyCard target, ref bool took) bool ready = game.BuildReady(target, out buyOut); if (!ready && buyOut > 0) if (WithdrawFor(buyOut * 1.01, true)) took = true; ready = game.BuildReady(target); Log("Properties: didn't replace a " + worst + " - " + target.Name + "'s BUILD isn't showing gold yet"); nextProperties = DateTime.UtcNow.AddMinutes(10); if (!game.DemolishLot(worst, out why)) Log("Properties: didn't replace a " + worst + " - " + why); nextProperties = DateTime.UtcNow.AddMinutes(30); Log("Properties: demolished a " + worst + " to build " + target); propLotsOpen = Math.Max(1, propLotsOpen); void PropertyBuilt(object p, object instead) Log("Properties: built " + p); Discord.Post(Ping.Property, "\U0001F3E0 New property", "Built " + p + (instead != null ? " in place of a " + instead : ""), Shot, Discord.Green); void SayReplace(string what) if (replaceSaid == what) return; Log("Properties: " + what); double PropertyKeep() { return S.Properties && propLotsOpen > 0 && propPrice > 0 && !TutorialOn ? propPrice : 0; } double keep = S.Properties && propPrice > 0 && !TutorialOn ? propPrice : 0; if (S.SafehouseUpgrade && upgradePrice > 0) keep += upgradePrice; double upgradePrice = -1; int upgradeNeedsLevel = -1; void DoSafehouseUpgrade() double keep = PropertyKeep(), floor = keep; bool took = upgradePrice > 0 && BankFee() == 0 && WithdrawKeeping(upgradePrice, true, upgradePrice, keep, out floor); if (!took) floor = KeepFloor(keep, true); string why = DoSafehouseUpgradeHere(!took, floor); if (!took && why == "saving up" && upgradePrice > 0 && WithdrawKeeping(upgradePrice, true, upgradePrice, keep, out floor)) DoSafehouseUpgradeHere(true, floor); string DoSafehouseUpgradeHere(bool sayWaiting, double keep) nextUpgrade = DateTime.UtcNow.AddMinutes(10); var r = game.UpgradeSafehouse(keep); upgradePrice = a == null ? -1 : a.Level > 0 && a.Level >= a.MaxLevel ? 0 : a.Price; Log("Safehouse: upgraded to " + r.Next + (a != null ? " (level " + a.Level + " of " + a.MaxLevel + ")" : "") + " for " + SafehouseInfo.Money(r.Price) + (upgradePrice > 0 && a.Next != null ? ". Next: " + a.Next + " for " + SafehouseInfo.Money(upgradePrice) : upgradePrice == 0 ? ". That's the top level" : "")); SnapshotOnce("safehouse upgraded"); nextUpgrade = nextSafehouse = DateTime.UtcNow; if (r.Why == "top level") { upgradePrice = 0; nextUpgrade = DateTime.UtcNow.AddHours(24); return r.Why; } upgradeNeedsLevel = last != null && last.Level >= r.NeedLevel ? -1 : r.NeedLevel; nextUpgrade = DateTime.UtcNow.AddHours(6); if (upgradeSaid != "level" + r.NeedLevel) { upgradeSaid = "level" + r.NeedLevel; Log("Safehouse: the next one" + (r.Next != null ? ", " + r.Next + "," : "") + " needs level " + r.NeedLevel + ". The bot upgrades it once you get there."); } if (r.Price > 0) upgradePrice = r.Price; if (r.Why == "saving up") bool bankCovers = r.Price > 0 && last != null && Spendable(last) >= r.Price + PropertyKeep() && CanWithdraw(); if (sayWaiting && !bankCovers && upgradeSaid != r.Next + r.Price) { upgradeSaid = r.Next + r.Price; Log("Safehouse: saving up for " + r.Next + " (" + SafehouseInfo.Money(r.Price) + ")"); } if (r.Why != null) { nextUpgrade = DateTime.UtcNow.AddHours(1); Log("Safehouse: can't upgrade right now - " + r.Why); return r.Why; } Log("Safehouse: UPGRADE pressed but the level didn't change" + (r.Toast != null ? " (\"" + r.Toast.Trim() + "\")" : "") + (!string.IsNullOrEmpty(r.Window) ? ". On screen: \"" + r.Window + "\"" : "")); game.Snapshot("safehouse upgrade did nothing", 60); nextUpgrade = DateTime.UtcNow.AddHours(6); DateTime nextCrew, nextCrewSlot; double crewSlotPrice = -1; double[] crewHirePrice = { 10000, 10e6, 10e9 }; for (int i = 0; i < 3; i++) if (i > 0) game.Wait(400); using (var f = game.Capture()) c = game.ReadCash(f); double CashFor(double need) if (cash >= need) return cash; using (var f = game.Capture()) again = game.ReadCash(f, true); return Math.Max(cash, again); using (var f = game.Capture()) b = game.ReadCash(f, true); return b < 0 ? -1 : Math.Min(a, b); static string M(double v) { return SafehouseInfo.Money(v); } public event Action<CrewInfo> CrewRead; bool BuySlots { get { return S.CrewSlots && S.CrewFill; } } var info = new CrewInfo { At = DateTime.Now, SlotPrice = crewSlotPrice }; List<Game.CrewRow> crew = null; try { DoCrewHere(info, out crew); } finally { ShowCrewInfo(info, crew); } void ShowCrewInfo(CrewInfo info, List<Game.CrewRow> crew) if (crew == null) return; foreach (var r in crew) if (r.Rarity >= 0 && r.Rarity < 8) info.ByRarity[r.Rarity]++; var worst = crew.Where(r => r.Rarity >= 0).OrderBy(r => r.Rarity).ThenBy(r => r.Power).FirstOrDefault(); info.Worst = worst != null ? worst.ToString() : ""; if (info.Henchmen < 0) info.Henchmen = crew.Count; DateTime slotWithdrawAfter = DateTime.MinValue; void DoCrewHere(CrewInfo info, out List<Game.CrewRow> crew) nextCrew = DateTime.UtcNow.AddMinutes(30); nextCrewSlot = DateTime.UtcNow.AddMinutes(5); var rows = ReadWholeCrewClosing(out page); if (page == null || !page.Page) { Log("Crew: couldn't read the crew page"); game.Snapshot("crew not read", 60); return; } crew = rows.Where(r => !r.Empty && !r.Locked && r.Rarity >= 0).ToList(); bool rolling = game.CrewRollsSeen; info.Henchmen = page.Henchmen; info.Slots = page.Slots; info.Empty = rows.Count(r => r.Empty); if (game.CrewTotalsSeen != null && game.CrewTotalsSeen.Read) { info.Attack = game.CrewTotalsSeen.Attack; info.Defense = game.CrewTotalsSeen.Defense; } foreach (var e in rows.Where(r => r.Empty)) for (int t = 0; t < 3; t++) if (e.HirePrice[t] > 0) crewHirePrice[t] = e.HirePrice[t]; bool changed = false, more = false; double keep = PropertyKeep(); double price = -1, cash = CashNow(), slotKeep = SlotKeep(), floor = KeepFloor(slotKeep, true); double paid = cash >= 0 ? game.UnlockCrewSlot(cash, floor, out price) : -1; if (price > 0) crewSlotPrice = info.SlotPrice = price; if (paid == 0 && price > 0 && DateTime.UtcNow >= slotWithdrawAfter && WithdrawKeeping(price, true, price, slotKeep, out floor)) if (game.OpenTab(Tab.Crew)) { cash = CashNow(); paid = cash >= 0 ? game.UnlockCrewSlot(cash, floor, out price) : -1; } slotWithdrawAfter = DateTime.UtcNow.AddHours(6); game.Snapshot("crew slot not bought", 360); Log("Crew: the cash taken out still didn't buy the slot" + (paid < 0 ? " (UNLOCK SLOT not found, or the press didn't take)" : "") + " - the bank isn't used for it again for 6 hours"); DepositBack(paid >= 0 ? paid : -1); if (!game.OpenTab(Tab.Crew)) return; Log("Crew: bought a new slot for " + M(paid)); changed = true; crewSlotPrice = info.SlotPrice = -1; nextCrewSlot = slotBoughtAt = DateTime.UtcNow; if (info.Slots > 0) info.Slots++; info.Status = "Bought a new slot."; else if (paid == 0 && price > 0) info.Status = "Saving up for the next slot (" + M(price) + ")."; if (crewSaid != "slot" + price) { crewSaid = "slot" + price; Log("Crew: saving up for the next slot (" + M(price) + ")"); } for (int n = 0; n < 5; n++) var empty = game.FindCrewRow(r => r.Empty, out p); if (empty == null) break; for (int t = 0; t < 3; t++) if (empty.HirePrice[t] > 0) crewHirePrice[t] = empty.HirePrice[t]; double hireCash = CashFor(empty.HirePrice[(int)hire] > 0 ? empty.HirePrice[(int)hire] : 0); hire = EmptySlotHire(hire, S.CrewRollType, empty.HirePrice[(int)hire], hireCash); var got = game.HireInto(empty, hire, true, hireCash, 0, out used, out paid); if (got == null && game.HiredUnseen != null) Log("Crew: hired " + game.HiredUnseen + " with a " + used + " hire (" + M(paid) + ") - the game said so; its row hadn't shown yet"); PingHenchman(game.HiredUnseen, -1, false, crew); if (game.HireBusy) Log("Crew: " + CrewBusy + " - the empty slot waits for the next look"); else if (paid > 0) { Log("Crew: a " + used + " hire showed no recruit"); game.Snapshot("crew hire no recruit", 60); } else if (crewSaid != "hire") { crewSaid = "hire"; Log("Crew: an empty slot waits - no hire the cash covers yet"); } Game.NoteOwn(ownStats, got); Log("Crew: hired " + got + " with a " + used + " hire (" + M(paid) + ")"); PingHenchman(got, -1, false, crew); SnapshotOnce("crew hired"); if (changed && S.EquipBest) DoEquip(); if (!S.CrewFill && !S.CrewReroll && !S.TrainCrew) { } else if (rolling) SayOnce("crew sort auto roll " + DateTime.UtcNow.ToString("yyyyMMddHH"), "Crew: the order waits while a slot is auto-rolling in the game"); else if (crew.Any(r => r.Training)) SayOnce("crew sort training " + DateTime.UtcNow.ToString("yyyyMMddHH"), "Crew: the order waits while a henchman trains"); else if (!changed && page.Henchmen > 0 && crew.Count == page.Henchmen && Game.InOrder(crew, ByOwnStats ? ownStats : null, SortSkipped)) { } else SortCrew(40, out sorted); if (!sorted) more = true; if (more) nextCrew = DateTime.UtcNow.AddMinutes(1); if (sortWasStuck) { sortStuckRounds++; nextCrew = DateTime.UtcNow.AddMinutes(sortStuckRounds == 1 ? 1 : sortStuckRounds == 2 ? 5 : 30); } else if (sorted) sortStuckRounds = 0; crewQuiet = !S.CrewFill && !S.CrewReroll && !S.TrainCrew && !more && !sortWasStuck; if (crewQuiet) nextCrew = DateTime.UtcNow.AddHours(6); if (S.TrainCrew && S.TrainAtOnce > 0) TrainCrew(crew, keep, info); void TrainCrew(List<Game.CrewRow> crew, double keep, CrewInfo info) int busy = crew.Count(r => r.Training && r.TrainSecondsLeft > 0); int soonest = crew.Where(r => r.Training && r.TrainSecondsLeft > 0).Select(r => r.TrainSecondsLeft).DefaultIfEmpty(-1).Min(); int best = Game.BestRoll(RollNow()); foreach (var r in crew.Where(r => !r.Empty && !r.Locked && !string.IsNullOrEmpty(r.Name))) int lv = Math.Max(r.TrainLevel, r.TrainingTo); foreach (var k in trainedTo) if (Game.NamedLike(r, k.Key)) lv = Math.Max(lv, k.Value); if (lv > r.TrainLevel) r.TrainLevel = lv; if (lv > 0) NoteTrained(r, lv); var picks = TrainFirst(crew.Where(r => !(r.Training && r.TrainSecondsLeft > 0) && !r.Stationed && !r.Recovering && r.Rarity >= (int)Rarity.Epic && r.TrainLevel < 10 && (r.Train == null || r.TrainGold) && !RerollWouldReplace(r, best)), S.TrainOrder, r => { var o = Game.OwnOf(r, ownStats); return o != null ? o.Sum : r.Power; }); var noTimer = crew.Where(r => r.Training && r.TrainSecondsLeft <= 0).Select(r => r.Name).ToList(); string nt = noTimer.Count > 0 ? string.Join(", ", noTimer) : null; if (nt != trainNoTimerSaid) { trainNoTimerSaid = nt; if (nt != null) Log("Crew: read as training but no time left on their row: " + nt + " - not counted, training goes on"); } if (picks.Count == 0 && busy < Math.Min(S.TrainAtOnce, Settings.MaxTraining)) int epic = crew.Count(r => r.Rarity >= (int)Rarity.Epic); why = "nobody to train (" + epic + " Epic or better: " + crew.Count(r => r.Rarity >= (int)Rarity.Epic && r.TrainLevel >= 10) + " at +10, " + crew.Count(r => r.Rarity >= (int)Rarity.Epic && r.Rarity < best && S.CrewReroll) + " the rerolls would replace, " + crew.Count(r => r.Rarity >= (int)Rarity.Epic && r.Train != null && !r.TrainGold) + " with a TRAIN that isn't gold, " + crew.Count(r => r.Rarity >= (int)Rarity.Epic && r.Train == null && !r.Training) + " with no TRAIN read)"; if (busy >= Math.Min(S.TrainAtOnce, Settings.MaxTraining)) why = busy + " training already, the most at once (" + string.Join(", ", crew.Where(r => r.Training && r.TrainSecondsLeft > 0).Select(r => r.Name + " " + r.RarityName + (r.TrainSecondsLeft > 0 ? ", " + Parse.Short(r.TrainSecondsLeft) + " left" : ", no time read"))) + ")"; double tooDear = double.MaxValue; foreach (var pick in picks) if (busy >= Math.Min(S.TrainAtOnce, Settings.MaxTraining) || started >= 5) break; if (pick.TrainLevel > 0 && trainPrice.TryGetValue(pick.TrainLevel + 1, out known) && known >= tooDear) continue; var row = game.FindCrewRow(r => Game.NamedLike(r, pick), out p, true); if (row == null) { why = why ?? "couldn't find " + pick.Name + " in the list"; continue; } if (row.Training && row.TrainSecondsLeft > 0) { why = why ?? pick.Name + " reads as training on a second look"; continue; } if (row.Stationed || row.Recovering) continue; if (cash < 0) { why = "the cash on hand didn't read"; break; } double floor = KeepFloor(keep, false); int got = game.TrainHenchman(row, pick, cash, floor, out offer); if (got == 0 && offer != null && offer.Price > 0 && offer.Names(pick.Name) && WithdrawKeeping(offer.Price, false, offer.Price, keep, out floor)) if (game.OpenTab(Tab.Crew)) row = game.FindCrewRow(r => Game.NamedLike(r, pick), out p, true); if (row != null && !(row.Training && row.TrainSecondsLeft > 0)) got = game.TrainHenchman(row, pick, CashNow(), floor, out offer); DepositBack(got == 1 && offer != null ? offer.Price : got == 0 ? 0 : -1); if (offer != null && offer.Price > 0 && offer.ToLevel > 0 && offer.Names(pick.Name)) trainPrice[offer.ToLevel] = offer.Price; if (offer.ToLevel > 0) NoteTrained(pick, offer.ToLevel); Count.CrewCash += offer.Price; string mins = offer.Minutes > 0 ? ", off duty for " + Parse.Short(offer.Minutes * 60) : ""; Log("Crew: training " + row.Name + " (" + row.RarityName + ") to " + (offer.ToLevel > 0 ? "+" + offer.ToLevel : "the next level (none read)") + " for " + M(offer.Price) + mins); info.Status = "Training " + row.Name + (offer.ToLevel > 0 ? " to +" + offer.ToLevel : "") + "."; if (offer.Minutes > 0 && (soonest < 0 || offer.Minutes * 60 < soonest)) soonest = offer.Minutes * 60; string who = pick.Name + " (" + pick.RarityName + (pick.TrainLevel > 0 ? ", +" + pick.TrainLevel : "") + ")"; if (got == 0 && offer != null && offer.Price > 0 && offer.Names(pick.Name)) double onHand = CashNow(); string dear = who + (offer.ToLevel > 0 ? " to +" + offer.ToLevel : "") + " (" + M(offer.Price) + ", " + (onHand >= 0 ? M(onHand) : "?") + " on hand" + (keep > 0 ? ", " + M(keep) + " kept for the next property" : "") + ")"; if (waiting == null) waiting = dear; why = "saving up to train " + waiting; info.Status = "Saving up to train " + row.Name + " (" + M(offer.Price) + ")."; tooDear = Math.Min(tooDear, offer.Price); else if (got == 0) why = who + ": the training window " + (offer == null ? "didn't read" : offer.Price <= 0 ? "showed no price the bot could read (\"" + offer.Words + "\")" : "named " + offer.Name) + " - CANCEL pressed"; else why = who + ": " + (game.TrainWhy ?? "nothing pressed"); if (started > 0 && waiting != null) string key = Regex.Replace(waiting, @"[^ (]+ on hand", ""); if (key != trainSkipSaid) { trainSkipSaid = key; Log("Crew: " + waiting + " waits for the cash - the next best were trained meanwhile"); } if (started > 0) trainSaid = null; string key = Regex.Replace(Regex.Replace(why, @"[0-9]+[hms ]*[0-9]*[hms]* left", ""), @"[^ (]+ on hand", ""); if (key != trainSaid) { trainSaid = key; Log("Crew: nobody trained - " + why); } var at = DateTime.UtcNow.AddSeconds(soonest + 20); if (at < nextCrew) nextCrew = at; if (started == 0 && game.TrainBusy) var at = DateTime.UtcNow.AddMinutes(2); if (at < nextCrew) nextCrew = at; internal static List<Game.CrewRow> TrainFirst(IEnumerable<Game.CrewRow> rows, TrainOrder order, Func<Game.CrewRow, double> strength) if (order == TrainOrder.Evenly) return rows.OrderBy(r => r.TrainLevel).ThenByDescending(r => r.Rarity).ThenByDescending(strength).ToList(); return rows.OrderByDescending(r => r.Rarity).ThenByDescending(strength).ToList(); readonly Dictionary<int, double> trainPrice = new Dictionary<int, double>(); readonly Dictionary<Game.CrewRow, int> trainedTo = new Dictionary<Game.CrewRow, int>(); if (trainedFor == S.Account) return; foreach (var part in (S.TrainedLevels ?? "").Split(';')) var m = Regex.Match(part, @"^(\d)\|(.+)=(\d{1,2})$"); if (m.Success) trainedTo[new Game.CrewRow { Rarity = int.Parse(m.Groups[1].Value), Name = m.Groups[2].Value }] = int.Parse(m.Groups[3].Value); void NoteTrained(Game.CrewRow r, int level) var had = trainedTo.Keys.FirstOrDefault(k => Game.NamedLike(r, k)); if (had != null) { if (trainedTo[had] >= level) return; trainedTo.Remove(had); } trainedTo[new Game.CrewRow { Rarity = r.Rarity, Name = r.Name.Replace(";", "").Replace("=", "").Replace("|", "") }] = level; S.TrainedLevels = string.Join(";", trainedTo.Select(k => k.Key.Rarity + "|" + k.Key.Name + "=" + k.Value)); bool RerollWouldReplace(Game.CrewRow r, int best) return S.CrewReroll && r.Rarity < best; int HireNoRecruit(CrewRoll roll, double paid, int fails, out string stop) if (game.HireBusy) { stop = CrewBusy; return fails + 1; } if (paid <= 0) { stop = "the " + roll + " hire isn't gold right now"; return fails + 1; } stop = "the " + roll + " hire showed no recruit"; int wait = 10000 * rerollHireFails; Log("Crew: a " + roll + " hire into an empty slot showed no recruit (try " + rerollHireFails + " of 3) - waiting " + (wait / 1000) + " s before the same slot is tried again"); return rerollHireFails >= 3 ? 3 : fails; readonly List<Game.OwnStats> ownStats = new List<Game.OwnStats>(); DateTime ownReadAt = DateTime.MinValue, gearBackHold = DateTime.MinValue; DateTime ownConfirmAt = DateTime.MinValue; get { return crewUnequipped; } if (!value) gearBackFails = 0; if (S.CrewGearOff == value) return; readonly object totalsLock = new object(); Counters totalsFolded = new Counters(); DateTime totalsFoldedAt = DateTime.UtcNow; get { lock (totalsLock) { var t = Counters.Parse(S.Totals); t.Add(Count, totalsFolded); return t; } } public void FoldTotals(bool save = true) var t = Counters.Parse(S.Totals); t.Add(Count, totalsFolded); totalsFolded = Count.Copy(); totalsFoldedAt = DateTime.UtcNow; if (save) SaveSettings(); internal void SaveSettings() if (lead == null) S.Save(Path.Combine(dir, "IdleMafiaBot.ini")); if (S.Account.Length > 0) { Directory.CreateDirectory(Accounts.Folder(dir, S.Account)); S.Save(Accounts.Ini(dir, S.Account), true); SavedOwn(); } catch (Exception e) { if (firstSeen.Add("settings not saved")) Log("Couldn't save the settings: " + e.Message); } ownReadAt = DateTime.UtcNow; bool off = game.UnequipAll(out pressed, out why); if (!pressed) CrewUnequipped = false; foreach (var r in game.ReadWholeCrew(out page)) if (!r.Empty && !r.Locked && r.Attack >= 0 && r.Defense >= 0) { Game.NoteOwn(ownStats, r); read++; } foreach (var r in game.ReadWholeCrew(out page, 3)) if (!r.Empty && !r.Locked && r.Attack >= 0 && r.Defense >= 0) Game.NoteOwn(ownStats, r); if (pressed) { game.ScrollPage(0.62, 40); DoEquip(); } if (read > 0) Log("Crew: read " + read + " henchmen's own stats with their gear off, then the best gear on again"); Log("Crew: " + (pressed ? "UNEQUIP ALL pressed, but " + (why ?? "no stats read") : "UNEQUIP ALL not pressed: " + why) + " - own stats looked at again in an hour"); ownReadAt = DateTime.UtcNow.AddHours(-5); bool OwnReadDue(int unknown) { return OwnReadDue(unknown, ownUnknownLeft, DateTime.UtcNow - ownReadAt); } internal static bool OwnReadDue(int unknown, int unknownLeft, TimeSpan sinceRead) if (unknown <= 0) return false; return sinceRead > TimeSpan.FromHours(6) || (unknown > unknownLeft && sinceRead > TimeSpan.FromHours(1)); List<Game.CrewRow> rerollCrew; DateTime emptyFootSaidAt; get { return ownOpenBefore; } set { ownOpenBefore = value; } int keep = S.CrewFill ? -1 : ownOpenBefore; if (S.CrewOpenBefore == keep) return; internal static int OpenSlots(Game.CrewPage p) return p != null && p.Page && p.Henchmen >= 0 && p.Slots > 0 && !p.AutoRolls ? Math.Max(0, p.Slots - p.Henchmen) : -1; internal static bool RerollFills(bool emptyRow, bool crewFill, int open, int openBefore) if (!emptyRow) return false; if (crewFill) return true; if (openBefore == -1) return false; return open < 0 || openBefore < 0 || open > openBefore; internal static bool OwnSlotFilled(int open, int openBefore) if (openBefore == -1 || open < 0) return false; return openBefore >= 0 ? open <= openBefore : open == 0; int open = OpenSlots(game.CrewPageSeen); return open == 0 ? 0 : OpenSlots(game.LookAtCrew()); bool OpenedSince(int before, out string count) if (before < 0) return false; var now = game.LookAtCrew(); if (OpenSlots(now) != before + 1) return false; count = now.Henchmen + " of " + now.Slots; bool RerollCashOk(Header h, DateTime now) if (!rerollShort) return true; double need = crewHirePrice[(int)roll] + (rerollEmpty ? 0 : PropertyKeep()); if (h != null && h.Cash >= 0 && roll != S.CrewRollType) return Spendable(h) >= need; return h != null && h.Cash >= 0 ? h.Cash >= need : now >= rerollShortAt.AddMinutes(1); internal static int ShortWaitSeconds(bool shortBefore, double spent) { return shortBefore && spent <= 0 ? 15 : 3; } public volatile bool RerollOnly; public void StartRerollOnly() if (Program.Reroller) { S.RerollerOnly(); rerollerBase = Count.CrewCash; } volatile string rerollerBreak; public double RerollerSpent { get { return Count.CrewCash - rerollerBase; } } void StartRerollOnlyGo() { FightOnly = false; onlyFails = 0; onlyProgressAt = DateTime.UtcNow; nextReroll = DateTime.UtcNow; RerollOnly = true; if (!Running) Begin(false); } public void StopRerollOnly() { if (!RerollOnly) return; RerollOnly = false; stop = true; } void EndRerollOnly(string why, bool finished) Log("Only rerolling: " + why + " - the bot stops"); stopWhy = (finished ? "Only rerolling is over: " : "Only rerolling gave up: ") + why; public volatile bool FightOnly; public void StartFightOnly() onlyFightFails = 0; onlyFightProgressAt = DateTime.UtcNow; nextFights = DateTime.UtcNow; if (!Running) Begin(false); public void StopFightOnly() { if (!FightOnly) return; FightOnly = false; stop = true; } void EndFightOnly(string why) Log("Only fighting: " + why + " - the bot stops"); stopWhy = "Only fighting gave up: " + why; DateTime onlyFightProgressAt; void AfterFightOnly(int fought) var now = DateTime.UtcNow; if (fought > 0) { onlyFightFails = 0; onlyFightProgressAt = now; return; } var h = ReadBarsSettled(2000); int kept = S.OnlyFightsSpendAll ? 0 : Kept(h.StaminaMax); bool noStamina = h.Stamina >= 0 && (S.OnlyFightsSpendAll ? h.Stamina <= 0 : h.Stamina - kept < FightSpare(h.StaminaMax, kept)); Log("Only fighting: waiting for stamina (" + h.Stamina + (h.StaminaMax > 0 ? "/" + h.StaminaMax : "") + ")"); if (nextFights < now.AddMinutes(1)) nextFights = now.AddMinutes(1); if (onlyFightFails >= 3 || now - onlyFightProgressAt > TimeSpan.FromHours(1)) EndFightOnly("nothing left worth attacking (after " + onlyFightFails + " tries)"); if (nextFights < now.AddMinutes(1)) nextFights = now.AddMinutes(1); Log("Only fighting: nothing worth attacking right now (try " + onlyFightFails + " of 3) - looking again at " + nextFights.ToLocalTime().ToString("HH:mm")); void DoRerolls(bool only) var info = new CrewInfo { At = DateTime.Now, SlotPrice = crewSlotPrice }; List<Game.CrewRow> crew = null; try { RerollsHere(info, only, out crew); } finally { KeepOwnOpenBefore(); ShowCrewInfo(info, crew); } List<Game.CrewRow> ReadCrewForRerolls(CrewInfo info, int step = 4) var rows = ReadWholeCrewClosing(out page, step); if (page == null || !page.Page) { Log("Crew: couldn't read the crew page"); game.Snapshot("crew not read", 60); return null; } info.Henchmen = page.Henchmen >= 0 ? page.Henchmen : game.CrewCountSeen; if (info.Henchmen < 0 && hiredRead >= 0 && hiredReadAt > slotBoughtAt) info.Henchmen = hiredRead; info.Slots = page.Slots; info.Empty = rows.Count(r => r.Empty); if (game.CrewTotalsSeen != null && game.CrewTotalsSeen.Read) { info.Attack = game.CrewTotalsSeen.Attack; info.Defense = game.CrewTotalsSeen.Defense; } foreach (var e in rows.Where(r => r.Empty)) for (int t = 0; t < 3; t++) if (e.HirePrice[t] > 0) crewHirePrice[t] = e.HirePrice[t]; int open = OpenSlots(page); if (OwnSlotFilled(open, OwnOpenBefore)) OwnOpenBefore = -1; rerollEmpty = RerollFills(rows.Any(r => r.Empty), S.CrewFill, open, OwnOpenBefore); return rows.Where(r => !r.Empty && !r.Locked && r.Rarity >= 0).ToList(); List<Game.CrewRow> ReadWholeCrewClosing(out Game.CrewPage page, int step = 4) var rows = game.ReadWholeCrew(out page, step); if (page != null && page.AutoRollWindow && game.CloseStrayAutoRoll()) rows = game.ReadWholeCrew(out page, step); List<Game.CrewRow> ReadAllHenchmen(CrewInfo info, out string why) for (int look = 0; look < 3; look++) int step = 4 - (listShortFails * 2 + look) % 3; string pages = look == 2 && listShortFails == 0 && !string.IsNullOrEmpty(game.ProblemsDir) ? Path.Combine(game.ProblemsDir, "active", "crew list short pages " + DateTime.Now.ToString("yyyyMMdd-HHmmss")) : null; game.CrewPagesDir = pages; try { rows = ReadCrewForRerolls(info, step); } finally { game.CrewPagesDir = null; } if (rows == null) { DropPages(pages); why = "couldn't read the crew page"; return null; } int want = info.Henchmen - 1; if (info.Henchmen > 0 && rows.Count == want) { DropPages(pages); ListReadWhole(); return rows; } if (info.Henchmen > 0 && rows.Count > want) var merged = Game.MergeRepeats(rows, rows.Count - want); Log("Crew: read " + rows.Count + " of " + want + " henchmen - " + (rows.Count - want == 1 ? "one was" : (rows.Count - want) + " were") + " read twice where two pages meet (the same rarity, attack and defense), taken once"); why = info.Henchmen <= 0 ? "couldn't read how many henchmen there are" : "read " + rows.Count + " of " + want + " henchmen"; if (look == 2 && listShortFails == 0) Log("Crew: the list read as " + string.Join(", ", rows.Select(r => r.ToString()))); if (pages != null && Directory.Exists(pages)) Log("Crew: the pages of that read are in problems\\active\\" + Path.GetFileName(pages)); if (listShortFails++ == 0) listShortSince = DateTime.UtcNow; game.Snapshot("crew list short", 60); static void DropPages(string dir) if (dir != null) try { if (Directory.Exists(dir)) Directory.Delete(dir, true); } catch (Exception) { } DateTime hiredReadAt = DateTime.MinValue, slotBoughtAt = DateTime.MinValue; bool SlotStandsEmpty(out string count) Func<Game.CrewPage, bool> open = p => p != null && p.Henchmen >= 0 && p.Slots > 0 && p.Henchmen < p.Slots && !p.AutoRolls; if (!open(game.CrewPageSeen)) return false; var now = game.LookAtCrew(); if (!open(now)) return false; count = now.Henchmen + " of " + now.Slots; if (listShortFails > 0) Log("Crew: the whole list reads right again (it didn't from " + listShortSince.ToLocalTime().ToString("HH:mm") + ")"); void RerollsHere(CrewInfo info, bool only, out List<Game.CrewRow> crew) var start = DateTime.UtcNow; nextReroll = start.AddMinutes(1); bool shortBefore = rerollShort; int best = Game.BestRoll(roll); double keep = only ? 0 : PropertyKeep(); if (rerollCrew != null && autoRun == null && start - rerollCrewAt < TimeSpan.FromMinutes(10) && game.OpenTab(Tab.Crew)) if (crewInfo != null) { info.Henchmen = crewInfo.Henchmen; info.Slots = crewInfo.Slots; info.Attack = crewInfo.Attack; info.Defense = crewInfo.Defense; } else crew = ReadCrewForRerolls(info); if (crew == null) { if (only) OnlyGo(false, "couldn't read the crew page", 0, keep); return; } if (rerollEmpty && !S.CrewFill && OwnOpenBefore == -1) rerollEmpty = false; bool rollEnds = AutoRollHere(info, keep, fromCache, ref crew, out rollWait); info.Status = rollWait + "."; if (only) OnlyGo(false, rollWait, 0, keep); if (AutoRollMay(false, -1, best) && crew.Any(r => r.Rarity >= 0 && r.Rarity < best && Dismissable(r))) RollCash(roll, keep, autoRun != null ? autoRun.SpentBefore + Math.Max(0, autoRun.Spent) : 0, out rollFloor); int rolls = 0, fails = 0, readAt = Environment.TickCount - 60000, badgesAt = Environment.TickCount; Game.CrewRow lastHired = null; var better = new List<string>(); string stop = null, pausedFor = null, idleFor = null; bool done = false, readAgain = false, reread = false, statsWait = false, statsSoon = false, listShort = false, skipWait = false, awayWait = false; if (!only && S.Events && EventOn(GameEvent.RecruitLuck) && Environment.TickCount - badgesAt > 30000) badgesAt = Environment.TickCount; if (!only && !RerollsLead()) var other = OtherTaskDue(ref readAt); if (other != null && other == idleFor && Environment.TickCount < idleUntil) other = null; else if (other != null && rolls == 0 && other == pausedIdle && DateTime.UtcNow - pausedIdleAt < TimeSpan.FromMinutes(1)) SayOnce("rerolls idle " + other, "Crew: the rerolls paused for " + other + " before their first reroll, and the round after didn't find it due - they now go on 30 s before looking again"); idleFor = other; idleUntil = Environment.TickCount + 30000; other = null; if ((pausedFor = other) != null) break; OtherTaskDue(ref readAt, true); var now = DateTime.UtcNow; if (last != null && now - fullBreakAt > TimeSpan.FromMinutes(1) && ((last.EnergyMax > 0 && last.Energy >= last.EnergyMax) || (last.StaminaMax > 0 && last.Stamina >= last.StaminaMax))) full = FullBarBreak(WhatIsDue(last, false), last); if (full != null) { fullBreakAt = now; pausedFor = full + " (a full bar)"; break; } var timed = TimedBreak(now, lastBank > clock ? lastBank : clock, S.Bank && Can(Tab.Bank), ShopOn() && now >= nextShop && Can(Tab.Shop)); if (timed == "") clock = now; else if (timed != null) { rerollsYield = true; pausedFor = timed + (timed == "shop" ? " (its new stock)" : " (every " + BreakMinutes + " minutes)"); break; } if (only && Program.Reroller && (S.RerollerJobs || S.RerollerGifts) && DateTime.UtcNow - fullBreakAt > TimeSpan.FromMinutes(3)) OtherTaskDue(ref readAt, true); string full = last == null ? null : FullBarBreak(WhatIsDue(last, false), last); if (full != null) { fullBreakAt = DateTime.UtcNow; rerollerBreak = full; pausedFor = full + " (a full bar)"; break; } if (TurnUp("rerolls") && turns.Wanted(this)) { pausedFor = "the next account's turn"; break; } pausedFor = LuckRollShort() ? "not enough cash for " + LuckRollFor(S.CrewRollType, S.CrewLuckRoll, true) + " hires, " + RollNow() + " hires meanwhile" : roll != S.CrewRollType && S.Events && EventOn(GameEvent.RecruitLuck) ? "enough cash again, " + RollNow() + " hires from now" : "recruitment luck " + (roll == S.CrewRollType ? "began" : "ended") + ", " + RollNow() + " hires from now"; var again = ReadCrewForRerolls(info); if (again == null) { stop = "couldn't read the crew page"; fails = 3; break; } double hold = rerollEmpty ? 0 : keep; double cash = CashFor(crewHirePrice[(int)roll] + hold); if (cash < 0 && Program.Reroller && cashEst >= 2 * crewHirePrice[(int)roll] + hold && crewHirePrice[(int)roll] > 0) { SnapshotOnce("reroller cash unread"); cash = cashEst; } else if (cash >= 0) cashEst = cash; if (cash < 0) { stop = "couldn't read the cash on hand"; fails++; game.Wait(1000); continue; } if (!only && crewHirePrice[(int)roll] > cash - hold && roll != S.CrewRollType) double whole = rerollEmpty ? 0 : keepAll, floor; hold = KeepFloor(whole, true); if (crewHirePrice[(int)roll] > cash - hold && WithdrawKeeping(crewHirePrice[(int)roll], true, 2 * crewHirePrice[(int)roll], whole, out floor)) if (!game.OpenTab(Tab.Crew)) { stop = "the Crew page didn't open after the bank"; fails = 3; break; } cash = CashFor(crewHirePrice[(int)roll] + hold); if (cash < 0) { stop = "couldn't read the cash on hand"; fails++; game.Wait(1000); continue; } if (only && Program.Reroller && crewHirePrice[(int)roll] > cash - hold) if (WithdrawKeeping(crewHirePrice[(int)roll], true, Math.Max(0, cash) + S.RerollerLimit, 0, out floor)) if (!game.OpenTab(Tab.Crew)) { stop = "the Crew page didn't open after the bank"; fails = 3; break; } cash = CashFor(crewHirePrice[(int)roll] + hold); if (cash < 0) { stop = "couldn't read the cash on hand"; fails++; game.Wait(1000); continue; } if (crewHirePrice[(int)roll] > cash - hold) stop = hold > 0 && crewHirePrice[(int)roll] <= cash ? "the cash is kept for the next property (" + M(hold) + ")" : "saving up for " + roll + " hires (" + M(crewHirePrice[(int)roll]) + " each, " + M(cash) + " on hand)"; rerollShortAt = DateTime.UtcNow; nextReroll = DateTime.UtcNow.AddSeconds(ShortWaitSeconds(shortBefore, spent)); var empty = game.FindCrewRow(r => r.Empty, out p, true); var short1 = game.CrewPageSeen; if (empty == null && short1 != null && short1.Page && short1.Slots > 0 && short1.Henchmen == short1.Slots - 1) empty = game.FindCrewRow(r => r.Empty, out p, true); var seen = game.CrewPageSeen; if (seen != null && seen.Henchmen >= 0 && seen.Slots > 0 && seen.Henchmen >= seen.Slots) { rerollEmpty = false; OwnOpenBefore = -1; continue; } stop = "a slot stands empty" + (seen != null && seen.Henchmen >= 0 && seen.Slots > 0 ? " (" + seen.Henchmen + " of " + seen.Slots + ")" : "") + " but it wasn't found to hire into - nobody is dismissed until it's filled"; if (DateTime.UtcNow - emptyFootSaidAt > TimeSpan.FromHours(1)) emptyFootSaidAt = DateTime.UtcNow; Log("Crew: the empty slot wasn't found - the list's foot shows: " + game.CrewFootSeen()); game.Snapshot("crew empty slot not found", 60); fails++; readAgain = true; got = HireForReroll(empty, roll, cash, 0, out used, out paid); if (got == null && game.HiredUnseen != null) var seen = game.HiredUnseen; rerollEmpty = false; OwnOpenBefore = -1; fails = 0; rerollHireFails = 0; readAgain = true; counted = false; Count.Hired++; Count.CrewCash += paid; spent += paid; PingHenchman(seen, emptiedRarity >= 0 ? emptiedRarity : crew.Count > 0 ? crew.Min(r => r.Rarity) : -1, false, crew); emptiedRarity = -1; emptiedByStats = false; Log("Crew: hired " + seen + " into the empty slot with a " + used + " hire (" + M(paid) + ") - the game said so; its row hadn't shown yet, so the list is read again"); if (paid > 0) readAgain = true; fails = HireNoRecruit(roll, paid, fails, out stop); rerollEmpty = false; OwnOpenBefore = -1; fails = 0; rerollHireFails = 0; Count.Hired++; Count.CrewCash += paid; spent += paid; PingHenchman(got, emptiedRarity >= 0 ? emptiedRarity : crew.Count > 0 ? crew.Min(r => r.Rarity) : -1, emptiedByStats && got.Rarity >= best && got.Attack >= Settings.PerfectFor(got.Rarity) && got.Defense >= Settings.PerfectFor(got.Rarity), crew); emptiedRarity = -1; emptiedByStats = false; Game.NoteOwn(ownStats, got); Log("Crew: hired " + got + " into the empty slot with a " + used + " hire (" + M(paid) + ")"); var order = crew.Where(Dismissable).OrderBy(r => r.Rarity).ThenBy(r => r.Power).ToList(); var worst = order.FirstOrDefault(); if (worst == null) { stop = "no henchmen to reroll"; break; } var next = order.FirstOrDefault(r => r.Rarity == worst.Rarity && !Skipped(r)); stop = worst.Name + " is left alone until " + SkipUntil(worst).ToLocalTime().ToString("HH:mm") + " (its DISMISS kept failing), and there's no other " + worst.RarityName + " to reroll"; var away = crew.Where(r => r.Rarity >= 0 && r.Rarity < worst.Rarity && !Dismissable(r)).OrderBy(r => r.Rarity).FirstOrDefault(); if (away != null && (worst.Rarity >= (int)Rarity.Mythic || worst.Rarity >= best)) stop = away.Name + " (" + away.RarityName + ") is away - no " + Game.RarityTitle(worst.Rarity) + " is dismissed until it's back"; Game.OwnStats weakOwn = null; if (worst.Rarity >= best) string everyone = "everyone is " + Game.RarityTitle(best) + ", the best " + roll + " hires can roll"; int min = S.CrewRerollStats > 0 ? Settings.PerfectFor(best) : 0; if (min <= 0) { stop = everyone; done = finished = true; break; } if (!S.EquipBest) { stop = everyone + " (rerolls by their own stats need \"Equip the best gear\" on)"; done = true; break; } int unknown, unconfirmed; var weak = Game.WeakestOwn(crew, ownStats, best, min, out unknown, out unconfirmed, r => Dismissable(r) && !Skipped(r)); if (ownJustRead) { ownUnknownLeft = unknown; ownJustRead = false; } if (unknown > 0 && OwnReadDue(unknown)) if (crewUnequipped) { stop = "the crew's gear isn't back on yet - that goes first"; break; } if (weak == null && unknown == 0 && unconfirmed > 0 && DateTime.UtcNow - ownReadAt > TimeSpan.FromHours(1) && DateTime.UtcNow - ownConfirmAt > TimeSpan.FromHours(1)) ownConfirmAt = DateTime.UtcNow; Log("Crew: " + unconfirmed + " " + Game.RarityTitle(best) + (unconfirmed == 1 ? "" : "s") + " under " + min + " read only once - their own stats are read again before one is dismissed"); if (crewUnequipped) { stop = "the crew's gear isn't back on yet - that goes first"; break; } if (unknown == 0 && unconfirmed > 0) { stop = unconfirmed + " " + Game.RarityTitle(best) + (unconfirmed == 1 ? "" : "s") + " read only once - read again before one is dismissed"; statsWait = statsSoon = true; } else if (unknown == 0) { stop = everyone + ", with " + min + "+ attack and defense of their own"; done = finished = true; } else { stop = "couldn't read the own stats of " + unknown + " henchm" + (unknown == 1 ? "an" : "en"); statsWait = true; statsSoon = unknown > ownUnknownLeft; } for (int go = 0; go < (only ? 10 : 2) && !sorted; go++) SortCrew(40, out sorted); if (!sorted) { stop = "sorting the crew by their own stats first"; readAgain = true; break; } weakOwn = Game.OwnOf(weak, ownStats); info.Status = "Rerolling the weakest " + Game.RarityTitle(best) + ": " + weak.Name + " (own " + weakOwn + ", under " + min + ")."; else info.Status = "Rerolling the worst: " + worst + "."; if ((byStats || worst.Rarity >= (int)Rarity.Mythic) && !counted) var all = ReadAllHenchmen(info, out why); if (all == null) { stop = "no Mythic or better dismissed: " + why; listShort = true; break; } var row = !byStats && ReferenceEquals(worst, lastHired) ? worst : game.FindCrewRow(r => Game.NamedLike(r, worst), out p, true); if (!reread) { reread = readAgain = true; continue; } stop = "couldn't find " + worst + " in the list"; Log("Crew: " + worst.Name + " not found - the list's foot shows: " + game.CrewFootSeen()); game.Snapshot("crew row not found", 60); fails++; readAgain = true; findFails = findFailsOn == worst.Key ? findFails + 1 : 1; rerollSkip.Add(Tuple.Create(worst.Name, worst.Rarity, DateTime.UtcNow.AddMinutes(45))); Log("Crew: couldn't find " + worst + " in the list 3 times - it's left alone for 45 minutes, the next one goes first"); findFails = 0; fails = 0; worst.Training |= row.Training; worst.Stationed |= row.Stationed; worst.Recovering |= row.Recovering; bool sure = (worst.Rarity < best && crew.All(r => r.Rarity >= worst.Rarity)) || (byStats && weakOwn != null && worst.Rarity == best && crew.All(r => r.Rarity >= best)); int openBefore = S.CrewFill ? -1 : OpenBeforeDismiss(); if (openBefore < 0) { stop = "the crew page's count didn't read - nobody is dismissed until it does"; fails++; game.Wait(1000); continue; } OwnOpenBefore = openBefore; if (S.CrewFill && SlotStandsEmpty(out open)) if (++emptyHolds > 3) { stop = "a slot stands empty (" + open + ") and couldn't be filled - nobody is dismissed"; game.Snapshot("crew empty slot not filled", 60); fails = 3; break; } Log("Crew: a slot stands empty (" + open + ") - it's filled before anyone else is dismissed"); if (!game.DismissHenchman(row, worst, sure, out emptied)) var late = game.EmptyRowNear(row.Anchor.CenterY); emptied = late != null && late.Anchor.CenterY >= row.Top && late.Anchor.CenterY <= row.Bottom ? late : null; if (emptied == null && (S.CrewFill ? SlotStandsEmpty(out nowOpen) : OpenedSince(openBefore, out nowOpen))) OwnOpenBefore = S.CrewFill ? -2 : openBefore; if (game.FindCrewRow(r => Game.NamedLike(r, worst), out p) == null) if (byStats) ownStats.Remove(weakOwn); Log("Crew: dismissed " + worst + " - its row didn't show the empty slot, but the page counts " + nowOpen + " henchmen and it's gone from the list: the slot is filled next"); emptiedRarity = worst.Rarity; emptiedByStats = byStats; else Log("Crew: a slot stands empty (" + nowOpen + ") - it's filled before anyone else is dismissed"); var again = game.FindCrewRow(r => Game.NamedLike(r, worst), out p); if (again == null || !game.DismissHenchman(again, worst, sure, out emptied)) if (Program.Reroller && S.CrewFill) { game.Wait(1000); } if (Program.Reroller && S.CrewFill && SlotStandsEmpty(out lateOpen)) rerollEmpty = true; OwnOpenBefore = -2; counted = false; emptiedRarity = worst.Rarity; emptiedByStats = byStats; Log("Crew: dismissed " + worst + " - its slot showed empty late (" + lateOpen + "): it's filled next"); stop = "DISMISS didn't take on " + worst; game.Snapshot("crew dismiss failed", 60); dismissFails = dismissFailsOn == worst.Key ? dismissFails + 1 : 1; dismissFailsOn = worst.Key; rerollSkip.Add(Tuple.Create(worst.Name, worst.Rarity, DateTime.UtcNow.AddMinutes(45))); Log("Crew: DISMISS failed 3 times on " + worst + " - it's left alone for 45 minutes, the next one goes first"); dismissFails = 0; fails = 0; readAgain = true; ownStats.Remove(weakOwn); Log("Crew: dismissed " + worst.Name + " (" + worst.RarityName + ", own " + weakOwn + ", under " + Settings.PerfectFor(worst.Rarity) + ") to roll a better one"); OwnOpenBefore = openBefore >= 0 ? openBefore : -2; emptiedRarity = worst.Rarity; emptiedByStats = byStats; if (AutoRollMay(byStats, worst.Rarity, best)) if (StartRollInto(emptied, roll, best, worst, crew)) rerollEmpty = false; OwnOpenBefore = -1; emptiedRarity = -1; emptiedByStats = false; counted = false; stop = "the game's Auto Roll rolls " + worst.Name + "'s slot for " + Game.RarityTitle(best) + " or better"; stop = "the Auto Roll didn't start and something is over the crew list - the empty slot is filled next go"; game.Snapshot("crew auto roll page not clear", 60); if (emptied != null) emptied = game.EmptyRowNear(emptied.Anchor.CenterY) ?? emptied; got = HireForReroll(emptied, roll, cash, hold, out used, out paid); if (got == null && game.HiredUnseen != null) var seen = game.HiredUnseen; rerollEmpty = false; OwnOpenBefore = -1; fails = 0; rerollHireFails = 0; reread = false; readAgain = true; counted = false; lastHired = null; emptiedRarity = -1; emptiedByStats = false; rolls++; spent += paid; Count.Rerolls++; Count.CrewCash += paid; if (seen.Rarity > worst.Rarity) better.Add(seen.ToString()); PingHenchman(seen, worst.Rarity, false, crew); Log("Crew: hired " + seen + " in place of " + worst + " - the game said so; its row hadn't shown yet, so the list is read again"); if (paid > 0) { rerollEmpty = false; readAgain = true; emptiedRarity = -1; } fails = HireNoRecruit(roll, paid, fails, out stop); rerollEmpty = false; OwnOpenBefore = -1; fails = 0; rerollHireFails = 0; reread = false; emptiedRarity = -1; emptiedByStats = false; rolls++; spent += paid; Count.Rerolls++; Count.CrewCash += paid; cashEst -= paid; if (!Program.Reroller) counted = false; Game.NoteOwn(ownStats, got); if (got.Rarity > worst.Rarity) better.Add(got.ToString()); PingHenchman(got, worst.Rarity, byStats && got.Rarity >= best && got.Attack >= Settings.PerfectFor(got.Rarity) && got.Defense >= Settings.PerfectFor(got.Rarity), crew); SnapshotOnce("crew reroll"); if (!readAgain && crew != null) { foreach (var r in crew) r.Recruit = false; rerollCrew = crew; rerollCrewAt = DateTime.UtcNow; } if (rolls > 0 && crew != null) Log(string.Format("Crew: {0} reroll{1} with {2} hires ({3}){4}. The worst now: {5}{6}", rolls, rolls == 1 ? "" : "s", roll, M(spent), better.Count > 0 ? ", better than before: " + string.Join(", ", better) : "", crew.OrderBy(r => r.Rarity).ThenBy(r => r.Power).FirstOrDefault(), pausedFor != null ? " (paused for: " + pausedFor + ")" : "")); if (rolls == 0 && (pausedFor != null || rerollShort)) Neutral(); if (pausedFor != null && rolls == 0) { pausedIdle = pausedFor; pausedIdleAt = DateTime.UtcNow; } if (pausedFor != null) { nextReroll = DateTime.UtcNow; info.Status = "Rerolls paused for: " + pausedFor + "."; } else if (rolls >= 500) nextReroll = DateTime.UtcNow; info.Status = (done ? "Rerolls done: " : "Rerolls wait: ") + stop + "."; if (done) nextReroll = DateTime.UtcNow.AddMinutes(30); if (statsWait) nextReroll = ownReadAt.AddHours(statsSoon ? 1 : 6); if (statsWait && statsSoon && ownConfirmAt.AddHours(1) > nextReroll) nextReroll = ownConfirmAt.AddHours(1); if (skipWait) nextReroll = rerollSkip.Select(x => x.Item3).DefaultIfEmpty(DateTime.UtcNow.AddMinutes(30)).Min(); if (awayWait) nextReroll = DateTime.UtcNow.AddMinutes(5); nextReroll = DateTime.UtcNow.AddMinutes(listShortFails <= 1 ? 5 : listShortFails == 2 ? 10 : 20); info.Status = "Rerolls stuck since " + listShortSince.ToLocalTime().ToString("HH:mm") + ": " + stop.Replace("no Mythic or better dismissed: ", "") + "."; string said = rerollShort ? "saving up " + roll + (keep > 0 ? " and keeping " + M(keep) : "") : stop; if (rerollSaid != said) { rerollSaid = said; if (!only) Log("Crew: rerolls " + (done ? "done" : "wait") + " - " + stop); } var skipped = rerollSkip.Where(x => x.Item3 > DateTime.UtcNow).ToList(); if (skipped.Count > 0) info.Status += " Left alone for now (its DISMISS kept failing): " + string.Join(", ", skipped.Select(x => x.Item1 + " until " + x.Item3.ToLocalTime().ToString("HH:mm"))) + "."; if (finished && !only) RerollsFirstDone(stop); if (only) OnlyGo(done, stop, rolls, keep); internal sealed class AutoRun public string Replaced = ""; public int ReplacedRarity = -1; public List<Game.CrewRow> Before; public DateTime StartedAt; public double Spent = -1, SpentBefore; public int Hires = -1, HiresBefore; DateTime autoRollNext = DateTime.MinValue; DateTime autoRollRest = DateTime.MinValue; const int AutoRollMinutes = 3; bool AutoRollOn { get { return S.CrewAutoRoll && S.CrewReroll && !autoRollOff; } } bool AutoRollMay(bool byStats, int rarity, int best) var now = DateTime.UtcNow; return AutoRollOn && !byStats && rarity < best && now >= autoRollNext && now >= autoRollRest; bool AutoRollHoldsBank() { return autoRun != null && autoRun.Running && !FightOnly; } const string RollStopTask = "stop auto roll"; const int RollStopSeconds = 15; DateTime rollStopNext = DateTime.MinValue; bool RollUnwanted { get { return autoRun != null && autoRun.Running && !(S.CrewAutoRoll && S.CrewReroll); } } bool RollStopDue(DateTime now) { return RollUnwanted && now >= rollStopNext && Can(Tab.Crew); } if (run == null || !run.Running) return; try { done = StopOwnRoll(!S.CrewReroll ? "the rerolls are off" : "\"Use the game's Auto Roll\" is off", out why); } catch (NeverPressException) { done = false; why = "the press was refused"; } catch (UserBusyException) { throw; } catch (StopException) { throw; } catch (NeedUserException) { throw; } catch (Exception e) { RollStopMissed(run, e is WindowChangedException ? e.Message : "an error: " + e.Message, false); throw; } if (!done) RollStopMissed(run, why, true); void RollStopMissed(AutoRun run, string why, bool say) if (autoRun != run || !run.Running) return; if (say) game.NoteFailure("the Auto Roll's STOP: " + why); if (++rollStopTries >= 3 && RollStopGivesUp(S.Bank, S.BankAt, Can(Tab.Bank))) Log("Auto Roll: STOP not pressed on " + run.Replaced + "'s slot (" + why + ", 3 tries) - left to the game; the bank goes on, its deposit ends the roll"); int wait = rollStopTries >= 3 ? 10 : 1; rollStopNext = DateTime.UtcNow.AddMinutes(wait); if (say) Log("Auto Roll: STOP not pressed on " + run.Replaced + "'s slot (" + why + ") - again in " + (wait == 1 ? "a minute" : wait + " minutes")); internal static bool RollStopGivesUp(bool bank, long bankAt, bool bankInMenu) { return bank && bankAt <= 0 && bankInMenu; } bool StopOwnRoll(string reason, out string why) if (run == null || !run.Running) return true; if (!game.OpenTab(Tab.Crew)) { why = "the Crew page didn't open"; return false; } if (game.LookAtCrew() == null) { why = "the Crew page didn't read"; return false; } var row = FindOwnRoll(run, out told); Log("Auto Roll: " + run.Replaced + "'s slot isn't rolling any more - " + reason); if (told == "yours" && run.Restored) { LeaveRun(run, "the slot rolling now isn't the bot's roll from before"); return true; } why = told == "yours" ? "its slot wasn't found in the list" : "couldn't tell whether it still rolls"; game.Snapshot("crew auto roll stop not found", 60); if (row.RollSpent >= 0) run.Spent = row.RollSpent; if (row.RollHires >= 0) run.Hires = row.RollHires; if (row.Name.Length > 0) run.Now = row.ToString(); if (!game.StopAutoRoll(row)) { why = "its STOP didn't take"; game.Snapshot("crew auto roll stop failed", 60); return false; } Log("Auto Roll: stopped on " + run.Replaced + "'s slot - " + reason + "; " + (run.HiresBefore + Math.Max(0, run.Hires)) + " hires, " + M(run.SpentBefore + Math.Max(0, run.Spent)) + " spent" + (run.Now.Length > 0 ? ", " + run.Now + " stays" : "")); internal static bool RollMayBe(int rollFor, bool tierRead, CrewRoll tier, int target, CrewRoll runTier) return (rollFor < 0 || rollFor == target) && (!tierRead || tier == runTier); internal static bool RunMayBe(int rollFor, bool tierRead, CrewRoll tier, int hires, double spent, AutoRun run) if (!RollMayBe(rollFor, tierRead, tier, run.Target, run.Tier)) return false; if (!run.Restored) return true; return rollFor >= 0 && (hires < 0 || run.Hires < 0 || hires >= run.Hires) && (spent < 0 || run.Spent < 0 || spent >= run.Spent); Game.CrewRow FindOwnRoll(AutoRun run, out string told) Func<Game.CrewRow, bool> mine = r => RunMayBe(r.RollFor, r.RollTierRead, r.RollTier, r.RollHires, r.RollSpent, run); var any = game.FindRollingRow(out page); if (page != null && page.Page && page.TopInSight && !page.AutoRolls) told = "over"; if (mine(any)) return any; var row = game.FindRollingRow(out page, mine); if (row == null && page != null && page.Page && page.TopInSight) told = "yours"; Count.CrewCash += run.SpentBefore + Math.Max(0, run.Spent); if (autoRun == run) autoRun = null; rollStopTries = 0; rollStopNext = DateTime.MinValue; void LeaveRun(AutoRun run, string why) Log("Auto Roll: " + why + " - left to you"); if (autoRun == run) autoRun = null; rollStopTries = 0; rollStopNext = DateTime.MinValue; bool Finishing { get { return finishing && !stopAgain && finishEnd - Environment.TickCount > 0; } } if (run == null || !run.Running) return; bool done = false, turn = false; finishEnd = Environment.TickCount + RollStopSeconds * 1000; State("Stopping: the game's Auto Roll first"); while (Finishing && game.Win.UserActiveWithin(1000)) Thread.Sleep(200); if (!Finishing) why = "you were using the PC"; else if (t != null && !(turn = t.Take(this, 100, () => !Finishing, Math.Max(1, finishEnd - Environment.TickCount)))) why = "another account had the mouse"; else if (!game.Win.Find()) why = "Roblox isn't open"; else if (!game.Win.BeginAct()) why = "Roblox couldn't be brought to the front"; while (!done && Finishing) done = StopOwnRoll("the bot was stopped", out why); if (!done) game.Wait(1000); catch (UserBusyException) { why = "you used the PC"; } finally { acting = false; game.Win.EndAct(S.GiveFocusBack, false); } catch (StopException) { } catch (Exception e) { why = e.Message; } if (!done && autoRun == run) Log("Auto Roll: STOP not pressed on " + run.Replaced + "'s slot (" + (stopAgain ? (keepRoll ? "the bot was closed" : "you pressed Stop again") : why ?? "it took too long") + ") - the bot stopped anyway; the next Start looks at it again"); var now = DateTime.UtcNow; string want = r != null && r.Running ? RunText(r, now) : ""; string had = S.AutoRollMine ?? ""; if (want.Length > 0 && had.Length > 0 && RunBody(want) == RunBody(had) && RunSeen(had, out seen) && now - seen < TimeSpan.FromMinutes(10)) return; var r = RunFrom(S.AutoRollMine, DateTime.UtcNow); if (!string.IsNullOrEmpty(S.AutoRollMine)) { S.AutoRollMine = ""; SaveSettings(); } rollStopTries = 0; rollStopNext = DateTime.MinValue; Log("Auto Roll: the bot's own roll on " + r.Replaced + "'s slot may still run (from before) - the bot looks for its slot before it watches it"); internal static string RunText(AutoRun r, DateTime seen) var c = CultureInfo.InvariantCulture; return string.Join("|", seen.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", c), ((int)r.Tier).ToString(c), r.Target.ToString(c), r.SpentBefore.ToString("R", c), r.HiresBefore.ToString(c), r.Spent.ToString("R", c), r.Hires.ToString(c), r.ReplacedRarity.ToString(c), r.Starts.ToString(c), (r.Replaced ?? "").Replace("|", " ").Replace("\r", " ").Replace("\n", " ")); static string RunBody(string text) { int bar = text.IndexOf('|'); return bar < 0 ? text : text.Substring(bar); } static bool RunSeen(string text, out DateTime seen) int bar = text.IndexOf('|'); return DateTime.TryParse(bar < 0 ? text : text.Substring(0, bar), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out seen); internal static AutoRun RunFrom(string text, DateTime now) if (string.IsNullOrEmpty(text)) return null; var p = text.Split(new[] { '|' }, 10); if (p.Length != 10) return null; var c = CultureInfo.InvariantCulture; int tier, target, hiresBefore, hires, replacedRarity, starts; double spentBefore, spent; if (!RunSeen(text, out seen) || now - seen > TimeSpan.FromHours(6) || seen - now > TimeSpan.FromMinutes(10)) return null; if (!int.TryParse(p[1], NumberStyles.Integer, c, out tier) || tier < 0 || tier > 2 || !int.TryParse(p[2], NumberStyles.Integer, c, out target) || target < 0 || !double.TryParse(p[3], NumberStyles.Float, c, out spentBefore) || !int.TryParse(p[4], NumberStyles.Integer, c, out hiresBefore) || !double.TryParse(p[5], NumberStyles.Float, c, out spent) || !int.TryParse(p[6], NumberStyles.Integer, c, out hires) || !int.TryParse(p[7], NumberStyles.Integer, c, out replacedRarity) || !int.TryParse(p[8], NumberStyles.Integer, c, out starts)) return null; return new AutoRun { Tier = (CrewRoll)tier, Target = target, SpentBefore = Math.Max(0, spentBefore), HiresBefore = Math.Max(0, hiresBefore), Spent = spent, Hires = hires, ReplacedRarity = replacedRarity, Starts = Math.Max(1, starts), Replaced = p[9], Running = true, StartedAt = seen, Restored = true }; bool StartRollInto(Game.CrewRow emptied, CrewRoll roll, int best, Game.CrewRow worst, List<Game.CrewRow> crew) if (emptied == null) return false; if (emptied.Auto == null) emptied = game.EmptyRowNear(emptied.Anchor.CenterY) ?? emptied; bool started = game.StartAutoRoll(emptied, null, roll, best); autoRollNext = DateTime.UtcNow.AddMinutes(AutoRollMinutes); Log("Auto Roll: " + game.AutoRollWhy + " - three looks in a row, so off until the bot is started again; the bot hires itself as before"); autoRollNext = DateTime.UtcNow.AddMinutes(10); Log("Auto Roll: " + game.AutoRollWhy + " (look " + autoRollGrey + " of 3) - looking again in 10 minutes; the bot hires itself meanwhile"); autoRollNext = DateTime.UtcNow.AddMinutes(30); Log("Auto Roll: not started on " + worst.Name + "'s slot - " + game.AutoRollWhy + "; the bot hires into it itself (Auto Roll again in 30 minutes)"); var run = autoRun != null && !autoRun.Running && autoRun.Tier == roll && autoRun.Target == best ? autoRun : new AutoRun { Tier = roll, Target = best }; if (run.Spent > 0) run.SpentBefore += run.Spent; if (run.Hires > 0) run.HiresBefore += run.Hires; run.Spent = -1; run.Hires = -1; run.Replaced = worst.Name; run.ReplacedRarity = run.Starts == 0 ? worst.Rarity : run.ReplacedRarity; run.Before = crew.ToList(); run.Restored = false; run.Misses = 0; run.StartedAt = DateTime.UtcNow; rollStopTries = 0; rollStopNext = DateTime.MinValue; Log("Auto Roll: " + (run.Starts == 1 ? "started" : "started again (" + run.Starts + ")") + " on " + worst + "'s slot - " + roll + " hires until " + Game.RarityTitle(best) + " or better, this slot only" + (run.SpentBefore > 0 ? " (" + M(run.SpentBefore) + " spent there so far)" : "") + "; the bank waits while it rolls"); SnapshotOnce("crew auto roll started"); bool AutoRollHere(CrewInfo info, double keep, bool fromCache, ref List<Game.CrewRow> crew, out string wait) Game.CrewRow rolling = null; var p = game.LookAtCrew(); rolls = p != null && p.AutoRolls; if (p != null) rolling = p.Rolling.FirstOrDefault(r => !r.Cut); else { rolls = game.CrewRollsSeen; rolling = game.CrewRollingSeen; } var now = DateTime.UtcNow; switch (RollState(rolls, autoRun != null && autoRun.Running)) wait = "a slot is auto-rolling in the game (STOP ALL AUTO ROLLS shows) - the rerolls wait until it's done"; SayOnce("auto roll yours " + now.ToString("yyyyMMddHH"), "Crew: " + wait); nextReroll = now.AddMinutes(5); rolling = FindOwnRoll(run, out told); if (told == "yours" || ++run.Misses >= 5) LeaveRun(run, told == "yours" ? "the slot rolling now isn't the bot's roll from before" : "the bot's roll from before wasn't found in the list (5 looks)"); wait = "a slot is auto-rolling in the game that the bot didn't start - the rerolls wait until it's done"; nextReroll = now.AddMinutes(5); wait = "the bot's Auto Roll from before: its slot wasn't found in the list"; game.Snapshot("crew auto roll row not found", 60); nextReroll = now.AddMinutes(1); run.Restored = false; run.Misses = 0; if (run.Before == null) run.Before = crew.ToList(); Log("Auto Roll: the bot's own roll from before still rolls on " + run.Replaced + "'s slot - watched again"); else rolling = game.FindRollingRow(out page); wait = "the game's Auto Roll rolls, but its slot wasn't found in the list"; game.Snapshot("crew auto roll row not found", 60); nextReroll = now.AddMinutes(1); if (rolling.RollSpent >= 0) autoRun.Spent = rolling.RollSpent; if (rolling.RollHires >= 0) autoRun.Hires = rolling.RollHires; if (rolling.Name.Length > 0) autoRun.Now = rolling.ToString(); double total = autoRun.SpentBefore + Math.Max(0, autoRun.Spent); double cashNow = CashNow(); double floor = KeepFloorRead(keep, false); string stopWhy = RollOverLimit(autoRun.SpentBefore, autoRun.Spent, S.AutoRollLimit) ? "its rolls spent " + M(total) + " (the limit is " + M(S.AutoRollLimit) + ")" : floor > 0 && cashNow >= 0 && cashNow < floor ? "the cash on hand is down to what's kept for the next property (" + M(floor) + ")" : null; bool stopped = game.StopAutoRoll(rolling); Log("Auto Roll: " + (stopped ? "stopped" : "STOP didn't take on") + " the slot - " + stopWhy + ", " + (autoRun.HiresBefore + Math.Max(0, autoRun.Hires)) + " hires" + (autoRun.Now.Length > 0 ? "; " + autoRun.Now + " stays" : "")); if (!stopped) { game.Snapshot("crew auto roll stop failed", 60); nextReroll = now.AddMinutes(1); wait = "the Auto Roll's STOP didn't take"; return true; } autoRollRest = now.AddHours(1); nextReroll = autoRollRest; wait = "the game's Auto Roll stopped at its limit - the next in an hour"; double price = crewHirePrice[(int)autoRun.Tier]; if (price > 0 && !RollUnwanted && cashNow >= 0 && cashNow < price * 3 + Math.Max(0, floor) && RollCash(autoRun.Tier, keep, total, out floor)) game.OpenTab(Tab.Crew); nextReroll = now.AddMinutes(1); wait = "the game's Auto Roll rolls " + autoRun.Replaced + "'s slot for " + Game.RarityTitle(autoRun.Target) + " or better: " + (autoRun.HiresBefore + Math.Max(0, autoRun.Hires)) + " hires, " + M(total) + " spent" + (autoRun.Now.Length > 0 ? ", now " + autoRun.Now : ""); if (fromCache) { crew = ReadCrewForRerolls(info); if (crew == null) { wait = "couldn't read the crew page"; return true; } } var got = RollLeft(autoRun.Before, crew, out fresh); double total = autoRun.SpentBefore + Math.Max(0, autoRun.Spent); int hires = autoRun.HiresBefore + Math.Max(0, autoRun.Hires); Log("Auto Roll: the roll on " + autoRun.Replaced + "'s slot " + (autoRun.Restored ? "from before is over" : "ended, but the henchman it left wasn't told apart (" + fresh + " new in the list)") + " - about " + hires + " hires, " + M(total) + " spent"); autoRollNext = DateTime.MinValue; if (got.Rarity >= autoRun.Target) Log("Auto Roll: got " + got.Name + " (" + got.RarityName + ") after " + (hires > 0 ? "about " + hires : "some") + " hires, " + M(total) + " spent"); Count.CrewCash += total; Count.Rerolls++; PingHenchman(got, autoRun.ReplacedRarity, false, crew); autoRollNext = DateTime.MinValue; if (RollOverLimit(autoRun.SpentBefore, autoRun.Spent, S.AutoRollLimit)) Log("Auto Roll: the slot's rolls stopped with " + got + " after " + M(total) + " (the limit is " + M(S.AutoRollLimit) + ") - the next in an hour"); autoRollRest = now.AddHours(1); nextReroll = autoRollRest; wait = "the game's Auto Roll spent its limit on that slot - the next in an hour"; Log("Auto Roll: stopped short with " + got + " after " + (hires > 0 ? "about " + hires : "some") + " hires, " + M(total) + " spent (the cash on hand ran out, most likely)" + " - started again " + (now >= autoRollNext ? "now" : "at " + autoRollNext.ToLocalTime().ToString("HH:mm"))); nextReroll = autoRollNext; wait = "the game's Auto Roll starts again at " + autoRollNext.ToLocalTime().ToString("HH:mm"); internal static string RollState(bool rolls, bool ours) if (rolls) return ours ? "watch" : "yours"; return ours ? "ended" : ""; internal static bool RollOverLimit(double spentBefore, double spent, long limit) return limit > 0 && spentBefore + Math.Max(0, spent) >= limit; internal static Game.CrewRow RollLeft(List<Game.CrewRow> before, List<Game.CrewRow> now, out int fresh) if (before == null || now == null) return null; var left = before.ToList(); var news = new List<Game.CrewRow>(); var same = left.FirstOrDefault(b => Game.NamedLike(b, r)) ?? left.FirstOrDefault(b => b.Rarity == r.Rarity && Game.WholeNameSame(b.Name, r.Name)) ?? left.FirstOrDefault(b => Game.SameHenchman(b, r)); if (same != null) left.Remove(same); return news.Count == 1 ? news[0] : null; internal static double RollStake(double price, long limit, double spent) if (price <= 0) return 0; return Math.Max(price, Math.Min(limit > 0 ? limit - spent : price * 100, price * 100)); internal static double RollWant(double stake, double cash, double banked, double keep) return Math.Min(stake - Math.Max(0, cash), banked - Math.Max(0, keep)); internal static bool RollTakes(double want, double cash, double price) return want > 0 && cash + want >= price && (want >= price || cash < price); internal static long RollAmount(double want, double cash, double banked, double keep) long amount = AmountToWithdraw(cash + want, cash, banked); if (amount <= 0) return 0; double free = Math.Floor(banked * 0.995 - Math.Max(0, keep)); return free < 1 ? 0 : (long)Math.Min(amount, free); bool RollCash(CrewRoll tier, double keep, double spent, out double floor) floor = Math.Max(0, keep); if (RerollOnly) return false; double stake = RollStake(crewHirePrice[(int)tier], S.AutoRollLimit, spent); if (stake <= 0 || !CanWithdraw() || BankFee() != 0) return false; if (cash < 0) return false; using (var f = game.Capture()) banked = game.ReadHeader(f).Banked; if (banked < 0) return false; floor = KeepOnHand(keep, banked); if (banked <= 0) return false; double want = RollWant(stake, cash, banked, keep); if (!RollTakes(want, cash, crewHirePrice[(int)tier])) return false; long amount = RollAmount(want, cash, banked, keep); if (amount <= 0 || !RollTakes(amount, cash, crewHirePrice[(int)tier])) return false; string typed = game.WithdrawAmount(amount, out why, out seen); if (typed != null && seen) Log("Bank: took " + M(amount) + " out for the crew's Auto Roll (it rolls on the cash on hand)"); floor = KeepOnHand(keep, banked - amount); if (typed != null) { cashOut = true; nextBank = DateTime.UtcNow; floor = KeepOnHand(keep, banked - amount); } SayOnce("auto roll cash " + (why ?? "unseen").Split('(', '"')[0], "Bank: no cash came out for the Auto Roll (" + (why ?? "the top bar showed none coming out") + ")"); bool RerollsLead() { return S.RerollsFirst || (S.Events && EventOn(GameEvent.RecruitLuck)); } var roll = LuckRollFor(S.CrewRollType, S.CrewLuckRoll, S.Events && EventOn(GameEvent.RecruitLuck)); return roll == S.CrewRollType || LuckRollPaid(roll) ? roll : S.CrewRollType; bool LuckRollPaid(CrewRoll roll) double price = crewHirePrice[(int)roll]; if (price <= 0 || last == null) return true; double cash = rerollCash >= 0 ? rerollCash : last.Cash; return cash < 0 || cash >= price; if (last.Cash < 0) return true; return Spendable(last) >= price + (rerollEmpty ? 0 : PropertyKeep()); internal static CrewRoll EmptySlotHire(CrewRoll now, CrewRoll usual, double price, double cash) return now != usual && cash >= 0 && price > cash ? usual : now; bool LuckRollShort() { return S.Events && EventOn(GameEvent.RecruitLuck) && LuckRollFor(S.CrewRollType, S.CrewLuckRoll, true) != S.CrewRollType && RollNow() == S.CrewRollType; } internal static CrewRoll LuckRollFor(CrewRoll usual, LuckRoll luck, bool lucky) if (!lucky || luck == LuckRoll.Same) return usual; return luck == LuckRoll.Elite ? CrewRoll.Elite : CrewRoll.Professional; public const int BreakMinutes = 10; DateTime fullBreakAt = DateTime.MinValue; internal static string FullBarBreak(List<string> due, Header h) bool energyFull = h.EnergyMax > 0 && h.Energy >= h.EnergyMax; bool staminaFull = h.StaminaMax > 0 && h.Stamina >= h.StaminaMax; var tasks = due.Where(t => (energyFull && (t == "heists" || t == "jobs")) || (staminaFull && (t == "takedown" || t == "give stamina" || t == "bosses" || t == "fights"))).ToList(); return tasks.Count > 0 ? string.Join(", ", tasks) : null; internal static string TimedBreak(DateTime now, DateTime since, bool bank, bool shopDue) if (now - since < TimeSpan.FromMinutes(BreakMinutes)) return shopDue ? "shop" : null; var tasks = new List<string>(); if (bank) tasks.Add("bank"); if (shopDue) tasks.Add("shop"); return string.Join(", ", tasks); void RerollsFirstDone(string why) if (!S.RerollsFirst) return; Log("Crew: " + why + " - the rerolls went back from First to On"); readonly List<Tuple<string, int, DateTime>> rerollSkip = new List<Tuple<string, int, DateTime>>(); internal static bool Dismissable(Game.CrewRow r) { return !r.Training && !r.Stationed && !r.Recovering; } bool Skipped(Game.CrewRow r) var now = DateTime.UtcNow; rerollSkip.RemoveAll(x => x.Item3 <= now); return rerollSkip.Any(x => SkipNames(x, r)); DateTime SkipUntil(Game.CrewRow r) return rerollSkip.Where(x => SkipNames(x, r)).Select(x => x.Item3).DefaultIfEmpty(DateTime.UtcNow).Max(); static bool SkipNames(Tuple<string, int, DateTime> x, Game.CrewRow r) return x.Item2 == r.Rarity && (Game.WholeNameSame(x.Item1, r.Name) || (Parse.Key(x.Item1).Length < 3 && Parse.Key(r.Name).Length < 3)); void OnlyGo(bool done, string why, int rolls, double keep) var now = DateTime.UtcNow; if (rolls > 0) { onlyFails = 0; onlyProgressAt = now; } if (why == null && rolls > 0) return; if (rerollerBreak != null) { onlyFails = 0; onlyProgressAt = now; return; } if (autoRun != null) { onlyFails = 0; onlyProgressAt = now; if (nextReroll < now) nextReroll = now.AddMinutes(1); return; } if (done) { EndRerollOnly(why, true); return; } if (cash < 0 || cash >= crewHirePrice[(int)RollNow()] + keep) { nextReroll = now; return; } EndRerollOnly(why, true); if (onlyFails >= 3 || now - onlyProgressAt > TimeSpan.FromHours(1)) { EndRerollOnly(why + " (" + onlyFails + " tries in a row)", false); return; } if (nextReroll < now.AddMinutes(1)) nextReroll = now.AddMinutes(onlyFails == 1 ? 1 : 2); Log("Only rerolling: " + (why ?? "nothing rerolled") + " - trying again at " + nextReroll.ToLocalTime().ToString("HH:mm")); const string CrewBusy = "the game says the crew is being updated (\"try again shortly\")"; Game.CrewRow HireForReroll(Game.CrewRow empty, CrewRoll roll, double cash, double keep, out CrewRoll used, out double paid) var got = game.HireInto(empty, roll, false, cash, keep, out used, out paid); if (game.HireBusy) return null; if (got == null && paid <= 0) var again = game.EmptyRowNear(empty.Anchor.CenterY); if (again != null) got = game.HireInto(again, roll, false, cash, keep, out used, out paid); if (got == null && game.HiredUnseen == null && (paid <= 0 || rerollHireFails >= 2)) game.Snapshot(paid > 0 ? "crew hire no recruit" : "crew hire not pressed", 60); string OtherTaskDue(ref int readAt, bool luck = false) bool unsure = !energyRegen.HasValue || !staminaRegen.HasValue, read = false; if (Environment.TickCount - readAt > (unsure ? 10000 : 30000)) readAt = Environment.TickCount; var fresh = unsure && !luck ? ReadBarsSettled(4000, x => x.Energy >= 0 && x.Stamina >= 0) : ReadBarsSettled(0, x => true); NoteHealth(fresh.Health, fresh.HealthMax, DateTime.UtcNow); if (last == null) last = fresh; if (fresh.Level > 0) { last.Level = fresh.Level; last.Xp = fresh.Xp; last.XpNext = fresh.XpNext; } if (fresh.SkillPoints >= 0) last.SkillPoints = fresh.SkillPoints; if (fresh.Health >= 0) { last.Health = fresh.Health; last.HealthMax = fresh.HealthMax; } if (fresh.Cash >= 0) last.Cash = fresh.Cash; if (fresh.Banked >= 0) last.Banked = fresh.Banked; using (var f = game.Capture()) NoteMenuNumbers(f); if (last == null) return null; var now = DateTime.UtcNow; int e = energyRegen.Guess(now), st = staminaRegen.Guess(now); if (e >= 0) { last.Energy = e; last.EnergyMax = energyRegen.Max; } if (st >= 0) { last.Stamina = st; last.StaminaMax = staminaRegen.Max; } if (read) NoteFullBars(new Header { Energy = e, EnergyMax = energyRegen.Max, Stamina = st, StaminaMax = staminaRegen.Max }); if (luck && !GlobalBossCuts("rerolls")) return null; if (rerollCash >= 0) last.Cash = rerollCash; var due = WhatIsDue(last, false); return due.Count > 0 ? string.Join(", ", due) : null; int SortCrew(int maxMoves, out bool done) int moves = 0, noArrow = 0; var movedUp = new List<string>(); bool autoTried = ByOwnStats || DateTime.UtcNow < autoSortOffUntil, restart = false, autoPressed = false; for (int pass = 0; pass < 6 && moves < maxMoves; pass++) Game.CrewRow lastMover = null; for (int look = 0; look < 120; look++) using (var f = game.Capture()) p = game.ReadCrew(f); if (!p.Page) return moves; var inSight = p.Rows.Where(r => !r.Empty && !r.Locked && !r.Cut && r.Rarity >= 0).ToList(); var mover = Game.NextToMoveUp(inSight, out missing, ByOwnStats ? ownStats : null, SortSkipped); if (mover != null && !autoTried) if (game.AutoSortCrew(out why)) { Log("Crew: AUTO SORT CREW pressed (the higher rarities on top)"); restart = autoPressed = true; break; } if (firstSeen.Add("crew auto sort " + why)) Log("Crew: AUTO SORT CREW not pressed (" + why + ") - sorting with the arrows"); if (moves >= maxMoves) return moves; if (lastMover != null && Game.SameHenchman(lastMover, mover) && Math.Abs(lastMover.Anchor.CenterY - mover.Anchor.CenterY) < 4) sortStuck.Add(Tuple.Create(mover.Name, mover.Rarity, DateTime.UtcNow.AddHours(1))); Log("Crew: an up arrow didn't move " + mover + " - it stays where it is for an hour, the sort goes on without it"); SnapshotOnce("crew arrow no move"); string who = (mover.Name.Length > 0 ? mover.Name : "a henchman") + " (" + mover.RarityName + ")"; if (!movedUp.Contains(who)) movedUp.Add(who); string sig = string.Join(",", p.Rows.Select(r => r.Empty ? "empty" : r.Locked ? "locked" : r.Key)); if (sig == lastSig) { top = true; break; } game.ScrollPage(0.62, game.ListNotches(3)); if (moves == before) { done = true; break; } if (autoPressed && moves > 0) autoSortOffUntil = DateTime.UtcNow.AddHours(24); Log("Crew: AUTO SORT CREW left the rarities out of order - only the arrows sort for the next 24 hours"); Log("Crew: sorted by rarity" + (ByOwnStats && ownStats.Count > 0 ? " and own stats" : "") + ", " + moves + " move" + (moves == 1 ? "" : "s") + " up: " + string.Join(", ", movedUp.Take(5)) + (movedUp.Count > 5 ? " and " + (movedUp.Count - 5) + " more" : "")); if (noArrow > 0 && moves == 0 && firstSeen.Add("crew arrows")) { Log("Crew: couldn't find the up arrows to sort the crew"); SnapshotOnce("crew arrows not found"); } bool ByOwnStats { get { return S.CrewReroll && S.CrewRerollStats > 0; } } readonly List<Tuple<string, int, DateTime>> sortStuck = new List<Tuple<string, int, DateTime>>(); DateTime autoSortOffUntil; bool SortSkipped(Game.CrewRow r) var now = DateTime.UtcNow; sortStuck.RemoveAll(x => x.Item3 <= now); return sortStuck.Any(x => x.Item2 == r.Rarity && Game.WholeNameSame(x.Item1, r.Name)); DateTime nextBriefcases, nextEvents; string badgeSig, eventsSig; const int UnreadEventSeconds = 10 * 60; readonly Dictionary<GameEvent, DateTime> eventsOn = new Dictionary<GameEvent, DateTime>(); string sig = Game.BadgeSignature(Game.FindBadges(f)); noBadges = sig.Length == 0 ? noBadges + 1 : 0; if (sig.Length == 0 && noBadges < 3) return; if (badgeSig.Length == 0 && eventsOn.Count > 0) { eventsOn.Clear(); eventsSig = ""; Log("Events: none running now"); } bool EventOn(GameEvent e) return eventsOn.TryGetValue(e, out until) && DateTime.UtcNow < until; List<Game.EventBadge> badges; using (var f = game.Capture()) badges = Game.FindBadges(f); using (var f = game.Capture()) badges = Game.FindBadges(f); string sig = Game.BadgeSignature(badges); if (sig == eventsSig) return; eventsOn.Remove(GameEvent.RecruitLuck); Log(Game.LuckBadge(badges) ? "Events: the badges changed - read again before more rerolls" : "Events: recruitment luck's badge is gone - the other tasks go first again"); if (!S.Events || !S.Bosses || !Can(Tab.Bosses) || !eventsOn.TryGetValue(GameEvent.BossDamage, out until) || DateTime.UtcNow >= until) return false; return bossesUp || bossBackAt < until || bossesReadAt == DateTime.MinValue; bool CratesHeld(DateTime now) if (!S.Events || EventOn(GameEvent.CrateLuck)) return false; if (!DateTime.TryParse(S.CratesOpenedAt, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out at)) return false; return now < at.AddHours(24); var now = DateTime.UtcNow; nextEvents = now.AddMinutes(30); List<Game.EventBadge> badges; using (var f = game.Capture()) badges = Game.FindBadges(f); string sig = Game.BadgeSignature(badges); badgeSig = eventsSig = sig; if (badges.Count == 0) { if (eventsOn.Count > 0) { eventsOn.Clear(); Log("Events: none running now"); } return; } var read = game.ReadEvents(badges); var before = new HashSet<GameEvent>(eventsOn.Where(kv => kv.Value > now).Select(kv => kv.Key)); foreach (var e in read.Where(e => e.Kind != GameEvent.Other)) var until = now.AddSeconds(e.SecondsLeft > 0 ? e.SecondsLeft : UnreadEventSeconds); if (!eventsOn.TryGetValue(e.Kind, out had) || had < until) eventsOn[e.Kind] = until; int soonest = read.Select(e => e.SecondsLeft > 0 ? e.SecondsLeft : UnreadEventSeconds).DefaultIfEmpty(30 * 60).Min(); nextEvents = now.AddSeconds(Math.Min(30 * 60, soonest + 5)); if (!before.SetEquals(eventsOn.Keys)) Log("Events: " + (read.Count > 0 ? string.Join(", ", read.Select(e => e + EventPlan(e.Kind))) : badges.Count + " badges, their words didn't read")); if (eventsOn.ContainsKey(GameEvent.CrateLuck) && !before.Contains(GameEvent.CrateLuck)) nextCrates = now; if (eventsOn.ContainsKey(GameEvent.BossDamage) && !before.Contains(GameEvent.BossDamage) && BossesDoneUntil(now) <= now) nextBosses = now; if (eventsOn.ContainsKey(GameEvent.RecruitLuck) && !before.Contains(GameEvent.RecruitLuck)) nextReroll = now; if (eventsOn.ContainsKey(GameEvent.OpsSpeed) && !before.Contains(GameEvent.OpsSpeed)) nextOps = now; if (read.Count < badges.Count) SnapshotOnce("event badge words not read"); if (read.Any(e => e.Kind == GameEvent.Other && !e.Title.StartsWith("TRICK", StringComparison.OrdinalIgnoreCase))) SnapshotOnce("event title not known"); DateTime playtimeClaimed = DateTime.MinValue; DateTime opsReadyAt = DateTime.MinValue; readonly Dictionary<string, string> expSaid = new Dictionary<string, string>(); const int MostWantedFrom = 33; const int ExpMinutes = 30; static string UtcDay(DateTime utc) { return utc.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } static string UtcText(DateTime utc) { return utc.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture); } static DateTime UtcParse(string s) return DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out t) ? t : DateTime.MinValue; void ExpSay(string key, string line) if (expSaid.TryGetValue(key, out was) && was == line) return; string ExpWhen(DateTime now) var route = ExpPlan(now, out at); if (route == ExpRoute.Operations && at > now) return " (the operations finish at " + at.ToLocalTime().ToString("HH:mm") + ", the 2X then)"; return " (the 2X is claimed once health is almost full)"; expHeld = opsKnown = opsWaiting = bossesNow = false; opsReadyAt = DateTime.MinValue; expMissing = expClaimFails = expUnread = 0; expClaimAgain = DateTime.MinValue; healthSeen = healthSeenMax = healthBase = -1; healthSpentAt = DateTime.MinValue; void ExpNewDay(DateTime now) if (S.ExpDay == UtcDay(now)) return; bool had = S.ExpDay.Length > 0; S.ExpUsed = S.ExpMostWantedDone = false; expHeld = opsWaiting = false; expMissing = expClaimFails = expUnread = 0; if (had) nextPlaytime = now; bool ExpPending(DateTime now) if (!S.Playtime || !Can(Tab.Safehouse) || S.ExpUsed || S.ExpCardAt == "none") return false; var at = UtcParse(S.ExpCardAt); return at == DateTime.MinValue || at < now.Date.AddDays(1).AddMinutes(-10); bool ExpRunning(DateTime now) { return now < UtcParse(S.ExpUntil); } bool ExpCardReady(DateTime now) var at = UtcParse(S.ExpCardAt); return expHeld || (at != DateTime.MinValue && now >= at); enum ExpRoute { Unknown, Operations, Bosses } ExpRoute ExpPlan(DateTime now, out DateTime at) var card = expHeld ? now : UtcParse(S.ExpCardAt); if (card == DateTime.MinValue) return ExpRoute.Unknown; if (opsWaiting) { at = now; return ExpRoute.Operations; } bool ops = S.Operations && Can(Tab.Operations); if (ops && !opsKnown) return ExpRoute.Unknown; if (ops && opsReadyAt != DateTime.MinValue && opsReadyAt < now.Date.AddDays(1)) if (opsReadyAt < card) return ExpRoute.Unknown; return ExpRoute.Operations; at = card < now ? now : card; int healthSeen = -1, healthSeenMax = -1, healthBase = -1; DateTime healthSeenAt, healthBaseAt; double HealthRate { get { return (S.HealthPerHour > 0 ? S.HealthPerHour : 360) / 3600.0; } } int HealthMaxKnown { get { return healthSeenMax > 0 ? healthSeenMax : game != null ? game.BarMax(2) : -1; } } void NoteHealth(int v, int max, DateTime now) if (v < 0 || max <= 0 || v > max) return; healthSeen = v; healthSeenMax = max; healthSeenAt = now; if (v >= max || healthBase < 0 || v < healthBase) { healthBase = v >= max ? -1 : v; healthBaseAt = now; return; } double s = (now - healthBaseAt).TotalSeconds; if (s < 300 || v - healthBase < 10) return; int perHour = (int)Math.Round((v - healthBase) * 3600 / s); if (perHour <= 0 || Math.Abs(perHour - S.HealthPerHour) * 10 <= S.HealthPerHour) return; S.HealthPerHour = perHour; Log(string.Format(CultureInfo.InvariantCulture, "Health: it refills about {0:0.#} a minute (the bar full from empty in {1})", perHour / 60.0, SpanText((int)(max * 3600.0 / perHour)))); DateTime healthSpentAt = DateTime.MinValue; void HealthSpent() { healthBase = -1; healthSpentAt = DateTime.UtcNow; } int HealthNow(DateTime now) if (healthSeen >= 0 && healthSeenAt >= healthSpentAt) return (int)Math.Min(healthSeenMax, healthSeen + HealthRate * Math.Max(0, (now - healthSeenAt).TotalSeconds)); int max = HealthMaxKnown; if (healthSpentAt == DateTime.MinValue || max <= 0) return -1; return (int)Math.Min(max, HealthRate * Math.Max(0, (now - healthSpentAt).TotalSeconds)); bool ExpBarsReady(DateTime now, Header h, out string why) if (!ExpForBosses) return true; int hp = HealthNow(now), max = HealthMaxKnown; if (hp >= 0 && max > 0 && hp < max - HealthRate * 60) { why = "health " + hp + "/" + max; return false; } if (h != null && h.Stamina >= 0 && h.StaminaMax > 0) int cost = S.BossHitHealth > 0 ? S.BossHitHealth : 10; int need = hp >= 0 && max > 0 ? Math.Min(h.StaminaMax - 10, (hp - max * LowHealthPct / 100) / cost) : h.StaminaMax - 10; if (h.Stamina < need) { why = "stamina " + h.Stamina + "/" + h.StaminaMax; return false; } bool ExpForBosses { get { return S.Bosses && Can(Tab.Bosses); } } static bool ExpLastCall(DateTime now) { return now >= now.Date.AddDays(1).AddMinutes(-10); } DateTime ExpSaveFrom(DateTime now) if (!ExpPending(now) || !ExpForBosses) return DateTime.MaxValue; var route = ExpPlan(now, out at); if (route == ExpRoute.Unknown) return DateTime.MaxValue; int max = HealthMaxKnown; double fill = (max > 0 ? max : 500) / HealthRate + 300; if (route == ExpRoute.Bosses) { var late = now.Date.AddDays(1).AddMinutes(-10).AddSeconds(-fill); return at < late ? at : late; } return at.AddSeconds(-fill); bool ExpHoldsHealth(DateTime now) if (ExpRunning(now)) return S.Bosses && Can(Tab.Bosses) && !crewUnequipped && BossesDoneUntil(now) <= now; return now >= ExpSaveFrom(now); bool ExpKeepsStamina(DateTime now) { return ExpRunning(now) || now >= ExpSaveFrom(now); } int ExpHealthFloor(DateTime now) if (!ExpPending(now)) return 0; var route = ExpPlan(now, out at); int max = HealthMaxKnown; if (route == ExpRoute.Unknown || max <= 0) return 0; if (route == ExpRoute.Bosses && now >= at && S.ExpMostWantedDone) return max; var by = route == ExpRoute.Bosses ? now.Date.AddDays(1).AddMinutes(-10) : at; double floor = max - HealthRate * ((by - now).TotalSeconds - 60); return floor <= 0 ? 0 : (int)Math.Min(max, floor); bool ExpClaimDue(DateTime now, Header h) if (!ExpPending(now) || !ExpCardReady(now)) return false; if (ExpPlan(now, out at) != ExpRoute.Bosses || now < at) return false; bool mw = S.Bosses && Can(Tab.Bosses) && !crewUnequipped && BossesDoneUntil(now) <= now; if (mw && !S.ExpMostWantedDone && !ExpLastCall(now)) return false; if (ExpBarsReady(now, h, out why) || ExpLastCall(now)) return true; string claimWait = "2X EXP: waiting to claim it until " + (why.StartsWith("health") ? "health is almost full" : "there's the stamina to spend it"); if (!expSaid.TryGetValue("claim", out claimWas) || !claimWas.StartsWith(claimWait)) ExpSay("claim", claimWait + " (" + why.Split(' ')[0] + " now " + why.Split(' ')[1] + ")"); void NoteExpCard(DateTime now) int s = game.PlaytimeExpSeconds; string card = S.ExpCardAt; expMissing = s == -3 && game.PlaytimeNextSeconds != -1 ? expMissing + 1 : 0; expUnread = s == -1 || s == -3 ? expUnread + 1 : 0; if (s == -2) S.ExpUsed = true; else if (s == 0) { var was = UtcParse(S.ExpCardAt); if (was == DateTime.MinValue || was > now) S.ExpCardAt = UtcText(now); } else if (s > 0) S.ExpCardAt = UtcText(now.AddSeconds(s)); else if (expMissing >= 2 || (s == -3 && game.PlaytimeNextSeconds == -2)) S.ExpCardAt = "none"; else if (expUnread >= 3 && UtcParse(S.ExpCardAt) == DateTime.MinValue && !expHeld) S.ExpCardAt = "none"; if (card == S.ExpCardAt && used == S.ExpUsed) return; if (S.ExpUsed && !used) { if (!ExpRunning(now)) Log("Playtime rewards: today's 2X EXP is claimed already - the bosses and fights as usual"); return; } if (S.ExpCardAt == "none") Log(expMissing >= 2 || s == -3 ? "Playtime rewards: no 2X EXP among today's rewards - the bosses and fights as usual" : "Playtime rewards: couldn't read when the 2X EXP can be claimed (3 looks) - the bosses and fights as usual for now"); if (s > 0 && (card.Length == 0 || Math.Abs((UtcParse(card) - UtcParse(S.ExpCardAt)).TotalMinutes) > 5)) Log("Playtime rewards: the 2X EXP can be claimed at " + UtcParse(S.ExpCardAt).ToLocalTime().ToString("HH:mm") + " - until it's used the bosses only hit the Most Wanted"); bool ClaimHeldExp(Tab back, string why) var now = DateTime.UtcNow; if (!S.Playtime || !ExpCardReady(now)) return false; int got = game.ClaimPlaytimeRewards(true, true); expHeld = game.PlaytimeExpHeld; expHeld = opsWaiting = false; S.ExpUntil = UtcText(now.AddMinutes(ExpMinutes)); Log("Playtime rewards: claimed the 2X EXP one for " + why + (hp >= 0 ? " (health " + hp + "/" + HealthMaxKnown + ")" : "") + " - for " + ExpMinutes + " minutes stages 1-" + (MostWantedFrom - 1) + " first, no player fights or Takedown while a boss is left"); if (!S.ExpUsed && game.PlaytimeExpSeconds <= 0 && ++expClaimFails >= 3) Log("Playtime rewards: couldn't claim the 2X EXP 3 times - the bosses and fights as usual today"); game.Snapshot("2x exp claim not found", 60); void BossesAfter(List<string> due, int i) if (S.GlobalBoss && globalBossUp) { nextBosses = DateTime.UtcNow; return; } if (!S.Bosses || crewUnequipped || !Can(Tab.Bosses) || BossesDoneUntil(DateTime.UtcNow) > DateTime.UtcNow) return; nextBosses = DateTime.UtcNow; int at = due.IndexOf("bosses"); if (at >= 0 && at <= i) return; if (at > i) due.RemoveAt(at); due.Insert(i + 1, "bosses"); string EventPlan(GameEvent e) case GameEvent.RecruitLuck: return (S.CrewReroll ? ": the rerolls go first while it runs" : "") + (S.CrewLuckRoll != LuckRoll.Same ? (S.CrewReroll ? ", with " : ": ") + LuckRollFor(S.CrewRollType, S.CrewLuckRoll, S.Events) + " hires" : ""); case GameEvent.CrateLuck: return S.Crates ? ": the crates are opened now" : ""; case GameEvent.BossDamage: return S.Bosses ? ": stamina goes to the bosses, no gifts while one is up" : ""; case GameEvent.OpsSpeed: return S.Operations ? ": operations looked at twice as often" : ""; nextBriefcases = DateTime.UtcNow.AddMinutes(20); if (!game.OpenBriefcases()) nextBriefcases = DateTime.UtcNow.AddHours(1); briefcasesStopped = false; if (briefcasesSaid != "off") Log(briefcasesSaid == "on" ? "Lucky Briefcases: no longer in the game's menu - the event is over. The bot looks again every hour" : "Lucky Briefcases: not in the game's menu (no event right now). The bot looks again every hour"); if (briefcasesSaid != "on") { briefcasesSaid = "on"; Log("Lucky Briefcases: the event is on - the bot picks a briefcase with each free pick (one every 20 minutes online), never with keys"); } try { PickBriefcases(); } finally { game.FoldEvents(); } if (briefcasesStopped) { nextBriefcases = DateTime.UtcNow.AddHours(6); return; } for (int pick = 0; pick < 6; pick++) var page = ReadBriefcasesSteady(); if (page == null || !page.Page) if (page == null) SayBriefcases("the free picks didn't read the same twice - not picking now, looked at again in 10 minutes"); nextBriefcases = DateTime.UtcNow.AddMinutes(10); if (page.ButtonFree <= 0) nextBriefcases = DateTime.UtcNow.AddSeconds(page.NextSeconds > 0 ? Math.Min(page.NextSeconds + 15, 3600) : 20 * 60); if (page.ButtonFree < 0) SnapshotOnce("briefcase pick button unread"); SayBriefcases(page.ButtonFree < 0 ? "the pick button didn't read - looked at again in 20 minutes" : "no free pick right now - the next one in " + (page.NextSeconds > 0 ? Parse.Short(page.NextSeconds) : "? (its time didn't read; looked at again in 20 minutes)")); if (page.Cases.Count == 0) Log("Lucky Briefcases: a free pick, but no closed briefcase in sight - looked at again in an hour"); SnapshotOnce("briefcases none closed"); nextBriefcases = DateTime.UtcNow.AddHours(1); Game.BriefcasePage after; string paid = game.PickBriefcase(page, out after); if (page.Keys >= 0 && after != null && after.Page && after.Keys >= 0 && after.Keys < page.Keys) briefcasesStopped = true; Log("Lucky Briefcases: the keys went down (" + page.Keys + " -> " + after.Keys + ") after a pick - no more picks while this event runs"); game.Snapshot("briefcase keys went down"); nextBriefcases = DateTime.UtcNow.AddHours(6); if (paid == null) { nextBriefcases = DateTime.UtcNow.AddMinutes(10); break; } Log("Lucky Briefcases: a free pick paid " + (paid.Length > 0 ? CultureInfo.InvariantCulture.TextInfo.ToTitleCase(paid.ToLowerInvariant()) : "something (its words didn't read)")); DateTime briefcasesSaidAt = DateTime.MinValue; void SayBriefcases(string what) if (DateTime.UtcNow < briefcasesSaidAt) return; briefcasesSaidAt = DateTime.UtcNow.AddHours(1); Log("Lucky Briefcases: " + what); Game.BriefcasePage ReadBriefcasesSteady() using (var f = game.Capture()) a = game.ReadBriefcases(f); using (var f = game.Capture()) b = game.ReadBriefcases(f); return a.ButtonFree == b.ButtonFree ? b : null; DateTime globalBossPinged = DateTime.MinValue; DateTime globalBossSaidHits = DateTime.MinValue; int globalBossClaimFails; DateTime globalBossClaimAfter = DateTime.MinValue; const int GlobalBossMinutes = 30; void ResetGlobalBoss() { globalBossSaid = null; globalBossUp = false; globalBossFails = globalBossHits = 0; } bool GlobalBossHoldsExp(DateTime now) if (!S.GlobalBoss || ExpLastCall(now)) return false; bool GlobalBossCuts(string task) var now = DateTime.UtcNow; if (!S.GlobalBoss || !globalBossUp || task == "global boss" || ExpLastCall(now)) return false; return now >= nextGlobalBoss || task == "fights" || task == "bosses" || task == "takedown" || task == "give stamina"; var now = DateTime.UtcNow; nextGlobalBoss = now.AddMinutes(GlobalBossMinutes); if (!game.OpenGlobalBoss()) if (globalBossUp) { GlobalBossFailed("its page didn't open"); return; } if (globalBossSaid != "off") Log("Global Boss: not in the game's menu right now - the bot looks every " + GlobalBossMinutes + " minutes"); try { ReadGlobalBoss(now); } finally { if (!globalBossUp || ExpLastCall(DateTime.UtcNow)) game.FoldEvents(); } void GlobalBossFailed(string why) if (globalBossFails < 3) { nextGlobalBoss = DateTime.UtcNow.AddMinutes(1); return; } Log("Global Boss: " + why + " three times in a row - the rest plays again, the boss is looked at again in " + GlobalBossMinutes + " minutes"); game.Snapshot("global boss " + why, 60); nextGlobalBoss = DateTime.UtcNow.AddMinutes(GlobalBossMinutes); void ReadGlobalBoss(DateTime now) Game.GlobalBossPage a, b; using (var f = game.Capture()) a = game.ReadGlobalBoss(f); using (var f = game.Capture()) b = game.ReadGlobalBoss(f); if (!a.Page || !b.Page || a.State != b.State) if (globalBossUp) GlobalBossFailed("its page didn't read"); string who = b.Boss != null ? CultureInfo.InvariantCulture.TextInfo.ToTitleCase(b.Boss.ToLowerInvariant()) : null; if (b.State != Game.BossState.Live && globalBossUp) Log("Global Boss: it's over" + (globalBossHits > 0 ? " - " + globalBossHits + " hits on it" : "") + " - the rest plays again"); if (b.State != Game.BossState.Live) globalBossHits = 0; case Game.BossState.Settled: if (globalBossSaid != "settled") Log("Global Boss: " + (who != null ? who + " is" : "the last one was") + " beaten" + (b.Seconds > 0 ? " (its page closes in " + Parse.Short(b.Seconds) + ")" : "") + " - the bot looks for the next one every " + GlobalBossMinutes + " minutes"); globalBossSaid = "settled"; if (b.Claim != null) ClaimGlobalBoss(b, now); case Game.BossState.Upcoming: if (b.Seconds > 0) nextGlobalBoss = now.AddSeconds(Math.Min(b.Seconds + 20, 6 * 3600)); if (globalBossSaid != "upcoming") Log("Global Boss: the next one starts in " + (b.Seconds > 0 ? Parse.Short(b.Seconds) + " - the bot looks again then" : "? (its time didn't read; looked at every " + GlobalBossMinutes + " minutes)")); globalBossSaid = "upcoming"; SnapshotOnce("global boss upcoming"); case Game.BossState.Live: globalBossSaidHits = now; Log("Global Boss: " + (who != null ? who : "a boss") + " is up! (\"" + b.Status + "\") - it comes first, every stamina point goes on it" + (ExpPending(now) ? "; the 2X EXP after it" : "")); SnapshotOnce("global boss live"); if (now >= globalBossPinged.AddHours(12)) Discord.Post(Ping.GlobalBoss, "\U0001F480 Global Boss is up", (who != null ? who + " is up" : "A Global Boss is up") + " in Idle Mafia. The bot is hitting it with all its stamina.", Shot, Discord.Red, true); void ClaimGlobalBoss(Game.GlobalBossPage p, DateTime now) if (now < globalBossClaimAfter) return; SnapshotOnce("global boss claim drops"); var after = game.ClaimGlobalBossDrops(p); if (++globalBossClaimFails < 3) Log("Global Boss: pressed CLAIM DROPS, the button stayed - tried again at the next look"); Log("Global Boss: CLAIM DROPS didn't take three times - left alone for 12 hours"); game.Snapshot("global boss claim didn't take", 60); globalBossClaimFails = 0; globalBossClaimAfter = now.AddHours(12); globalBossClaimFails = 0; Log("Global Boss: claimed the drops - " + (after.Received.Count > 0 ? string.Join(", ", after.Received) : "what it gave didn't read")); SnapshotOnce("global boss drops claimed"); void HitGlobalBoss(Game.GlobalBossPage p) string stop = GlobalBossHits(p, out hits); if (hits > 0) Count.BossHits += hits; var now = DateTime.UtcNow; if (hits > 0 || stop == "stamina" || stop == "health") globalBossFails = 0; if (hits > 0 && now >= globalBossSaidHits.AddMinutes(15)) globalBossSaidHits = now; Log("Global Boss: " + globalBossHits + " hits so far"); if ((stop == "stamina" || stop == "health") && S.GlobalBossConsumables && UseConsumable(stop == "stamina")) { nextGlobalBoss = DateTime.UtcNow; return; } globalBossWaits = stop == "stamina" || stop == "health"; SayOnce("global boss stamina " + now.ToString("yyyyMMddHH"), "Global Boss: no stamina left - the rest plays until a point has refilled"); nextGlobalBoss = now.AddSeconds(20); SayOnce("global boss health " + now.ToString("yyyyMMddHH"), "Global Boss: health is low - the rest plays until it has refilled"); nextGlobalBoss = now.AddMinutes(2); case "over": nextGlobalBoss = now; break; case null: nextGlobalBoss = now; break; case "bars": GlobalBossFailed("stamina didn't read"); break; case "no attack": GlobalBossFailed("no ATTACK on its page"); break; case "nothing": GlobalBossFailed("hits took no stamina"); break; default: GlobalBossFailed(stop); break; bool FightItemsReady(DateTime now) { return S.FightItems && now >= consumablesNoneUntil0; } bool HealWithItem() { return S.FightItems && UseConsumable(false, "the fights"); } DateTime consumablesNoneUntil0 = DateTime.MinValue, consumablesNoneUntil1 = DateTime.MinValue; bool UseConsumable(bool stamina, string forWhat = "the Global Boss") var now = DateTime.UtcNow; if (now < (stamina ? consumablesNoneUntil0 : consumablesNoneUntil1)) return false; string what = stamina ? "stamina" : "health"; using (var f = game.Capture()) before = game.ReadHeader(f); if (!stamina && forWhat == "the fights") if (before.Health < 0) before = ReadBarsSettled(3000, x => x.Health >= 0); if (before.Health >= 0 && before.HealthAtLeast(50)) SayOnce("consumables health fine " + now.ToString("yyyyMMddHH"), "Consumables: health is " + before.Health + " / " + before.HealthMax + " - no health item needed, the fights go on"); try { used = game.UseConsumable(stamina, out none); } finally { game.FoldEvents(); } if (stamina) consumablesNoneUntil0 = now.AddMinutes(30); else consumablesNoneUntil1 = now.AddMinutes(30); SayOnce("consumables none " + what + " " + now.ToString("yyyyMMddHH"), "Consumables: " + (none ? "no " + what + " item left in the inventory" : "couldn't use a " + what + " item") + " - " + forWhat + " wait for the bar; looked for again in 30 minutes"); SnapshotOnce("consumable used"); var after = ReadBarsSettled(5000, x => stamina ? x.Stamina >= 0 : x.Health >= 0); int b = stamina ? before.Stamina : before.Health, a = stamina ? after.Stamina : after.Health; int max = stamina ? Math.Max(before.StaminaMax, after.StaminaMax) : Math.Max(before.HealthMax, after.HealthMax); bool worked = Game.ItemWorked(b, a, max); Log("Consumables: " + (worked ? "used a " : "pressed USE on a ") + used + " for " + forWhat + " (" + what + " " + (b >= 0 ? b.ToString() : "?") + " -> " + (a >= 0 ? a.ToString() : "?") + ")" + (worked ? "" : " - the bar didn't rise, so it wasn't used")); game.Snapshot("consumable used no change", 60); if (stamina) consumablesNoneUntil0 = now.AddMinutes(30); else consumablesNoneUntil1 = now.AddMinutes(30); string GlobalBossHits(Game.GlobalBossPage p, out int hits) var h = ReadBarsSettled(8000); if (h.Stamina < 0) return "bars"; int stamina = h.Stamina, checkAt = h.Stamina, presses = 0, misses = 0, looks = 0; for (int go = 0; go < 400; go++) if (p.YourHealthMax > 0 && p.YourHealth >= 0 ? p.YourHealth * 100 < LowHealthPct * p.YourHealthMax : h.Health >= 0 && !h.HealthAtLeast(LowHealthPct)) { hits += presses; return "health"; } if (stamina <= 0) { hits += presses; return "stamina"; } if (looks++ >= 3) { hits += presses; return "no attack"; } using (var f = game.Capture()) p = game.ReadGlobalBoss(f); if (!p.Page || p.State != Game.BossState.Live) { hits += presses; return "over"; } game.GlobalBossAttack(p); using (var f = game.Capture()) p = game.ReadGlobalBoss(f); refused = p.YourHealthMax > 0 && p.YourHealth >= 0 && p.YourHealth * 2 < p.YourHealthMax && Game.TopRightYouHospitalized(f); if (refused) { hits += presses - 1; return "health"; } if (!p.Page || p.State != Game.BossState.Live) using (var f = game.Capture()) p = game.ReadGlobalBoss(f); if (!p.Page || p.State != Game.BossState.Live) { hits += presses; return "over"; } if (presses >= 10 || stamina <= 0) h = ReadBarsSettled(5000); if (h.Stamina < 0) { hits += presses; return "bars"; } int spent = checkAt - h.Stamina; if (++misses >= 2) { game.Snapshot("global boss hits take no stamina", 60); return "nothing"; } else { misses = 0; hits += Math.Min(presses, spent); } stamina = checkAt = h.Stamina; DateTime nextEquip, nextShop, nextSafehouse; bool saidBestGear, saidDeposit; public event Action<SafehouseInfo> SafehouseRead; public PlayerStats LastGameStats; public event Action<PlayerStats> GameStatsRead; var now = DateTime.UtcNow; bool gearDue = now >= nextEquip; nextEquip = now.AddHours(6); int bought = 0, opened = 0; if (ShopOn() && now >= nextShop && Can(Tab.Shop)) bought = BuyFromShop(); if (S.Crates && now >= nextCrates && Can(Tab.Inventory) && !CratesHeld(now)) nextCrates = now.AddMinutes(20); game.CratesLeftText = S.CratesLeft; opened = Report(game.OpenCrates(out done), "Opened {0} crate(s)" + (S.Events && EventOn(GameEvent.CrateLuck) ? " during crate luck" : "")); if (game.CratesLeftText != S.CratesLeft) { S.CratesLeft = game.CratesLeftText; SaveSettings(); } if (done) S.CratesOpenedAt = now.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); if (bought > 0 || opened > 0 || gearDue) EquipThenDeposit(); bool ShopOn() { return S.ShopBuy; } double ShopKeep(bool all) if (TutorialOn) return 0; double keep = PropertyKeep(); if (keep <= 0 && S.SafehouseUpgrade && upgradePrice > 0 && Can(Tab.Safehouse)) keep = upgradePrice; if (keep <= 0 || all) return Math.Max(0, keep); double banked = CanWithdraw() && last != null && last.Banked > 0 ? last.Banked : 0; return Math.Max(0, keep - banked); string ShopKeepLine(double keep) string keptFor = keep > 0 ? (PropertyKeep() > 0 ? "the next property" : "the next safehouse upgrade") : null; string say = keptFor == null ? null : keptFor + " " + M(ShopKeep(true)); string line = say != null && say != shopKeepSaid ? "Shop: keeps " + M(keep) + " of the cash on hand for " + keptFor : null; double keepAll = ShopKeep(true), keep = KeepFloor(keepAll, true); string keepLine = ShopKeepLine(keep); if (keepLine != null) Log(keepLine); var dear = new List<Game.ShopItem>(); var goldLeft = new HashSet<string>(); var overLimit = new HashSet<string>(); Func<Game.ShopItem, bool> want = i => if (i.Price <= S.ShopGoldMax) return true; goldLeft.Add(i.ToString()); if (TutorialOn && tutorialItem != null && i.Name != null && Parse.Key(i.Name) == Parse.Key(tutorialItem)) return true; if (!S.ShopCashFor(i.Price)) { overLimit.Add(i.ToString()); return false; } var bought = game.BuyFromShop(out stock, keep, TutorialOn ? tutorialItem : null, dear, 0, want, S.ShopGoldMax > 0 && !TutorialOn); if (S.Events && EventOn(GameEvent.ShopSale)) SnapshotOnce("shop sale page"); nextShop = DateTime.UtcNow.AddSeconds(stock >= 0 ? Math.Max(120, stock + 5) : 15 * 60); bool bankPays = S.ShopWithdraw && S.Bank && BankFee() == 0; var unread = bankPays ? dear.Where(i => i.Rarity < 0 && i.Price > 0).ToList() : new List<Game.ShopItem>(); string unreadSay = unread.Count == 0 ? null : "Shop: left " + string.Join(", ", unread.Select(i => i.ToString())) + " for the cash on hand - its rarity didn't read, and the bank's cash only pays for the rarities picked"; if (unreadSay != null && unreadSay != shopRaritySaid) { Log(unreadSay); SnapshotOnce("shop rarity not read"); } if (unreadSay != null) shopRaritySaid = unreadSay; dear.RemoveAll(i => !S.ShopBankFor(i.Rarity)); if (S.ShopWithdraw && dear.Count > 0) double cheapest = dear.Min(i => i.Price), floor, spent = -1; bool took = WithdrawKeeping(cheapest, false, dear.Where(i => i.Price > 0).Sum(i => i.Price), keepAll, out floor); var more = game.BuyFromShop(out again, floor, null, null, cheapest * 0.99, i => want(i) && S.ShopBankFor(i.Rarity)); spent = more.Where(i => !i.Gold && i.Price > 0).Sum(i => i.Price); finally { if (took) DepositBack(spent); } string limitSay = overLimit.Count == 0 ? null : "Shop: left in the shop (over your price limit): " + string.Join(", ", overLimit.OrderBy(x => x)); if (limitSay != null && limitSay != shopLimitSaid) Log(limitSay); shopLimitSaid = limitSay; string goldSay = goldLeft.Count == 0 || S.ShopGoldMax <= 0 ? null : "Shop: left in the shop (over your " + S.ShopGoldMax + " gold limit): " + string.Join(", ", goldLeft.OrderBy(x => x)); if (goldSay != null && goldSay != shopGoldSaid) Log(goldSay); if (bought.Count == 0) return 0; Count.Bought += bought.Count; Log("Shop: bought " + string.Join(", ", bought.Select(i => i.ToString()))); SnapshotOnce("shop bought"); var good = bought.Where(i => S.ShopPingFor(i.Rarity)).OrderByDescending(i => i.Rarity).ToList(); Func<int, string> rarity = r => Game.Rarities[r].Substring(0, 1) + Game.Rarities[r].Substring(1).ToLowerInvariant(); Discord.Post(Ping.Shop, "\U0001F6CD️ Bought " + (good.Count == 1 ? "a " + rarity(good[0].Rarity) + " item" : good.Count + " good items") + " in the shop", string.Join("\n", good.Select(i => rarity(i.Rarity) + ": " + i)), Shot, Discord.RarityColors[Math.Min(good[0].Rarity, Discord.RarityColors.Length - 1)], true); if (!S.EquipBest || !Can(Tab.Crew)) return; if (!DoEquip() || !Can(Tab.Collection)) return; if (S.MarketPending.Length > 0 && !Settings.MarketRelistShown) Log("Black Market: " + S.MarketPending.Split('|')[0] + " was taken out of your shop to relist and is in your inventory - the bot no longer relists (the game asks a captcha now), so put it back up yourself"); if (S.MarketPending.Length > 0) { Log("Collection: no deposit while " + S.MarketPending.Split('|')[0] + " waits to go back up on the Black Market"); return; } nextEquip = DateTime.UtcNow.AddHours(6); var r = game.EquipBest(); if (r == null) return false; var b = r.After ?? new Game.CrewTotals(); if (r.Window != null) { Log("Crew: a window came up after AUTO EQUIP BEST - left alone: \"" + r.Window + "\""); return false; } Log("Crew: AUTO EQUIP BEST pressed " + r.Presses + " time" + (r.Presses == 1 ? "" : "s") + ", but nothing showed it took (" + a + " before, " + b + " after" + (r.Bare ? ", no gear on the crew" : "") + ") - no deposit into the collection this time"); game.Snapshot("auto equip best not confirmed", 60); if (!a.Read || !b.Read) Log("Crew: equipped the best gear (" + a + " before, " + b + " after)"); else if (a.Attack != b.Attack || a.Defense != b.Defense) Log(string.Format("Crew: best gear on. Attack +{0} -> +{1}, defense +{2} -> +{3}", a.Attack, b.Attack, a.Defense, b.Defense)); else if (!saidBestGear) Log("Crew: already wearing the best gear (" + b + ")"); SnapshotOnce("crew auto equip best"); var r = game.DepositCollection(); var b = r.After ?? new Game.CollectionState(); if (r.Window != null) Log("Collection: a window came up after DEPOSIT ALL - left alone: \"" + r.Window + "\""); bool more = a.Deposited >= 0 && b.Deposited > a.Deposited, stronger = a.Attack >= 0 && b.Attack > a.Attack; if (more) Count.Collected += b.Deposited - a.Deposited; Log("Collection: " + (more ? string.Format("{0} new item(s) deposited, {1} of {2}", b.Deposited - a.Deposited, b.Deposited, b.Total) : "new items deposited") + (stronger ? string.Format(". Attack +{0} -> +{1}", a.Attack, b.Attack) : "")); else if (!saidDeposit) Log("Collection: DEPOSIT ALL pressed (" + b + ")" + (!string.IsNullOrEmpty(r.Toast) ? " - \"" + r.Toast + "\"" : "")); SnapshotOnce("collection deposit"); nextSafehouse = DateTime.UtcNow.AddHours(24); var info = game.ReadSafehouse(); if (info == null || info.Found == 0) if (info != null) { Log("Couldn't read the Safehouse page"); game.Snapshot("safehouse not read", 60); } nextSafehouse = DateTime.UtcNow.AddHours(1); if (info.AttackPower > 0 && S.LastAttackPower > 0 && info.AttackPower * 3 < S.LastAttackPower) info.AttackPower = -1; if (info.DefensePower > 0 && S.LastDefensePower > 0 && info.DefensePower * 3 < S.LastDefensePower) info.DefensePower = -1; if ((info.AttackPower > 0 && info.AttackPower != S.LastAttackPower) || (info.DefensePower > 0 && info.DefensePower != S.LastDefensePower)) if (info.AttackPower > 0) S.LastAttackPower = info.AttackPower; if (info.DefensePower > 0) S.LastDefensePower = info.DefensePower; if (info.Prestige >= 0 && info.Prestige != S.LastPrestige) S.LastPrestige = info.Prestige; Log("Safehouse: prestige " + info.Prestige + " of 6"); foreach (var line in info.Lines()) Log(line); if (info.Hired >= 0) { hiredRead = info.Hired; hiredReadAt = DateTime.UtcNow; } if (info.Level > 0 && info.MaxLevel > 0 && info.Level >= info.MaxLevel) upgradePrice = 0; else if (info.UpgradeCost > 0) upgradePrice = info.UpgradeCost; if (!info.Complete) SnapshotOnce("safehouse partly read"); nextGameStats = DateTime.UtcNow.AddHours(3); var st = game.ReadPlayerStats(); if (st == null || st.Found == 0) Log("Couldn't read YOUR STATS from the game's menu"); nextGameStats = DateTime.UtcNow.AddMinutes(30); if (st.Level > 0) { statsLevel = st.Level; statsAt = DateTime.UtcNow; } if (st.Level > 0 && level > st.Level + 5) Log("Level " + st.Level + " from the game's STATS (the bot had " + level + ", too high)"); Log("Game stats (" + st.Found + " read):"); foreach (var line in st.Lines()) Log("Game stats: " + line); if (st.Level > 0 && st.Level > level && (last == null || last.Level <= 0 || last.Level < st.Level)) Log("Level " + st.Level + " from the game's STATS (the bot had " + (level > 0 ? level.ToString() : "none") + ")"); game.MenuChanged(); PropertiesLevelUp(); nextOps = DateTime.UtcNow.AddMinutes(5); if (!game.OpenTab(Tab.Operations)) { Visited(Tab.Operations, null, true); return; } int collectedBefore = Count.OpsCollected, startedBefore = Count.OpsStarted; using (var f = game.Capture()) slots = game.ReadOpSlots(f); var now = DateTime.UtcNow; var cardAt = UtcParse(S.ExpCardAt); bool cardSoon = !ExpCardReady(now) && cardAt != DateTime.MinValue && cardAt - now <= TimeSpan.FromMinutes(5); if (slots.Any(x => x.Collect != null) && ExpPending(now) && cardSoon) else if (slots.Any(x => x.Collect != null) && ExpPending(now) && ExpCardReady(now) && GlobalBossHoldsExp(now)) holdUntil = now.AddMinutes(10); ExpSay("ops gb", "2X EXP: waits for the Global Boss - the finished operations are kept for it"); else if (slots.Any(x => x.Collect != null) && ExpPending(now) && ExpCardReady(now)) if (ExpBarsReady(now, last, out why) || ExpLastCall(now)) if (ClaimHeldExp(Tab.Operations, "the finished operations, then the bosses")) using (var f = game.Capture()) slots = game.ReadOpSlots(f); else if (game.PlaytimeExpSeconds > 0 && game.PlaytimeExpSeconds <= 300 && ExpPending(now)) holdUntil = UtcParse(S.ExpCardAt); int hp = HealthNow(now), max = HealthMaxKnown; double s = why.StartsWith("health") && hp >= 0 && max > 0 ? (max - HealthRate * 60 - hp) / HealthRate : 300; holdUntil = now.AddSeconds(Math.Max(60, Math.Min(600, s))); ExpSay("ops", "2X EXP: the operations are finished - collected inside the 2X once " + (why.StartsWith("health") ? "health is almost full" : "there's the stamina to spend it") + " (" + why.Split(' ')[0] + " now " + why.Split(' ')[1] + ")"); if (hold && holdUntil < now.AddMinutes(1)) holdUntil = now.AddMinutes(1); for (int i = 0; i < 4 && !hold; i++) var s = slots.FirstOrDefault(x => x.Collect != null); string toast = game.ReadToast(1500, t => Parse.Has(t, "COLLECTED")); Log(toast != null ? "Operation done - " + Regex.Replace(toast, @"\s+[xX×]$", "") : "Collected operation \"" + s.Name + "\""); using (var f = game.Capture()) slots = game.ReadOpSlots(f); int free = slots.Count(x => !x.Locked && !x.Running && !x.Done && x.Collect == null && x.Name.Length == 0); if (slots.Any(x => x.Done && x.Collect == null) && firstSeen.Add("finished operation without collect")) Log("An operation is finished but I can't find its collect button - leaving it for you"); game.Snapshot("operation finished without collect button"); if (slots.Any(x => !x.Locked && !x.Running && !x.Done && x.Collect == null && x.Name.Length > 0)) SnapshotOnce("operation slot in an unknown state"); using (var f = game.Capture()) slots = game.ReadOpSlots(f); if (ops != null) ops(slots); bool allRunning = slots.Count > 0 && slots.Where(x => !x.Locked).All(x => x.Running); var left = slots.Where(x => x.Running && x.SecondsLeft > 0).Select(x => x.SecondsLeft).ToList(); opsReadyAt = DateTime.MinValue; if (S.Events && EventOn(GameEvent.OpsSpeed)) soonest /= 2; opsReadyAt = DateTime.UtcNow.AddSeconds(soonest); nextOps = DateTime.UtcNow.AddSeconds(Math.Min(soonest + 8, 30 * 60)); Visited(Tab.Operations, false, true, holdUntil.AddSeconds(90)); Visited(Tab.Operations, Count.OpsCollected > collectedBefore || Count.OpsStarted > startedBefore, !allRunning, soonest >= 0 ? DateTime.UtcNow.AddSeconds(soonest + 90) : DateTime.MinValue); void StartOperations(int free) var all = new List<OpRow>(); for (int page = 0; page < 30; page++) using (var f = game.Capture()) rows = game.ReadOpRows(f, page); if (page == 0) step = Game.OpsStep(rows); if (!all.Any(a => Parse.Key(a.Name) == Parse.Key(r.Name))) all.Add(r); string sig = string.Join(",", rows.Select(r => r.Name)); if (sig == lastSig) break; string only = (S.OperationOnly ?? "").Trim(); OpRow pick = only.Length > 0 ? all.FirstOrDefault(r => r.CanStart && Game.SameOp(r.Name, only)) : all.Where(r => r.CanStart && r.Seconds > 0 && r.Xp > 0).OrderByDescending(r => (double)r.Xp / r.Seconds).FirstOrDefault(); if (!opsNoneSaid) Log(only.Length > 0 ? "A slot is free but \"" + only + "\" can't be started (still locked, or not enough cash on hand?) - looking again every 15 minutes" : "A slot is free but no operation can be started (not enough cash on hand?) - looking again every 15 minutes"); if (only.Length == 0) SnapshotOnce("no operation to start"); nextOps = DateTime.UtcNow.AddMinutes(15); for (int i = 0; i < pick.Page; i++) game.OpsDown(step); for (int n = 0; n < free; n++) using (var f = game.Capture()) row = game.ReadOpRows(f, pick.Page).FirstOrDefault(r => Parse.Key(r.Name) == Parse.Key(pick.Name)); if (row == null || !row.CanStart) break; Log("Started operation \"" + row.Name + "\""); DateTime BossesDoneUntil(DateTime now) return DateTime.TryParse(S.BossesDoneUntil, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out until) && until > now ? until : now; void BossesLevelUp(int newLevel) var now = DateTime.UtcNow; if (newLevel > 0 && newLevel < S.BossesOpenLevel && BossesDoneUntil(now) > now) return; void BossesDone(DateTime until) S.BossesDoneUntil = until.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture); S.BossesOpenLevel = stagesLocked > level ? stagesLocked : 0; if (S.BossesOpenLevel > 0) SayOnce("bosses open " + S.BossesOpenLevel, "Bosses: nothing to fight now - the next stage opens at level " + S.BossesOpenLevel); nextBosses = DateTime.UtcNow.AddMinutes(20); stagesLocked = -1; stagesWalked = false; var now = DateTime.UtcNow; bool exp = ExpRunning(now), mwOnly = !exp && ExpPending(now); if (softDay != UtcDay(now)) { softDay = UtcDay(now); softTop = MostWantedFrom - 1; softMw = MostWantedFrom; } bool soften = mwOnly && StageHitsKnown(now); int floor = mwOnly ? ExpHealthFloor(now) : 0, lowest = mwOnly ? MostWantedFrom : 1; var softened = new SortedDictionary<int, string>(); var route = ExpPlan(now, out claimAt); if (mwOnly && floor > 0 && HealthNow(now) >= 0 && HealthNow(now) <= floor) ExpSay("mw", "Bosses: the Most Wanted wait - the health left is saved for the 2X EXP"); nextBosses = claimAt > now ? claimAt : now.AddMinutes(10); if (!game.OpenTab(Tab.Bosses)) return; if (st != null && st.Stage < 0 && st.StagesTab != null) { game.StagesTab(st); st = ReadStage(); } if (st == null || st.Stage < 0) Log("Bosses: the stages page didn't read" + (st != null && st.Said.Length > 0 ? " (\"" + Cut(st.Said, 160) + "\")" : "")); game.Snapshot("boss stage page not read", 60); nextBosses = DateTime.UtcNow.AddMinutes(15); int hits = 0, waits = 0, nexts = 0, stamina = -1, sinceBars = 99, stageHits = 0, stageAt = st.Stage, seeks = 0, lastBeaten = -1, idle = 0, calm = 0; bool sought = !(S.BossWeakestFirst || exp || mwOnly), noneReady = false, rescued = false, reopened = false; var hitCosts = new List<int>(); string stageBoss = st.Boss, stop = null; var done = new List<string>(); int progressAt = Environment.TickCount; for (int look = 0; look < 400 && stop == null; look++) if (Environment.TickCount - progressAt > 180000) stop = "the stages list went round 3 minutes without a hit"; game.Snapshot("boss stages went round", 60); if (st == null || st.Stage < 0) if (waits == 8 && !reopened) SnapshotOnce("boss page left"); Log("Bosses: the stages page went away under the fights - opening Bosses again"); if (game.OpenTab(Tab.Bosses)) { waits = 0; st = ReadStage(); continue; } if (++waits > 8) { stop = "the page stopped reading"; game.Snapshot("boss stage page not read", 60); break; } if (!noneReady && lastBeaten > 0 && st.Stage != lastBeaten + 1 && seeks < 4) sought = false; stageAt = st.Stage; stageBoss = st.Boss; stageHits = 0; progressAt = Environment.TickCount; if (st.Boss.Length > 0) stageBoss = st.Boss; if (st.Cooldown || ((st.DefeatedToday || st.Cleared) && waits < 10)) { waits++; game.Wait(500); st = ReadStage(); continue; } if (mwOnly ? soften && SeekSoft(ref st) : SeekReady(ref st, Math.Max(lowest, lastBeaten > 0 ? lastBeaten + 1 : 1))) { waits = 0; continue; } if (st == null || st.Stage < 0) continue; if (!st.Cleared && !st.DefeatedToday && st.Next == null && !rescued && !stagesWalked) rescued = true; sought = true; seeks++; if (SeekReady(ref st, lowest, mwOnly)) { waits = 0; continue; } if (st == null || st.Stage < 0) continue; if ((st.Cleared || st.DefeatedToday) && nexts < 2 && !stagesWalked) for (int roll = 0; roll < 6 && st != null && st.Next == null && !st.ListAt.IsEmpty; roll++) game.StagesListScroll(st, -3); game.Wait(500); st = ReadStage(); if (ListStill(sig, st)) break; if (st != null && st.Next != null) if (st == null) continue; else if (!st.Cleared && !st.DefeatedToday && st.Next != null && nexts < 2) if (calm < 4 && !stagesWalked) { calm++; game.Wait(1500); st = ReadStage(); continue; } stop = st.Cleared || st.DefeatedToday ? "cleared" : stageHits > 0 ? "no attack on a stage just hit" : "no attack"; if (sinceBars >= 5 || stamina <= 1) bool cardHealth = st.YourHealth >= 0; var h = ReadBarsSettled(15000, x => x.Stamina >= 0 && (x.Health >= 0 || cardHealth)); int max = game.BarMax(1); if (card != null && card.YourStamina >= 0 && max > 0 && card.YourStaminaMax == max) h.Stamina = card.YourStamina; if (h.Stamina < 0) { Log("Couldn't read stamina (" + game.BarLook(1) + ") - bosses wait a couple of minutes"); game.Snapshot("stamina unreadable at bosses", 60); stop = "bars"; break; } NoteHealth(h.Health, h.HealthMax, DateTime.UtcNow); if (h.Health >= 0 && !h.HealthAtLeast(LowHealthPct)) { stop = "health"; break; } if (st.YourHealthMax > 0 && st.YourHealth * 100 < LowHealthPct * st.YourHealthMax) { stop = "health"; break; } if (stamina <= 0) { stop = "stamina"; break; } NoteHealth(st.YourHealth, st.YourHealthMax, DateTime.UtcNow); floor = ExpHealthFloor(DateTime.UtcNow); int hp = HealthNow(DateTime.UtcNow); if (floor > 0 && hp >= 0 && hp - (S.BossHitHealth > 0 ? S.BossHitHealth : 10) < floor) { stop = "saving"; break; } if (mwOnly ? soften && SeekSoft(ref st) : SeekReady(ref st, Math.Max(lowest, lastBeaten > 0 ? lastBeaten + 1 : 1))) { waits = 0; continue; } if (st == null || st.Stage < 0 || st.Attack == null) continue; bool target = soften && SoftTarget(st.Stage); if (target && SoftenOk(ref st, out why)) softHit = true; if (st == null || st.Stage < 0) continue; if (stageHits == 0 && why != null && saidOnce.Add("soften " + softDay + " " + st.Stage)) Log("Bosses: stage " + st.Stage + " not brought low before the 2X EXP - " + why); if (soften && softSeeks++ < 60 && SeekSoft(ref st)) { waits = 0; continue; } int bossBefore = st.Health, youBefore = st.YourHealth; progressAt = Environment.TickCount; hits++; stageHits++; stamina--; sinceBars++; if (st != null) NoteHealth(st.YourHealth, st.YourHealthMax, DateTime.UtcNow); if (st != null && youBefore > 0 && st.YourHealth >= 0 && st.YourHealth < youBefore) hitCosts.Add(youBefore - st.YourHealth); if (st != null && st.Stage == stageAt && bossBefore > 0) bool beaten = st.DefeatedToday || st.Health == 0; int d = beaten ? (softHit ? Math.Max(bossBefore, st.Damage) : -1) : st.Health >= 0 && st.Health < bossBefore ? Math.Max(bossBefore - st.Health, st.Damage) : -1; if (d > 0) NoteStageHit(stageAt, d, DateTime.UtcNow, softHit && beaten); if (softHit && !beaten) softened[stageAt] = "stage " + stageAt + " at " + (st.Health >= 0 ? st.Health.ToString("N0", CultureInfo.InvariantCulture) : "?") + (st.HealthMax > 0 ? " / " + st.HealthMax.ToString("N0", CultureInfo.InvariantCulture) : ""); Log("Bosses: stage " + stageAt + " was beaten by a hit meant to bring it low (" + bossBefore.ToString("N0", CultureInfo.InvariantCulture) + " health left, the biggest hit lately said less) - its hits count bigger from now on"); game.Snapshot("boss beaten before the 2X", 60); if (st != null && st.Stage == stageAt && bossBefore >= 0 && st.Health == bossBefore && !st.DefeatedToday) idle++; using (var f = game.Capture()) hospital = Game.TopRightYouHospitalized(f); if (hospital) { stop = "health"; break; } if (idle >= 6) { stop = "hits change nothing (in the hospital?)"; game.Snapshot("boss hits change nothing", 60); break; } if (st != null && st.Stage == stageAt && (st.DefeatedToday || st.Health == 0)) string who = "stage " + stageAt + (stageBoss.Length > 0 ? " " + stageBoss : ""); done.Add(who + (stageHits > 1 ? " (" + stageHits + " hits)" : "")); Discord.Post(Ping.Boss, "\U0001F44A Boss beaten", who + " in " + stageHits + (stageHits == 1 ? " hit" : " hits"), Shot, Discord.Gold); if (softened.Count > 0) Log("Bosses: brought low for the 2X EXP (not beaten): " + string.Join(", ", softened.Values)); if (done.Count > 0) Log("Bosses: beat " + string.Join(", ", done) + (st != null && st.Tokens >= 0 ? " - " + st.Tokens.ToString("N0", CultureInfo.InvariantCulture) + " boss tokens" : "")); int reset = st != null && st.ResetSeconds >= 0 ? st.ResetSeconds : SecondsToUtcMidnight(); string where = st != null && st.Stage > 0 ? "stage " + st.Stage + (st.Boss.Length > 0 ? " (" + st.Boss + ")" : "") : "the next stage"; int cost = hitCosts.OrderBy(x => x).ElementAt(hitCosts.Count / 2); if (cost != S.BossHitHealth) { S.BossHitHealth = cost; SaveSettings(); } if (stop == "cleared" || stop == "no attack") stop = "no most wanted"; if (route == ExpRoute.Bosses && DateTime.UtcNow >= claimAt && stop != null && stop != "bars" && !S.ExpMostWantedDone) S.ExpMostWantedDone = true; Log("Bosses: the Most Wanted had their round - the 2X EXP next, once health is almost full"); if (stop == "saving" || stop == "no most wanted") ExpSay("mw", stop == "saving" ? "Bosses: the Most Wanted stop - the health left is saved for the 2X EXP" : "Bosses: nothing left to bring low safely - every boss waits for the 2X EXP"); nextBosses = claimAt > DateTime.UtcNow && claimAt < DateTime.UtcNow.AddHours(1) ? claimAt : DateTime.UtcNow.AddHours(1); NoteBossRound(1, int.MaxValue); SayOnce("bosses cleared " + DateTime.UtcNow.ToString("yyyyMMdd"), "Bosses: every stage you can reach is beaten today (highest " + (st != null ? st.Highest : -1) + ") - back after the game's reset at 00:00 UTC"); BossesDone(DateTime.UtcNow.AddSeconds(reset + 60)); if (saidOnce.Add("bosses noattack " + DateTime.UtcNow.ToString("yyyyMMdd") + " " + (st != null ? st.Stage : -1))) Log("Bosses: " + where + " shows no ATTACK" + (st != null && st.NeedLevel > 0 ? " - it needs level " + st.NeedLevel : " (a higher level needed?)") + (st != null ? " - highest stage " + st.Highest + ", " + (st.Next != null ? "the list's NEXT pressed " + nexts + "x" : "no NEXT in the list's sight") : "") + (level >= 180 ? " - the bot looks again in 15 minutes" : " - the bot looks again after a level-up or the game's reset") + (st != null && st.Said.Length > 0 ? " (\"" + Cut(st.Said, 160) + "\")" : "")); SnapshotOnce("boss stage locked after the reset"); if (level >= 180) { nextBosses = DateTime.UtcNow.AddMinutes(15); break; } BossesDone(DateTime.UtcNow.AddSeconds(reset + 60)); if (hits == 0) Log("Bosses: " + where + " waits - " + (stop == "stamina" ? "no stamina left" : "health is low")); nextBosses = DateTime.UtcNow.AddMinutes(exp ? 2 : 10); case "bars": nextBosses = DateTime.UtcNow.AddMinutes(2); break; case null: nextBosses = DateTime.UtcNow.AddMinutes(1); break; Log("Bosses: stopped at " + where + " - " + stop); nextBosses = DateTime.UtcNow.AddMinutes(15); bool up = stop != "cleared" && stop != "no attack"; Info("bossname", st != null && st.Stage > 0 ? "Stage " + st.Stage + (st.Boss.Length > 0 ? " - " + st.Boss : "") : "?"); var t = TimeSpan.FromSeconds(Math.Max(60, reset)); Info("bosswhen", string.Format("all beaten - again in {0}h {1:00}m", (int)t.TotalHours, t.Minutes)); else Info("bosswhen", stop == "no attack" ? "needs a higher level" : "ready to fight"); NoteBossRound(up ? 1 : 0, up ? int.MaxValue : reset); int softTop = MostWantedFrom - 1; int softMw = MostWantedFrom; const int SoftTimes = 3, SoftHitsNeeded = 3; static readonly TimeSpan StageHitsKeep = TimeSpan.FromDays(3); bool SoftTarget(int stage) { return stage > 0 && (stage < MostWantedFrom ? stage <= softTop : stage >= softMw); } void SoftPassed(int stage) if (stage < MostWantedFrom) softTop = Math.Min(softTop, stage - 1); else softMw = Math.Max(softMw, stage + 1); Dictionary<int, Tuple<int, DateTime, int>> StageHitsRead() var d = new Dictionary<int, Tuple<int, DateTime, int>>(); foreach (var part in (S.StageHits ?? "").Split(';')) if (p.Length == 4 && int.TryParse(p[0], out stage) && int.TryParse(p[1], out dmg) && dmg > 0 && int.TryParse(p[3], out n) && n > 0 && DateTime.TryParseExact(p[2], "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out at)) d[stage] = Tuple.Create(dmg, at, n); void NoteStageHit(int stage, int dmg, DateTime now, bool trusted = false) if (stage <= 0 || dmg <= 0) return; Tuple<int, DateTime, int> was; bool fresh = d.TryGetValue(stage, out was) && now - was.Item2 < StageHitsKeep; int big = fresh ? Math.Max(was.Item1, dmg) : dmg, n = (fresh ? was.Item3 : 0) + 1; if (trusted) n = Math.Max(n, SoftHitsNeeded); d[stage] = Tuple.Create(big, now.Date, Math.Min(n, 999)); S.StageHits = string.Join(";", d.OrderBy(kv => kv.Key).Select(kv => kv.Key + ":" + kv.Value.Item1 + ":" + kv.Value.Item2.ToString("yyyyMMdd", CultureInfo.InvariantCulture) + ":" + kv.Value.Item3)); int StageHitOf(int stage, DateTime now) Tuple<int, DateTime, int> was; return StageHitsRead().TryGetValue(stage, out was) && now - was.Item2 < StageHitsKeep && was.Item3 >= SoftHitsNeeded ? was.Item1 : -1; bool StageHitsKnown(DateTime now) { return StageHitsRead().Any(kv => now - kv.Value.Item2 < StageHitsKeep && kv.Value.Item3 >= SoftHitsNeeded); } bool SoftenOk(ref BossStage st, out string why) if (st.Attack == null || st.DefeatedToday || st.Cleared) return false; int big = StageHitOf(st.Stage, DateTime.UtcNow); if (big <= 0) { why = "fewer than " + SoftHitsNeeded + " hits on it written down lately"; return false; } if (st.Health <= 0 || st.HealthMax <= 0 || st.Health > st.HealthMax) { why = "its health didn't read"; return false; } if (st.Health <= (long)SoftTimes * big) { why = "it's low already"; return false; } if (again == null || again.Stage != st.Stage || again.Health != st.Health || again.Attack == null) if (again != null && again.Stage > 0) st = again; why = "its health read differently twice"; bool SeekSoft(ref BossStage st) if (st == null || st.Stage < 0) return false; if (SeekReadyDown(ref st, softTop)) { if (st != null && st.Stage > 0 && st.Stage < MostWantedFrom) softTop = Math.Min(softTop, st.Stage); return true; } if (st == null || st.Stage < 0) return false; if (SeekReadyFrom(ref st, softMw)) return true; if (st == null || st.Stage < 0 || st.Next == null || st.NextStage < softMw) return false; if (now != null && now.Stage > 0) st = now; bool SeekReadyDown(ref BossStage st, int top) if (st == null || st.ListAt.IsEmpty) return false; for (int roll = 0; roll < 30 && st.ListLast > 0 && st.ListLast < top; roll++) string wasSig = st.ListSig; game.StagesListScroll(st, -3); if (now == null || now.Stage < 0 || now.ListAt.IsEmpty) return false; if (st.ListLast == was || ListStill(wasSig, st)) break; for (int roll = 0; roll < 30; roll++) var r = st.Readies.Where(kv => kv.Key > 0 && kv.Key <= top).OrderByDescending(kv => kv.Key).FirstOrDefault(); game.StageReady(r.Value); if (now != null && now.Stage > 0) st = now; if (st.Stage != r.Key && saidOnce.Add("bosses ready " + r.Key)) Log("Bosses: READY pressed on stage " + r.Key + " but the page shows stage " + st.Stage); if (st.ListFirst == 1) return false; int first = st.ListFirst; string firstSig = st.ListSig; game.StagesListScroll(st, 3); if (then == null || then.Stage < 0 || then.ListAt.IsEmpty) return false; if ((first > 0 && st.ListFirst == first) || ListStill(firstSig, st)) return false; static bool ListStill(string before, BossStage now) { return now != null && !string.IsNullOrEmpty(before) && before == now.ListSig; } for (int look = 0; look < 3; look++) if (look > 0) game.Wait(400); using (var f = game.Capture()) st = game.ReadBossStage(f); if (st != null && st.Stage > 0) return st; bool SeekReady(ref BossStage st, int from, bool strict = false) if (st == null || st.ListAt.IsEmpty) return false; if (strict) return SeekReadyFrom(ref st, from); for (int roll = 0; roll < 30 && !(st.ListFirst > 0 && st.ListFirst <= from); roll++) string wasSig = st.ListSig; game.StagesListScroll(st, 10); if (now == null || now.Stage < 0 || now.ListAt.IsEmpty) break; if ((was > 0 && st.ListFirst == was) || ListStill(wasSig, st)) break; for (int roll = 0; roll < 60; roll++) if (st.Ready != null && (st.ReadyStage < 0 || st.ReadyStage >= from)) int at = st.ReadyStage, was = st.Stage; if (now != null && now.Stage > 0) st = now; SnapshotOnce("boss stage READY pressed"); if (at > 0 && st.Stage != at && saidOnce.Add("bosses ready " + at)) Log("Bosses: READY pressed on stage " + at + " but the page shows stage " + st.Stage + (was == st.Stage ? " still" : "")); if (at < 0 && st.Stage == was && st.Stage > 0) if (saidOnce.Add("bosses ready still " + UtcDay(DateTime.UtcNow))) Log("Bosses: READY pressed in the list but the page still shows stage " + st.Stage); SnapshotOnce("boss READY did nothing"); if (st.LockedLevel > 0 && st.Readies.All(r => r.Key > 0 && r.Key < from)) { stagesLocked = st.LockedLevel; stagesWalked = st.Next == null; return false; } if (st.Next != null || st.ListAt.IsEmpty) return false; string lastSig = st.ListSig; game.StagesListScroll(st, -3); if (then == null || then.Stage < 0) return false; if (last > 0 && st.ListLast == last && st.Ready == null) { stagesWalked = st.Next == null; return false; } if (ListStill(lastSig, st)) { stagesWalked = st.Next == null; return false; } bool SeekReadyFrom(ref BossStage st, int from) for (int roll = 0; roll < 30 && !(st.ListFirst > 0 && st.ListFirst <= from); roll++) string wasSig = st.ListSig; game.StagesListScroll(st, 10); if (now == null || now.Stage < 0 || now.ListAt.IsEmpty) break; if ((was > 0 && st.ListFirst == was) || ListStill(wasSig, st)) break; for (int roll = 0; roll < 60; roll++) var r = st.Readies.FirstOrDefault(kv => kv.Key >= from); game.StageReady(r.Value); if (now != null && now.Stage > 0) st = now; if (st.Stage != r.Key && saidOnce.Add("bosses ready " + r.Key)) Log("Bosses: READY pressed on stage " + r.Key + " but the page shows stage " + st.Stage); if (st.Next != null || st.ListAt.IsEmpty) return false; string lastSig = st.ListSig; game.StagesListScroll(st, -3); if (then == null || then.Stage < 0) return false; if ((last > 0 && st.ListLast == last) || ListStill(lastSig, st)) return false; static int SecondsToUtcMidnight() { var now = DateTime.UtcNow; return (int)(now.Date.AddDays(1) - now).TotalSeconds; } static string Cut(string s, int n) { return s.Length <= n ? s : s.Substring(0, n) + "..."; } Header ReadBarsSettled(int maxMs) { return ReadBarsSettled(maxMs, h => h.Stamina >= 0); } Header ReadBarsSettled(int maxMs, Func<Header, bool> ok) int end = Environment.TickCount + maxMs; using (var f = game.Capture()) if (!ok(h) && closed < 10 && game.CloseTerritoryToast(f)) { closed++; end = Math.Max(end, Environment.TickCount + 1500); continue; } if (ok(h) || end - Environment.TickCount <= 0) return h; const int LowHealthPct = 10; Rejoiner rejoiner = new Rejoiner(); DateTime barsSameSince = DateTime.MinValue; int dialogLookAt = Environment.TickCount - 60000; DateTime GameInput { get { return game.Win.LastInput > gameInput ? game.Win.LastInput : gameInput; } } internal static int LookEvery(bool hint, bool tryUnderWay, bool suspect, bool upLastTime, bool rejoining) return hint || tryUnderWay ? 0 : suspect || upLastTime || rejoining ? 10000 : 60000; bool LookForDisconnect(Frame f, bool suspect, ref Game.Disconnect dialog) int every = LookEvery(disconnectHint, rejoiner.Busy, suspect, dialogUp, rejoiner.Active); if (Environment.TickCount - dialogLookAt < every) return false; dialogLookAt = Environment.TickCount; dialog = game.ReadDisconnect(f); if (dialog == null && !rejoiner.Active) eventWindow = !Game.EventPopupX(f).IsEmpty; if (dialog == null && !rejoiner.Active) inviteWindow = !Game.InviteIgnore(f).IsEmpty; if (dialog == null && !rejoiner.Active && suspect) notifyWindow = Game.NotifyPromptCancel(f) != null; string defendName, defendCity; DateTime defendHit = DateTime.MinValue, defendNext = DateTime.MinValue; static readonly TimeSpan DefendAgain = TimeSpan.FromMinutes(3), DefendOver = TimeSpan.FromMinutes(2); DateTime nextRulingLook = DateTime.MinValue; for (int toast = 0; toast < 4; toast++) using (var f = game.Capture()) read = game.TerritoryToastDistrict(f, out lost, out x, out by, out text); string spared = S.DefendTerritory ? DefendSpared(S.NeverDefend, by, text) : null; SayOnce("never defend " + Parse.Key(read) + DateTime.UtcNow.ToString("yyyyMMddHH"), "Territory: " + read + " is attacked by " + (by.Length > 0 ? by : spared) + " - not defended, \"" + spared + "\" is on your never-defend list"); bool ours = S.DefendTerritory && !Lowering && spared == null && NoteAttack(read, lost); if (ours && DateTime.UtcNow >= defendNext && DateTime.UtcNow >= takedownHold) Defend(true); using (var f = game.Capture()) game.CloseTerritoryToast(f); if (DateTime.UtcNow >= nextRulingLook) nextRulingLook = DateTime.UtcNow.AddMinutes(1); using (var f = game.Capture()) game.CloseRulingFamily(f); var now = DateTime.UtcNow; if (defendName == null || Lowering) return; if (now - defendHit >= DefendOver) EndDefence("no attack notification for 2 minutes"); else if (S.DefendTerritory && now >= defendNext && now >= takedownHold && defendByList) Defend(); S.Defence = defendName == null ? "" : string.Join("|", defendName, defendCity, defendNo.ToString(CultureInfo.InvariantCulture), defendStationed ? "1" : "0", defendHit.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture)); defendName = null; defendStationed = false; defendByList = false; defendNext = DateTime.MinValue; var p = (S.Defence ?? "").Split('|'); if (p.Length != 5 || p[0].Length == 0 || !int.TryParse(p[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out no) || !DateTime.TryParse(p[4], CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out hit)) return; defendName = p[0]; defendCity = p[1]; defendNo = no; defendStationed = p[3] == "1"; defendHit = hit; if (defendStationed) Log("Territory: henchmen are still stationed at " + defendName + " from before - recalled 2 minutes after its last attack"); internal static string DefendSpared(string list, string by, string text) Func<string, string> key = s => Parse.Key(s).Replace("2", "II").Replace("3", "III"); string b = key(by), t = key(text); foreach (var e in NeverAttackEntries(list)) if (k.Length < 3) continue; if (b == k || (k.Length >= 6 && View.Distance(b, k) <= 1)) return e; if (b.Length > 0) continue; for (int i = 0; i + k.Length <= t.Length; i++) if (i + k.Length < t.Length && t[i + k.Length] == 'I') continue; string part = t.Substring(i, k.Length); if (part == k || (k.Length >= 6 && View.Distance(part, k) <= 1)) return e; readonly Dictionary<string, DateTime> shipmentsSaid = new Dictionary<string, DateTime>(); using (var f = game.Capture()) text = game.ShipmentToast(f, out x); if (text == null) return; var now = DateTime.UtcNow; string key = Parse.Key(text); foreach (var old in shipmentsSaid.Where(kv => now - kv.Value > TimeSpan.FromMinutes(30)).Select(kv => kv.Key).ToList()) shipmentsSaid.Remove(old); if (!shipmentsSaid.ContainsKey(key)) shipmentsSaid[key] = now; Log("Territory: a shipment notification: \"" + Cut(text, 200) + "\""); SnapshotOnce("territory shipment notification"); game.CloseShipmentToast(x); bool NoteAttack(string read, bool lost) if (!Game.MatchDistrict(read, out city, out no, out name)) SayOnce("district unknown " + Parse.Key(read), "Territory: \"" + read + "\" is attacked, but it isn't one of the game's districts as the bot knows them - not defended"); if (defendName != null && defendName != name) SayOnce("defending elsewhere " + name, "Territory: " + name + " is attacked too - the crew stays at " + defendName + " while that attack lasts"); bool fresh = defendName == null; defendName = name; defendCity = city; defendNo = no; defendHit = DateTime.UtcNow; if (fresh) { defendNext = DateTime.MinValue; Log("Territory: " + name + " (" + city + ") is attacked - defending it"); } else if (lost) defendNext = DateTime.MinValue; void Defend(bool toast = false) defendNext = DateTime.UtcNow + DefendAgain; if (S.DefendHeal && defendStationed) var h = ReadBarsSettled(3000, b => b.Stamina >= 0); int keep = Kept(h.StaminaMax); if (h.Stamina > keep && game.OpenTab(Tab.Crew)) int spent = game.HealRecovering(h.Stamina, keep); if (spent > 0) Log("Territory: healed the beaten defenders with " + spent + " stamina"); bool viaToast = toast && game.OpenDistrictFromToast(defendName); if (!viaToast) SayOnce("district by list", "Territory: pressing the notification didn't open " + defendName + " - through the Territory page instead"); else SnapshotOnce("district opened from the notification"); defendByList = !viaToast; if (!viaToast) game.CloseDistrict(); if (!viaToast && !game.OpenDistrict(defendCity, defendNo, defendName)) if (game.LastDistrictGone) { Log("Territory: " + defendName + " isn't your family's any more - the defence ends"); EndDefence(null); return; } game.Snapshot("territory district not opened", 60); using (var f = game.Capture()) w = game.ReadDistrictWindow(f); string ours = last != null ? last.Family : null; if (w != null && !string.IsNullOrEmpty(ours) && !Game.HeldByUs(w.HeldBy, ours)) Log("Territory: " + defendName + " is held by " + w.HeldBy + " now - the defence ends"); int n = game.StationAllHere(); if (n == -2) SnapshotOnce("territory station crew none free"); else if (n < 0) game.Snapshot("territory station all not read", 60); if (n > 0) { defendStationed = true; SaveDefence(); Log("Territory: stationed " + n + " henchmen at " + defendName); } else if (n == -2) SayOnce("station crew none free", "Territory: STATION CREW showed no free henchmen at " + defendName + " - none stationed (all away or busy)"); void EndDefence(string why) string name = defendName; bool stationed = defendStationed; defendName = null; defendByList = false; defendStationed = false; if (!stationed || name == null) { if (why != null && name != null) Log("Territory: the attack on " + name + " is over (" + why + ")"); return; } int n = game.OpenTab(Tab.Crew) ? game.RecallFrom(name) : 0; Log("Territory: the attack on " + name + " is over" + (why != null ? " (" + why + ")" : "") + " - " + (n > 0 ? n + " henchmen recalled" : "none of yours were still there")); using (var f = game.Capture()) game.CloseNotifyPrompt(f); using (var f = game.Capture()) game.CloseInvite(f); if (!eventWindow) return; using (var f = game.Capture()) game.CloseEventPopup(f); DateTime robloxBarSince = DateTime.MinValue, robloxBarSaid = DateTime.MinValue; void NoteRobloxBar(bool showing) var now = DateTime.UtcNow; if (robloxBarSince != DateTime.MinValue) return; if (now - robloxBarSaid < TimeSpan.FromMinutes(30)) return; Log("Roblox's own title bar is over the game's top bar - the bot reads nothing up there (bars, cash, name) until it goes"); SnapshotOnce("roblox title bar over the top bar"); else if (robloxBarSince != DateTime.MinValue) if (robloxBarSaid >= robloxBarSince && now - robloxBarSince >= TimeSpan.FromMinutes(1)) Log("Roblox's title bar is gone after " + (int)Math.Round((now - robloxBarSince).TotalMinutes) + " min - the top bar reads again"); robloxBarSince = DateTime.MinValue; nextBarSweep = DateTime.MinValue; DateTime nextBarSweep = DateTime.MinValue; bool SweepRobloxBar(bool eagerly = false) var now = DateTime.UtcNow; if (!game.RobloxBar || robloxBarSince == DateTime.MinValue || now - robloxBarSince < TimeSpan.FromSeconds(20) && !eagerly || now < nextBarSweep) return false; if (!eagerly && S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) return false; nextBarSweep = now.AddMinutes(barSweeps < 3 ? 2 : 15); State("Moving the mouse over Roblox's title bar so it goes away"); if (!Act(delegate { gone = game.ClearRobloxBar(); })) return true; if (gone) { Log("Roblox's title bar went away after the bot moved the mouse up to it and back down"); return true; } State("Pressing F11 so Roblox's title bar goes away"); if (!Act(delegate { f11 = game.ToggleFullscreenClearBar(); })) return true; if (f11) { Log("Roblox's title bar went away after the bot pressed F11 - the top bar reads again"); barSweeps = 0; } else if (eagerly || barSweeps >= 3) SayOnce("roblox bar stays", "Roblox's title bar still stays after the mouse and F11 - the bot can't read the top bar until it goes " + "(moving the mouse to the top of the screen and away, or playing Roblox in a window instead of fullscreen, clears it)"); else Log("Roblox's title bar stays after the mouse and F11 - trying again in 2 minutes"); if (!game.RobloxBar) return false; if (robloxBarSince == DateTime.MinValue) robloxBarSince = DateTime.UtcNow; nextBarSweep = DateTime.MinValue; return SweepRobloxBar(true); int emptySaid, copiesSaid; DateTime emptySaidAt = DateTime.MinValue; DateTime halfSaidAt = DateTime.MinValue; readonly HashSet<string> coverSaid = new HashSet<string>(); string cover = RobloxWindow.CoveredBy; if (cover != null && coverSaid.Add(cover)) Log(cover + " lies over Roblox on this PC, so no game pictures go to Discord while it does (they could show it)" + (cover.EndsWith(" overlay") ? " - turn that overlay off to get them back" : "")); int half = RobloxWindow.HalfDrawn; if (half > halfSaid && DateTime.UtcNow - halfSaidAt >= TimeSpan.FromHours(1)) Log("Roblox's picture came half drawn " + (half - halfSaid) + " time" + (half - halfSaid == 1 ? "" : "s") + " - the bot copied it again from the screen"); halfSaid = half; halfSaidAt = DateTime.UtcNow; int empty = RobloxWindow.EmptyPictures, copies = RobloxWindow.ScreenCopies; int n = (empty - emptySaid) - (copies - copiesSaid); if (n <= 0) { emptySaid = empty; copiesSaid = copies; return; } if (DateTime.UtcNow - emptySaidAt < TimeSpan.FromMinutes(30)) return; emptySaid = empty; copiesSaid = copies; emptySaidAt = DateTime.UtcNow; Log("Windows gave the bot an empty picture of Roblox " + (n == 1 ? "once" : n + " times") + " (a graphics problem on this PC) - " + "it reads the game only with nothing over Roblox. If it goes on: restart the PC, update the graphics driver"); readonly object screenFirstLock = new object(); if (!RobloxWindow.ScreenFirst || S.ScreenFirst) return; if (S.ScreenFirst) return; Log("Windows gave the bot an empty picture of Roblox while the screen showed the game. On this PC that can make Roblox " + "flash white, so from now on the bot copies Roblox from the screen while nothing is over it"); bool DisconnectBeforeRound() if (LeaveDeadGame()) return true; if (Environment.TickCount - dialogLookAt < 5000) return false; using (var f = game.Capture()) return DisconnectBeforeRound(f); var step = rejoiner.DeadAfterTeleport(DateTime.UtcNow, game.TabFails, game.TabFailsSince, Rejoins, barsSameSince); if (step == Rejoiner.Step.Wait) return false; game.Snapshot("game dead after teleport failed", 60); return TakeRejoinStep(step); void NoteBarsMoving(Header h, DateTime now) var bars = new[] { h.Energy, h.EnergyMax, h.Stamina, h.StaminaMax, h.Health, h.HealthMax }; if (h.Energy < 0 || h.Stamina < 0 || h.Health < 0 || h.EnergyMax <= 0 || h.StaminaMax <= 0 || h.HealthMax <= 0) return; if (h.Energy >= h.EnergyMax && h.Stamina >= h.StaminaMax && h.Health >= h.HealthMax) { barsSeen = null; barsSameSince = DateTime.MinValue; return; } if (barsSeen == null || !bars.SequenceEqual(barsSeen)) { barsSeen = bars; barsSameSince = now; } bool DisconnectBeforeRound(Frame f) var d = game.ReadDisconnect(f); dialogLookAt = Environment.TickCount; if (d == null && !rejoiner.Active) eventWindow = !Game.EventPopupX(f).IsEmpty; if (d == null) return false; if (!Offline(d)) { State(rejoiner.Status ?? Game.Disconnected); WatchRoblox(4000); } int end = Environment.TickCount + ms; if (game.Win.IsForeground && game.Win.UserActiveWithin(1000)) { robloxUsed = true; robloxUsedAt = DateTime.UtcNow; } Sleep(Math.Max(1, Math.Min(250, end - Environment.TickCount))); } while (!stop && end - Environment.TickCount > 0); var now = DateTime.UtcNow; readHandle = game.Win.Handle; if (focusBack != IntPtr.Zero) if (Native.LastInputTick() == stepInput) game.Win.GiveFocusBack(focusBack); if (game.Win.IsForeground && game.Win.UserActiveWithin(5000)) gameInput = now; bool Offline(Game.Disconnect d) var now = DateTime.UtcNow; string text = d.ToString(); Log((d.Teleport ? "Roblox shows its Teleport Failed window: " : "Roblox disconnected the game: ") + (text.Length > 0 ? text : "(its message didn't read)")); SnapshotOnce((d.Teleport ? "teleport failed " : "disconnected ") + (d.Code >= 0 ? d.Code.ToString() : "no code")); step = rejoiner.OnDialog(now, d, Rejoins, robloxUsed, TimeSpan.FromMinutes((int)S.OtherDeviceRejoin)); if (!disconnectSaid && !robloxUsed && !(d.Teleport && (step == Rejoiner.Step.Dismiss || rejoiner.Busy))) Discord.Problem("disconnected", d.Teleport ? "⚠️ Roblox shows its Teleport Failed window" : "⚠️ Roblox disconnected the game", (text.Length > 0 ? text + "\n" : "") + DisconnectPlan(d), Shot, false, 10); disconnectSaid = offlineSaid = true; step = rejoiner.OnUnreadable(now, Rejoins, robloxUsed); return TakeRejoinStep(step); bool TakeRejoinStep(Rejoiner.Step step) if (step == Rejoiner.Step.Wait) return false; if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) State("Roblox disconnected the game - getting back in once you stop using the mouse/keyboard"); if (!rejoinIdleSaid) { Log("Roblox disconnected the game - getting back in once you've been idle for " + S.IdleSeconds + " s (you're using the PC)"); rejoinIdleSaid = true; } int end = Environment.TickCount + 30000; while (!stop && !robloxUsed && game.Win.UserActiveWithin(S.IdleSeconds * 1000) && end - Environment.TickCount > 0); var now = DateTime.UtcNow; if (step == Rejoiner.Step.Launch) IntPtr front = Native.GetForegroundWindow(); rejoiner.Took(step, now); try { RobloxWindow.StartGame(); } Log("Couldn't start Idle Mafia Game: " + e.Message); Log("Starting Idle Mafia Game again"); stepInput = Native.LastInputTick(); focusBack = S.GiveFocusBack ? front : IntPtr.Zero; State("Starting Idle Mafia Game in Roblox..."); if (step == Rejoiner.Step.MenuLeave) State("The game stopped answering - leaving it through Roblox's menu, to start it again"); if (Act(delegate { game.LeaveByMenu(); })) rejoiner.Took(step, now); stepInput = Native.LastInputTick(); Log("Pressed Esc, L and Enter in Roblox (its menu's Leave) - the game is started again once Roblox has closed"); bool reconnect = step == Rejoiner.Step.Reconnect, dismiss = step == Rejoiner.Step.Dismiss, pressed = false; State(dismiss ? "Roblox's Teleport Failed window - pressing Ok" : reconnect ? "Roblox disconnected the game - pressing Reconnect" : "Roblox disconnected the game - pressing Leave"); Act(delegate { pressed = dismiss ? game.PressTeleportOk() : game.PressDisconnect(reconnect, Rejoins && S.OtherDeviceRejoin != OtherDeviceWait.Off); }); rejoiner.Took(step, now); stepInput = Native.LastInputTick(); Log(dismiss ? "Pressed Ok in Roblox's Teleport Failed window (it only closes it)" : reconnect ? "Pressed Reconnect" : "Pressed Leave - the game is started again once Roblox has closed"); DateTime awaySince = DateTime.MinValue, nextSummary, nextLevelPing; volatile bool offlineSaid; bool disconnectSaid, awayYours; public bool OutOfGame { get { return offlineSaid || awayOut; } } public bool GameLeftToYou { get { return rejoiner.LeftToYou || robloxCheckUp; } } volatile bool robloxCheckUp; int summaryLevel = -1, levelSaid = -1, levelSeen = -1, levelSaidXp = -1, rerollsSaid; var now = DateTime.UtcNow; awaySince = DateTime.MinValue; awaySaid = null; offlineSaid = disconnectSaid = awayOut = false; nextSummary = now.AddHours(3); summaryFrom = Count.Copy(); summaryLevel = level; levelSaid = levelSeen = levelSaidXp = -1; nextLevelPing = now; rerollsSaid = Count.Rerolls; string DisconnectPlan(Game.Disconnect d) if (!rejoiner.Active) return "It was up before the bot started (or in another game), so the bot leaves it to you."; if (d.OtherDevice && Rejoins && S.OtherDeviceRejoin != OtherDeviceWait.Off) return "Your account joined the game on another device. The bot waits " + (int)S.OtherDeviceRejoin + " minutes, then gets back in (that throws the other device out)."; if (d.OtherDevice) return "Your account joined the game on another device, so the bot leaves Roblox alone."; if (d.Kicked) return "Roblox says you were kicked. The bot doesn't rejoin after a kick, so start the game yourself."; if (!Rejoins) return turns != null ? "Rejoining is off while the bot plays several accounts in turns, so it waits." : "Rejoining is off in the bot's settings, so it waits."; if (d.Foreign) return "Roblox's window isn't in English, so the bot can't rejoin by itself. Set Roblox to English."; if (d.Teleport) return "The bot closes it with its Ok and plays on."; return "The bot gets back in by itself."; using (var f = game.Win.Capture()) return Discord.Jpg(f.Bitmap); void Away(string key, string title, string text) var now = DateTime.UtcNow; if (awaySince == DateTime.MinValue || awaySaid != null && awaySaid != key || awayLogged != null && awayLogged != key) awaySince = now; awaySaid = null; awayLogged = null; awayYours = key == "closed" && now - robloxUsedAt < TimeSpan.FromMinutes(1); if (now - awaySince >= TimeSpan.FromMinutes(5)) awayOut = true; if (awayLogged == null && now - awaySince >= TimeSpan.FromMinutes(2)) string since = awaySince.ToLocalTime().ToString("HH:mm"); Log(key == "closed" ? "Roblox isn't open since " + since + " - " + text : "Can't read the game since " + since + ": " + text); if (key != "closed") game.Snapshot("game unreadable", 60); if (awaySaid != null || now - awaySince < TimeSpan.FromMinutes(robloxCheckUp && key == "unreadable" ? 0 : 5)) return; Discord.Problem("away " + key, title, text, key == "closed" ? null : (Func<byte[]>)Shot, false, 60); if (awayLogged != null && !rejoinBackSaid) Log("The game reads again, after " + Math.Max(1, (int)Math.Round((DateTime.UtcNow - awaySince).TotalMinutes)) + " minutes"); if (awaySaid != null && offlineSaid) Discord.Problem("back", "✅ The bot can read the game again", "It plays on.", Shot, false, 0, Discord.Green); if (awaySaid != null) offlineSaid = false; awaySaid = awayLogged = null; awaySince = DateTime.MinValue; string n = rejoiner.Note; if (n.StartsWith("Back in the game")) if (offlineSaid) Discord.Problem("back", "✅ " + n, "The bot plays on.", Shot, false, 0, Discord.Green); else if (n.StartsWith("Couldn't get back")) Discord.Problem("rejoin failed", "⚠️ " + n, null, null, false, 30); void LevelPing(int lv, int xpNext) if (lv != levelSeen) { levelSeen = lv; return; } if (levelSaid < 0) { levelSaid = lv; levelSaidXp = xpNext; return; } if (lv == levelSaid && levelSaidXp <= 0) levelSaidXp = xpNext; if (lv < levelSaid - 50) { levelSaid = lv; levelSaidXp = xpNext; return; } if (lv <= levelSaid) return; if (lv - levelSaid > 3 && !(xpNext > 0 && levelSaidXp > 0 && xpNext > levelSaidXp)) return; if (!Discord.On(Ping.Level)) { levelSaid = lv; levelSaidXp = xpNext; return; } if (DateTime.UtcNow < nextLevelPing) return; Discord.Post(Ping.Level, "⬆️ Level " + lv, lv - levelSaid == 1 ? "Up from " + levelSaid + "." : "Up " + (lv - levelSaid) + " levels from " + levelSaid + ".", Shot, Discord.Purple); nextLevelPing = DateTime.UtcNow.AddMinutes(10); void PingHenchman(Game.CrewRow got, int replaced, bool perfect, IEnumerable<Game.CrewRow> crew) if (got == null || got.Rarity >= Discord.RarityColors.Length) return; if (!HenchmanNews(got, replaced, perfect, S.CrewReroll || RerollOnly, Game.BestRoll(RollNow()), crew)) return; int took = Count.Rerolls - rerollsSaid; rerollsSaid = Count.Rerolls; Discord.Post(Ping.Rarity, "\U0001F3B2 " + (perfect ? "Perfect " : "") + got.RarityName + " henchman!", got + (took > 1 ? " after " + took + " rerolls" : ""), Shot, Discord.RarityColors[got.Rarity]); internal static bool HenchmanNews(Game.CrewRow got, int replaced, bool perfect, bool rerolls, int best, IEnumerable<Game.CrewRow> crew) if (got == null || got.Rarity < (int)Rarity.Mythic) return false; if (perfect) return true; if (got.Rarity <= replaced) return false; return !rerolls || got.Rarity >= best || (crew ?? Enumerable.Empty<Game.CrewRow>()).Any(r => !ReferenceEquals(r, got) && r.Rarity >= 0 && r.Rarity < got.Rarity); var now = DateTime.UtcNow; if (now < nextSummary) return; nextSummary = now.AddHours(3); int fromLevel = summaryLevel; summaryFrom = Count.Copy(); if (from == null || !Discord.On(Ping.Summary)) return; var lines = new List<string>(); if (level > 0) lines.Add("Level " + level + (fromLevel > 0 && level > fromLevel ? " (+" + (level - fromLevel) + ")" : "")); if (last != null && last.Cash >= 0) lines.Add("Cash " + M(last.Cash) + (last.Banked >= 0 ? ", " + M(last.Banked) + " in the bank" : "")); var did = new List<string>(); Action<int, string, string> add = (n, one, many) => { if (n > 0) did.Add(n + " " + (n == 1 ? one : many)); }; add(c.Jobs - from.Jobs, "job", "jobs"); add(c.Helps - from.Helps, "heist help", "heist helps"); int fights = c.Fights - from.Fights; if (fights > 0) did.Add(fights + (fights == 1 ? " fight" : " fights") + " (" + (c.Wins - from.Wins) + " won)"); add(c.BossesBeaten - from.BossesBeaten, "boss beaten", "bosses beaten"); add(c.Properties - from.Properties, "property built", "properties built"); add(c.Hired - from.Hired, "hire", "hires"); add(c.Rerolls - from.Rerolls, "reroll", "rerolls"); add(c.Bought - from.Bought, "shop item", "shop items"); add(c.Crates - from.Crates, "crate", "crates"); add(c.OpsCollected - from.OpsCollected, "operation collected", "operations collected"); add(c.Rewards - from.Rewards, "reward", "rewards"); add(c.StaminaGiven - from.StaminaGiven, "stamina given to the family", "stamina given to the family"); lines.Add(did.Count > 0 ? "Last 3 hours: " + string.Join(", ", did) + "." : "Nothing done in the last 3 hours."); Discord.Post(Ping.Summary, "\U0001F4CA 3-hour update", string.Join("\n", lines), Shot, Discord.Gold); public event Action<FamilyInfo> FamilyRead; FamilyInfo family = new FamilyInfo(); DateTime nextTakedown, nextGive, nextFamily; DateTime freeGoneAt = DateTime.MinValue; int takedownHealthCost = 25; DateTime takedownHold = DateTime.MinValue; DateTime takedownAwaySince = DateTime.MinValue; readonly HashSet<string> saidOnce = new HashSet<string>(); DateTime bossesReadAt = DateTime.MinValue, bossBackAt = DateTime.MaxValue; readonly RegenGuess energyRegen = new RegenGuess(), staminaRegen = new RegenGuess(); DateTime giveBrokenUntil = DateTime.MinValue; void GiveMissed(string what) int wait = ++giveMissed == 1 ? 10 : 60; Log(what + " - tries again in " + wait + " minutes"); nextGive = giveBrokenUntil = DateTime.UtcNow.AddMinutes(wait); void ResetFamily(DateTime now) nextTakedown = nextGive = nextFamily = now; familyMissingSaid = false; bossesReadAt = DateTime.MinValue; bossBackAt = DateTime.MaxValue; takedownWants = givenSinceFights = giveMissed = 0; giveBrokenUntil = DateTime.MinValue; void SayOnce(string key, string msg) { if (saidOnce.Add(key)) Log(msg); } void FamilyChanged() { var h = FamilyRead; if (h != null) h(family.Copy()); } void NoteBossRound(int stillUp, int soonest) bossesReadAt = DateTime.UtcNow; bossBackAt = soonest < int.MaxValue ? DateTime.UtcNow.AddSeconds(soonest) : DateTime.MaxValue; var now = DateTime.UtcNow; energyRegen.Note(h.Energy, h.EnergyMax, h.EnergyTick, now); staminaRegen.Note(h.Stamina, h.StaminaMax, h.StaminaTick, now); internal static bool GuessBar(RegenGuess regen, DateTime now, ref int value, ref int max) if (value >= 0) return true; int g = regen.Guess(now); value = g; max = regen.Max; int k = Math.Max(0, S.FightStaminaReserve); if (max <= 0 || k <= max / 2) return k; SayOnce("kept half " + max, "Your stamina bar holds " + max + ", so the bot keeps half of it (" + max / 2 + ") for bosses instead of " + k); static int FightSpare(int max, int kept) { return max > 0 ? Math.Max(1, Math.Min(5, max - kept)) : 5; } return S.GiveStamina && Can(Tab.Family) && !family.NotInFamily && DateTime.UtcNow >= giveBrokenUntil; bool FightsMayUse(Header h) if (h == null || h.Stamina < 0) return false; int kept = Kept(h.StaminaMax); int need = FightSpare(h.StaminaMax, kept); if (h.Stamina - kept >= need) return true; SayOnce("fights wait " + kept + "/" + h.StaminaMax, "Fights wait until the stamina is " + need + " above the " + kept + " kept for bosses" + (h.StaminaMax > 0 ? " (your bar holds " + h.StaminaMax + ")" : "")); return kept > 0 && h.Stamina >= kept && h.StaminaMax > 0 && givenSinceFights >= Math.Max(5, h.StaminaMax - kept); DateTime familyDoubtAt = DateTime.MinValue; bool FamilyOpen(string sub, ref DateTime retry) var nav = game.OpenFamily(sub); if (nav == FamilyNav.Open) familyDoubtAt = DateTime.MinValue; if (family.NotInFamily) { family.NotInFamily = false; familyMissingSaid = false; FamilyChanged(); } if (nav == FamilyNav.NotInFamily && familyOpened) if (familyDoubtAt == DateTime.MinValue) familyDoubtAt = DateTime.UtcNow; Log("FAMILY shows no family of yours, but it did before - looking again in 30 minutes"); if (DateTime.UtcNow - familyDoubtAt < TimeSpan.FromMinutes(20)) retry = DateTime.UtcNow.AddMinutes(30); familyDoubtAt = DateTime.MinValue; if (nav == FamilyNav.Failed && game.NotInMenu(Tab.Family)) { retry = DateTime.UtcNow.AddMinutes(30); return false; } if (nav == FamilyNav.Failed && familyFails++ == 0) familyShots = game.Shots; if (nav == FamilyNav.Failed && familyFails >= 5) nextTakedown = nextGive = nextFamily = DateTime.UtcNow.AddHours(3); Log("The Family tab didn't open as expected 5 times in a row - the family tasks wait 3 hours" + (game.Shots > familyShots ? " (pictures are in the problems folder)" : "")); if (nav == FamilyNav.NotInFamily) nextTakedown = nextGive = nextFamily = DateTime.UtcNow.AddHours(6); familyMissingSaid = true; Log("You're not in a family, so the family tasks (Takedown attacks, stamina perks, the family watch) wait - the bot looks again every 6 hours"); SnapshotOnce("family page without a family"); family.NotInFamily = true; retry = DateTime.UtcNow.AddMinutes(30); void NoteTakedown(TakedownState t) if (t == null || !t.Page) return; family.TakedownAt = DateTime.Now; void NotePerks(List<PerkRow> rows) if (rows == null || rows.Count == 0) return; family.PerksAt = DateTime.Now; static string TakedownBlock(TakedownState t) if (t == null || !t.Page) return "The Takedown page isn't showing (or didn't read) - no attacks this time"; if (t.Free < 0) return "Couldn't read the Takedown's free attacks - not attacking (without free ones ATTACK uses tickets or Robux)"; if (t.Free == 0) return ""; if (t.Segments <= 0) return string.Format("The Takedown's free attacks read as {0}/{1}, but their bar shows none - not attacking", t.Free, t.FreeMax); if (t.Attack == null && !t.AttackBelow) return "Couldn't find the Takedown's ATTACK button - not attacking"; var now = DateTime.UtcNow; if (!Can(Tab.Crew) || !game.OpenTab(Tab.Crew)) { SayOnce("takedown crew not checked", "Takedown: the Crew page didn't open - attacking without the check for henchmen at a district"); return true; } int recalled = game.RecallEveryone(); int away = recalled > 0 ? 1 : game.CrewAway(); if (takedownAwaySince == DateTime.MinValue) takedownAwaySince = now; if (now - takedownAwaySince < TimeSpan.FromMinutes(30)) int wait = recalled > 0 ? 11 : 3; nextTakedown = now.AddMinutes(wait); takedownHold = nextTakedown.AddMinutes(10); if (recalled > 0) SnapshotOnce("takedown crew recalled"); if (recalled > 0) Log("Takedown: recalled " + recalled + " henchm" + (recalled == 1 ? "an" : "en") + " from the districts (their gear adds nothing there) - the attacks wait until they're back"); else SayOnce("takedown crew away", "Takedown: henchmen are still away at a district or on their way back - the attacks wait for them"); Log("Takedown: henchmen still read as away after 30 minutes - attacking anyway"); game.Snapshot("takedown crew still away", 60); takedownAwaySince = DateTime.MinValue; saidOnce.Remove("takedown crew away"); if (S.EquipBest) DoEquip(); nextTakedown = DateTime.UtcNow.AddMinutes(takedownEvery); if (!FamilyOpen("TAKEDOWN", ref nextTakedown)) return; var t = game.ReadTakedownSteady(); if (t.Page && t.Free > 0 && freeGoneAt != DateTime.MinValue) Log(string.Format("{0} free Takedown attack(s) came back in {1}", t.Free, SpanText((int)(DateTime.UtcNow - freeGoneAt).TotalSeconds))); if (t.FreeMax > 0 && t.Free >= t.FreeMax && takedownEvery > 15) takedownEvery = Math.Max(15, takedownEvery / 2); Log("(They were all back, so the bot now looks every " + takedownEvery + " minutes)"); if (t.Page && t.Free > 0) freeGoneAt = DateTime.MinValue; if (t.Page && t.Free >= 0 && t.Segments >= 0 && t.Segments != t.Free && saidOnce.Add("takedown bar")) Log(string.Format("(The Takedown's free attacks read {0}/{1} while their bar shows {2} gold - keeping a picture)", t.Free, t.FreeMax, t.Segments)); SnapshotOnce("takedown count and bar differ"); int done = 0, hpBefore = -1, toastTries = 0; bool waitShort = false, crewChecked = false, crewWait = false; for (int i = 0; i < 20; i++) string why = TakedownBlock(t); bool noAttack = t.Page && t.Free > 0 && t.Attack == null && !t.AttackBelow; takedownNoAttack = noAttack ? takedownNoAttack + 1 : 0; if (why.Length > 0) SayOnce("takedown: " + why, why); if (noAttack && takedownNoAttack >= 3 && t.NewSeconds > 0) nextTakedown = DateTime.UtcNow.AddSeconds(t.NewSeconds + 120); SayOnce("takedown finished", "Takedown: still no ATTACK after three looks - taken as finished, looked at again when the new one starts (in " + SpanText(t.NewSeconds) + ")"); else if (why.Length > 0 && (!noAttack || takedownNoAttack == 1)) game.Snapshot("takedown " + why, 60); var bars = ReadBarsSettled(3000, x => x.Health >= 0 && x.Stamina >= 0); int hp = t.Health >= 0 ? t.Health : bars.Health, hpMax = t.HealthMax > 0 ? t.HealthMax : bars.HealthMax; if (hpBefore >= 0 && hp >= 0 && hpBefore > hp && hpMax > 0 && (hpBefore - hp) * 10 <= hpMax * 4) takedownHealthCost = hpBefore - hp; bool costly = hp >= 0 && hpMax > 0 && (hp - takedownHealthCost) * 100 < LowHealthPct * hpMax; if (hp >= 0 && hpMax > 0 && (hp * 100 < LowHealthPct * hpMax || costly)) Log("Health is low" + (costly ? " (" + hp + ", an attack takes about " + takedownHealthCost + ")" : "") + " - Takedown attacks wait until it comes back"); nextTakedown = DateTime.UtcNow.AddMinutes(20); if (bars.Stamina < 0 || bars.Stamina <= Kept(bars.StaminaMax)) if (bars.Stamina >= 0) takedownWants = Math.Max(1, t.Free); if (bars.Stamina < 0 && game.RobloxBar && barFixTries < 2) Log("Takedown: the stamina didn't read and Roblox's own title bar is over the top bar - the bot clears it first"); if (FixRobloxBarNow()) continue; if (bars.Stamina < 0 && toastTries < 2 && game.CloseTerritoryToasts()) { toastTries++; continue; } if (bars.Stamina < 0) { Log("Couldn't read stamina (" + game.BarLook(1) + ") - " + t.Free + " free Takedown attack(s) wait"); game.Snapshot("stamina unreadable at the takedown", 60); } else SayOnce("takedown waits for stamina", "Takedown: " + t.Free + " free attack(s) wait for stamina above the " + Kept(bars.StaminaMax) + " kept for bosses and fights - they go before the stamina gifts"); nextTakedown = DateTime.UtcNow.AddMinutes(20); if (!TakedownCrewHome()) { crewWait = waitShort = true; break; } if (!FamilyOpen("TAKEDOWN", ref nextTakedown)) break; t = game.ReadTakedownSteady(); if (!game.TakedownAttack(t)) const string notFound = "Couldn't find the Takedown's ATTACK button below the window's edge - not attacking"; SayOnce("takedown: " + notFound, notFound); game.Snapshot("takedown attack not found below", 60); if (done == 1) { game.Wait(600); SnapshotOnce("takedown right after an attack"); } var after = game.WaitTakedownResult(free, text => { Log("A window came up over the Takedown: " + text); SnapshotOnce("takedown window"); }, out said); if (!after.Page || after.Free < 0 || after.Free >= free) Log(string.Format("The Takedown's free attacks didn't go down after ATTACK ({0} before, {1} now){2} - no more attacks this time", free, !after.Page ? "page not read" : after.Free < 0 ? "unreadable" : after.Free.ToString(), said != null ? " - the game said \"" + Cut(said, 160) + "\"" : "")); if (said != null && Parse.Has(said, "ALREADY FOUGHT FOR")) done--; Count.TakedownAttacks--; family.Attacks--; nextTakedown = DateTime.UtcNow.AddSeconds(after.NewSeconds > 0 ? after.NewSeconds + 120 : 6 * 3600); SayOnce("takedown other family", "Takedown: the game keeps you out until the new Takedown (you fought for your old family this week) - looking again " + (after.NewSeconds > 0 ? "when it starts (in " + SpanText(after.NewSeconds) + ")" : "in 6 hours")); game.Snapshot("takedown attack not counted", 60); done--; Count.TakedownAttacks--; family.Attacks--; if (after.Page && after.Free >= free && ++takedownNotTaken >= 2) nextTakedown = DateTime.UtcNow.AddHours(6); SayOnce("takedown not taken", "Takedown: the game took no ATTACK twice in a row (a family joined during this week's Takedown?) - looking again every 6 hours" + (after.NewSeconds > 0 ? " and when the new one starts (in " + SpanText(after.NewSeconds) + ")" : "")); if (after.NewSeconds > 0 && DateTime.UtcNow.AddSeconds(after.NewSeconds + 120) < nextTakedown) nextTakedown = DateTime.UtcNow.AddSeconds(after.NewSeconds + 120); t = game.ReadTakedownSteady(); if (t.Page && t.Free > 0 && t.Attack == null && !t.AttackBelow && game.CloseTakedownWindow(text => { Log("A window stayed over the Takedown: " + text); SnapshotOnce("takedown window"); })) t = game.ReadTakedownSteady(); var parts = new List<string> { string.Format("{0} free attack{1} used", done, done == 1 ? "" : "s") }; if (t.Page && t.Free >= 0) parts.Add(t.Free == 0 ? "none left" : t.Free + " of " + t.FreeMax + " left"); if (start.Damage >= 0 && t.Damage > start.Damage) parts.Add(string.Format(CultureInfo.InvariantCulture, "+{0:N0} damage ({1:N0} this week{2})", t.Damage - start.Damage, t.Damage, t.Place.Length > 0 ? ", " + t.Place : "")); if (start.Stage > 0 && t.Stage > start.Stage) parts.Add("now at stage " + t.Stage + (t.StageName.Length > 0 ? " (" + t.StageName + ")" : "")); if (start.Health >= 0 && t.Health >= 0) parts.Add("health " + start.Health + " -> " + t.Health); Log("Takedown: " + string.Join(", ", parts)); if (start.Damage >= 0 && t.Damage > start.Damage && t.Damage - start.Damage <= done) game.Snapshot("takedown one damage", 60, failure: false); if (t.Page && t.Free == 0) freeGoneAt = DateTime.UtcNow; if (!crewWait && takedownHold != DateTime.MinValue) takedownHold = DateTime.MinValue; if (defendName != null && S.DefendTerritory) { Log("Territory: the Takedown is done - back to defending " + defendName); Defend(); } if (!t.Page || waitShort) return; var now = DateTime.UtcNow; if (t.Free == 0 && t.NextFreeSeconds > 0) nextTakedown = now.AddSeconds(Math.Max(120, Math.Min(3 * 3600, t.NextFreeSeconds + 30))); if (t.Free == 0 && t.NextFreeSeconds < 0 && saidOnce.Add("takedown refill text")) Log("(No free Takedown attacks left. Under the bar it says \"" + t.Under + "\". Looking again in an hour.)"); SnapshotOnce("takedown without free attacks"); if (t.NewSeconds > 0 && now.AddSeconds(t.NewSeconds + 120) < nextTakedown) nextTakedown = now.AddSeconds(t.NewSeconds + 120); static string SpanText(int seconds) var t = TimeSpan.FromSeconds(Math.Max(0, seconds)); return t.TotalHours >= 1 ? string.Format("{0}h {1:00}m", (int)t.TotalHours, t.Minutes) : string.Format("{0} min", Math.Max(1, t.Minutes)); if (max > 10 && ExpKeepsStamina(DateTime.UtcNow)) return Math.Max(Kept(max), max - 10); internal static int GiveAtLeast(int max, int kept) { return Math.Max(5, Math.Min(15, (max - kept - 5) / 5 * 5)); } string WhyNotGive(Header h) if (h == null || h.Stamina < 0 || h.StaminaMax <= 0) return "stamina unreadable"; int kept = GiveReserve(h.StaminaMax); int least = GiveAtLeast(h.StaminaMax, kept); if (h.Stamina - kept < least) return "gives once stamina is " + least + " above the " + kept + " kept for bosses and fights (" + h.Stamina + " now)"; if (S.Takedown && takedownWants > 0 && Can(Tab.Family) && !ExpHoldsHealth(DateTime.UtcNow)) return "the Takedown's free attacks come first"; void NoteGiveWhy(Header h) string why = !S.GiveStamina ? "" : family.NotInFamily ? "you're not in a family" : DateTime.UtcNow < giveBrokenUntil ? "giving didn't work, tries again at " + giveBrokenUntil.ToLocalTime().ToString("HH:mm") : WhyNotGive(h) ?? (BossDamageHold() ? "kept for the bosses during 2X boss damage" : DateTime.UtcNow < nextGive ? "gave just now, gives again at " + nextGive.ToLocalTime().ToString("HH:mm") : "gives this round"); if (why == giveWhySaid) return; PerkRow PickPerk(List<PerkRow> rows) if (S.StaminaPerk.Trim().Length > 0) var r = rows.FirstOrDefault(x => Game.SamePerk(x.Name, S.StaminaPerk)); if (r == null) SayOnce("perk missing", "The stamina perk \"" + S.StaminaPerk + "\" isn't on the Perks page - no stamina given (pick another on the Stamina page)"); else if (r.Maxed) { SayOnce("perk maxed", r.Name + " is at its top level - pick another stamina perk on the Stamina page"); r = null; } var pick = rows.Where(x => x.Left > 0 && !x.Maxed && x.Level >= 0 && x.Name.Length > 0).OrderByDescending(x => x.GiveReady).ThenBy(x => x.Left).FirstOrDefault(); if (pick == null) SayOnce("perk none", "Couldn't read how far any stamina perk is from its next level - no stamina given"); static string Progress(PerkRow r) return r == null || r.Have < 0 ? "?" : string.Format(CultureInfo.InvariantCulture, "{0:N0} of {1:N0}", r.Have, r.Need); try { GiveStaminaHere(); } if (nextGive > DateTime.UtcNow.AddMinutes(20)) giveBrokenUntil = nextGive; nextGive = DateTime.UtcNow.AddMinutes(5); var h = ReadBarsSettled(3000); if (!FamilyOpen("PERKS", ref nextGive)) return; h = ReadBarsSettled(6000); SayOnce("give stamina covered", "Give stamina: the stamina bar stayed covered by notifications - it tries again in 2 minutes"); SnapshotOnce("stamina bar covered at give stamina"); nextGive = DateTime.UtcNow.AddMinutes(2); if (WhyNotGive(h) != null) { Neutral(); return; } if (!open && !FamilyOpen("PERKS", ref nextGive)) return; var rows = game.WalkStaminaPerks(); if (!FamilyOpen("PERKS", ref nextGive)) return; rows = game.WalkStaminaPerks(); game.Snapshot("stamina perks unreadable", 60); GiveMissed("Couldn't read the stamina perks - no stamina given"); var pick = PickPerk(rows); if (pick == null) { nextGive = DateTime.UtcNow.AddMinutes(60); return; } int reserve = GiveReserve(h.StaminaMax), stamina = h.Stamina, given = 0; while (stamina - 5 >= reserve && given < 500) row = (row != null ? game.PerkRowInSight(pick.Name) : null) ?? game.FindPerkRow(pick.Name); game.Snapshot("stamina perk button not found", 60); GiveMissed("Couldn't find the GIVE 5 button of " + pick.Name + " - no more stamina given this time"); long haveBefore = row.Have; var pressedAt = DateTime.UtcNow; for (int look = 0; look < 3 && !counted; look++) if (look > 0) game.Wait(500); var r = game.PerkRowInSight(pick.Name, true); counted = haveBefore >= 0 && r.Have >= haveBefore + 4 && r.Have <= haveBefore + 6; if (!counted && ((row.Level > 0 && r.Level > row.Level) || (row.Need > 0 && r.Need > row.Need))) counted = true; string toast = game.ReadToast(800, t => Parse.Has(t, "GAVE") && Parse.Has(t, "STAMINA")); var after = ReadBarsSettled(6000, x => x.Stamina >= 0 && x.Stamina <= stamina - 4); Log("Gave stamina to " + pick.Name + " but couldn't see the 5 go (the stamina bar was covered) - giving again in 5 minutes"); if (after.Stamina > stamina - 4) if (game.ToastClosedSince(pressedAt, "SERVICES ARE BUSY") || game.ReadToast(300, t => Parse.Has(t, "SERVICES ARE BUSY")) != null) Log("Gave stamina to " + pick.Name + ", but the game said its family services are busy - giving again in 5 minutes"); nextGive = DateTime.UtcNow.AddMinutes(5); game.Snapshot("give stamina not counted", 60); GiveMissed(string.Format("GIVE 5 on {0} didn't take stamina ({1} before, {2} after)", pick.Name, stamina, after.Stamina)); if (seen != null) row = seen; if (given == 0) SnapshotOnce("stamina perk after GIVE 5"); family.StaminaGiven += 5; if (given == 0) { FamilyChanged(); return; } var now = game.PerkRowInSight(pick.Name) ?? row; Log(string.Format("Gave {0} stamina to {1} ({2} -> {3}), stamina now {4}/{5}", given, pick.Name, Progress(pick), Progress(now), stamina, h.StaminaMax)); if (now != null) NotePerks(rows.Select(x => Game.SamePerk(x.Name, pick.Name) ? now : x).ToList()); if (nextGive < DateTime.UtcNow.AddMinutes(30)) { nextGive = DateTime.UtcNow.AddMinutes(5); giveBrokenUntil = DateTime.MinValue; } nextFamily = DateTime.UtcNow.AddHours(3); if (!FamilyOpen("OVERVIEW", ref nextFamily)) return; var o = game.ReadFamilyOverviewAll(); if (o.Level < 0 && o.Name.Length == 0 && o.Xp < 0) { Log("Couldn't read the family's overview"); game.Snapshot("family overview unreadable", 60); } var old = family.Overview; if (old != null && old.Xp >= 0 && o.Xp >= 0 && old.XpNext == o.XpNext && o.Xp != old.Xp) Log(string.Format(CultureInfo.InvariantCulture, "Family XP {0:+#,0;-#,0} since {1:HH:mm} ({2:N0} of {3:N0})", o.Xp - old.Xp, family.OverviewAt, o.Xp, o.XpNext)); family.OverviewAt = DateTime.Now; var stale = DateTime.Now.AddHours(-6); DateTime ignore = DateTime.MinValue; if (family.TakedownAt < stale && FamilyOpen("TAKEDOWN", ref ignore)) NoteTakedown(game.ReadTakedownSteady()); if (family.PerksAt < stale && FamilyOpen("PERKS", ref ignore)) NotePerks(game.WalkStaminaPerks()); string day = DateTime.Now.ToString("yyyy-MM-dd"); if (familyLogDay != day) { familyLogDay = day; Log(FamilyLine()); } var ic = CultureInfo.InvariantCulture; var parts = new List<string>(); var p = new List<string> { (o.Name.Length > 0 ? o.Name : "your family") + (o.Tag.Length > 0 ? " [" + o.Tag + "]" : "") }; if (o.Level > 0) p.Add("level " + o.Level); if (o.Xp >= 0) p.Add(string.Format(ic, "family XP {0:N0} of {1:N0}", o.Xp, o.XpNext)); if (o.Members >= 0) p.Add(o.Members + " of " + o.MembersMax + " members"); if (o.Respect >= 0) p.Add(o.Respect.ToString("N0", ic) + " respect"); if (o.PerkLevels >= 0) p.Add(o.PerkLevels + " of " + o.PerkLevelsMax + " perk levels"); if (o.Territories >= 0) p.Add(o.Territories + " of " + o.TerritoriesMax + " territories"); if (o.Vault >= 0) p.Add("vault " + CashText(o.Vault) + (o.Gold >= 0 ? " and " + o.Gold + " gold bars" : "")); parts.Add(string.Join(", ", p)); var p = new List<string> { "Takedown" + (t.Name.Length > 0 ? " " + t.Name : "") }; if (t.Damage >= 0) p.Add(string.Format(ic, "your damage this week {0:N0}{1}", t.Damage, t.Place.Length > 0 ? " (" + t.Place + ")" : "")); if (t.Free >= 0) p.Add(t.Free + " of " + t.FreeMax + " free attacks"); if (t.Stage > 0) p.Add(string.Format(ic, "stage {0}{1}", t.Stage, t.StageHpMax > 0 ? string.Format(ic, " ({0:N0} of {1:N0} health left)", t.StageHp, t.StageHpMax) : "")); parts.Add(string.Join(", ", p)); if (family.Perks != null && family.Perks.Count > 0) parts.Add("stamina perks: " + string.Join(", ", family.Perks.Select(r => r.Name + (r.Level > 0 ? " " + r.Level : "") + (r.Have >= 0 ? " (" + Progress(r) + ")" : "")))); if (family.Attacks > 0 || family.StaminaGiven > 0) parts.Add(string.Format("this session: {0} Takedown attacks, {1} stamina given", family.Attacks, family.StaminaGiven)); return "Family: " + (parts.Count > 0 ? string.Join(" | ", parts) : "nothing read yet"); static string CashText(double v) var ic = CultureInfo.InvariantCulture; if (v >= 1e12) return "$" + (v / 1e12).ToString("0.##", ic) + "T"; if (v >= 1e9) return "$" + (v / 1e9).ToString("0.##", ic) + "B"; if (v >= 1e6) return "$" + (v / 1e6).ToString("0.##", ic) + "M"; return "$" + v.ToString("N0", ic); DateTime ignore = DateTime.MinValue; if (!FamilyOpen("OVERVIEW", ref ignore)) { if (family.NotInFamily) Log("Family: you're not in a family"); return; } family.Overview = game.ReadFamilyOverviewAll(); family.OverviewAt = DateTime.Now; if (FamilyOpen("TAKEDOWN", ref ignore)) var t = game.ReadTakedownSteady(); string why = TakedownBlock(t); Log(string.Format(" Takedown: free attacks {0} of {1} (bar {2} gold, under it \"{3}\"), tickets {4}, health {5} of {6} - {7}", t.Free, t.FreeMax, t.Segments, t.Under, t.Tickets, t.Health, t.HealthMax, why == null ? (S.Takedown ? "would attack" : "would attack, but that's switched off") : why.Length == 0 ? "none left" : why)); if (FamilyOpen("PERKS", ref ignore)) var rows = game.WalkStaminaPerks(); Log(string.Format(" stamina perk {0}: level {1} of {2}, {3}{4}", r.Name, r.Level, r.LevelMax, Progress(r), r.Left > 0 ? ", " + r.Left + " to go" : "")); var pick = rows.Count > 0 ? PickPerk(rows) : null; string why = WhyNotGive(last); Log(" spare stamina: " + (pick == null ? "no perk to give to" : "would go to " + pick.Name) + " - " + (!S.GiveStamina ? "switched off" : why == null ? "would give now" : "not now: " + why)); public event Action<Profile> ProfileRead; public event Action<Profile> ProfileChanged; Log("--- Checking your game: opening its tabs and reading them (it only opens tabs and rolls lists) ---"); State("Checking your game - about a minute"); var p = new Profile { Checked = DateTime.Now, Version = Program.Version }; var h = ReadBarsSettled(4000, x => x.Level > 0 && !string.IsNullOrWhiteSpace(x.Name) && x.Stamina >= 0); p.Name = (h.Name ?? "").Trim(); if (S.Account.Length > 0 && (p.Name.Length == 0 || Accounts.Same(p.Name, S.Account))) p.Name = S.Account; p.Level = h.Level > 0 ? h.Level : level; p.Xp = h.Xp; p.XpNext = h.XpNext; p.SkillPoints = h.SkillPoints; p.EnergyMax = h.EnergyMax; p.StaminaMax = h.StaminaMax; p.HealthMax = h.HealthMax; p.Cash = h.Cash; p.Banked = h.Banked; using (var f = game.Capture()) p.Window = f.Width + "x" + f.Height; if (p.Name.Length == 0) p.Note("your name (the top bar)"); game.OnlyNavigate = true; CheckPart(p, "the game's menu", () => var m = game.ReadWholeMenu(); if (m == null) { p.Note("the game's menu"); return; } p.Tabs = string.Join(",", m.Seen); p.MenuWhole = m.Whole && m.Unread == 0 ? 1 : 0; if (m.Unread > 0) p.Note(m.Unread + " entries of the game's menu"); CheckPart(p, "the Safehouse", () => var s = game.ReadSafehouse(); if (s == null || s.Found == 0) { p.Note("the Safehouse"); return; } p.SafeLevel = s.Level; p.SafeMax = s.MaxLevel; p.SafeBonus = s.Bonus; p.Respect = s.Respect; p.AttackPower = s.AttackPower; p.DefensePower = s.DefensePower; p.Income = s.Income; p.Hired = s.Hired; p.HireMax = s.HireMax; p.Gear = s.Gear; p.GearMax = s.GearMax; p.Capacity = s.Capacity; if (s.Family != null) { p.Family = s.Family; p.InFamily = 1; } p.Rank = Profile.NormalRank(s.Rank); if (s.Members >= 0) { p.Members = s.Members; p.MembersMax = s.MembersMax; } if (Open(p, Tab.Bank, "the Bank")) CheckPart(p, "the Bank's fee", () => using (var f = game.Capture()) p.BankFee = game.ReadBankFee(f, out line); if (p.BankFee >= 0) p.BankFeeFrom = "the Bank page"; if (Open(p, Tab.Operations, "Operations")) CheckPart(p, "Operations", () => using (var f = game.Capture()) { slots = game.ReadOpSlots(f); p.OfflineOpsOffer = game.OffersOfflineOps(f) ? 1 : 0; } p.OpsSlots = slots.Count; p.OpsOpen = slots.Count(s => !s.Locked); p.OpsPass = slots.Count(s => s.Pass); p.OpsRunning = slots.Count(s => s.Running || s.Done || s.Collect != null); if (ops != null) ops(slots); if (Open(p, Tab.Jobs, "Jobs")) CheckPart(p, "the Jobs list", () => var j = game.ReadJobsList(Catalog); p.CitiesSeen = string.Join(",", j.Cities.Select(i => GameData.Cities[i])); p.CitiesFolded = string.Join(",", j.Folded.Select(i => GameData.Cities[i])); if (game.LearnJobs(Catalog, 0, false) > 0) { GameData.Renumber(Catalog); SaveCatalog(); } if (Open(p, Tab.Crew, "the Crew")) CheckPart(p, "the Crew", () => using (var f = game.Capture()) c = game.ReadCrewTotals(f); p.Henchmen = c.Henchmen; p.HenchSlots = c.Slots; p.CrewAttack = c.Attack; p.CrewDefense = c.Defense; if (p.Henchmen < 0 && p.Hired >= 0) { p.Henchmen = p.Hired; p.HenchSlots = p.HireMax; } if (p.Lacks(Tab.Family)) { if (p.InFamily < 0) p.InFamily = 0; } CheckPart(p, "your family", () => var nav = game.OpenFamily("OVERVIEW"); if (nav == FamilyNav.NotInFamily || (nav == FamilyNav.Failed && game.NotInMenu(Tab.Family) && p.InFamily != 1)) p.InFamily = 0; p.Family = ""; p.Rank = ""; if (nav != FamilyNav.Open) { p.Note("your family"); return; } var o = game.ReadFamilyOverviewAll(); if (o.Name.Length > 0) p.Family = o.Name + (o.Tag.Length > 0 ? " [" + o.Tag + "]" : ""); if (o.Members >= 0) { p.Members = o.Members; p.MembersMax = o.MembersMax; } family.OverviewAt = DateTime.Now; if (Open(p, Tab.Bosses, "Bosses")) CheckPart(p, "the Bosses page", () => var b = game.ReadStageLook(p.Level); if (b == null) { p.Note("the Bosses page"); return; } p.BossStage = b.Stage; p.BossHighest = b.Highest; p.BossNeedLevel = b.NeedLevel; p.BossReady = b.Ready ? 1 : b.Cleared ? 0 : -1; if (Open(p, Tab.Heists, "Heists")) CheckPart(p, "Heists", () => using (var f = game.Capture()) { p.Favors = game.ReadFavors(f); p.FamilyHeists = game.ReadHeists(f).Count(r => r.YourFamily); } if (p.Favors >= 0) Info("favors", p.Favors.ToString()); finally { game.OnlyNavigate = false; } string file = Profile.FileFor(dir, p.Name); catch (Exception e) { Log("Couldn't save what the check found: " + e.Message); } foreach (var row in Setup.Found(p)) Log("Check - " + row[0] + ": " + row[1]); Log("--- Check finished (saved in " + System.IO.Path.GetFileName(file) + "). The Tasks page shows what it suggests. Nothing changes until you say so ---"); Log("--- Check finished (saved in " + System.IO.Path.GetFileName(file) + "). The window shows what it suggests. Nothing changes until you say so ---"); bool Open(Profile p, Tab t, string what) if (p.Lacks(t)) return false; CheckPart(p, what, () => { ok = game.OpenTab(t); if (!ok && !game.NotInMenu(t)) p.Note(what + " (the tab didn't open)"); }); void CheckPart(Profile p, string what, Action read) catch (StopException) { throw; } catch (UserBusyException) { throw; } catch (WindowChangedException) { throw; } catch (NeedUserException) { throw; } p.Note(what + " (" + e.Message + ")"); if (MemoryShort(e)) { Log("Checking your game: the PC ran out of memory (" + what + ")"); throw new OutOfMemoryPause(e); } SnapshotOnce("check " + what); Log("--- Scan: reading every screen, no actions ---"); Log(string.Format("Level {0}, XP {1}/{2}, energy {3}/{4}, stamina {5}/{6}, health {7}/{8}, skill points {9}", last.Level, last.Xp, last.XpNext, last.Energy, last.EnergyMax, last.Stamina, last.StaminaMax, last.Health, last.HealthMax, last.SkillPoints)); if (job != null) Log("Job it would do now: " + job.Name + " (" + job.Cost + " energy)"); if (game.OpenTab(Tab.Heists)) using (var f = game.Capture()) Log("Heist favors left: " + game.ReadFavors(f)); foreach (var r in game.ReadHeists(f)) Log(string.Format(" heist \"{0}\": {1}, +{2} XP per help{3}{4}", r.Name, r.HelpText, r.XpPerHelp, r.YourFamily ? " (your family)" : "", r.CanHelp && S.WouldHelp(r.YourFamily, r.XpPerHelp, JobXpPerEnergy() * HelpCost(r)) ? " <- would help" : "")); if (game.OpenTab(Tab.Fight)) using (var f = game.Capture()) var rows = game.ReadFightRows(f, out war); Log("Fight list" + (war ? " (at war)" : "") + ": " + string.Join(" ", rows.Select(r => string.Format("#{0} {1} steal {2}% {3} stam{4}", r.Index + 1, r.War ? "war target" : "lvl " + r.Level, r.Steal, r.StaminaCost, r.Ready ? "" : " (not ready)")))); if (game.OpenTab(Tab.Operations)) using (var f = game.Capture()) var slotsSeen = game.ReadOpSlots(f); if (ops != null) ops(slotsSeen); foreach (var s in slotsSeen) Log(string.Format(" operation slot {0}: {1}", s.Index + 1, s.Locked ? "locked" : s.Collect != null || s.Done ? "READY to collect: " + s.Name : s.Running ? s.Name + ", " + (s.SecondsLeft >= 0 ? TimeSpan.FromSeconds(s.SecondsLeft).ToString() + " left" : "running") : "free")); if (game.OpenTab(Tab.Bosses)) var b = game.ReadStageLook(level); Log(b == null ? "Bosses: the stages page didn't read" : string.Format("Bosses: stage {0}{1}, highest stage {2}{3}{4}", b.Stage, b.Ready ? " (ATTACK shows)" : b.Cleared ? " (beaten today)" : "", b.Highest, b.ResetSeconds >= 0 ? ", resets in " + Parse.Short(b.ResetSeconds) : "", b.NeedLevel > 0 ? ", the next locked stage needs level " + b.NeedLevel : "")); if (game.OpenTab(Tab.Crew)) using (var f = game.Capture()) System.Drawing.Rectangle box; var auto = game.FindAutoEquip(f, out box, out why); Log("Crew: " + game.ReadCrewTotals(f) + "; AUTO EQUIP BEST " + (auto != null ? "found at " + (box.X + box.Width / 2) + "," + (box.Y + box.Height / 2) : "not pressable: " + why)); var info = game.ReadSafehouse(); foreach (var line in info.Lines()) Log(line); if (h != null && info.Found > 0) h(info); Log("--- Scan finished ---");