using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; namespace IdleMafiaBot { sealed class Counters { 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 double CrewCash; public double FightCash; public Counters Copy() { return (Counters)MemberwiseClone(); } public string Text() { return string.Join(";", typeof(Counters).GetFields().Select(f => f.Name + "=" + Convert.ToString(f.GetValue(this), CultureInfo.InvariantCulture))); } public static Counters Parse(string text) { var c = new Counters(); foreach (var part in (text ?? "").Split(';')) { int eq = part.IndexOf('='); if (eq <= 0) continue; var f = typeof(Counters).GetField(part.Substring(0, eq).Trim()); double v; 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); } return c; } 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) : ""); } } sealed class RegenGuess { public int Period, Max = -1; int prevTick = -1, value = -1, tick = -1; DateTime at; 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 (v < max && t > 0) { if (prevTick > 0 && t > prevTick + 2) Period = Math.Min(900, Math.Max(Period, t)); prevTick = t; } else prevTick = -1; 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); } } sealed partial class Bot { public readonly Settings S; public readonly Counters Count = new Counters(); public readonly Discord Discord; public readonly StatsCard Stats; public readonly RemoteControl Remote; public List Catalog; public volatile List CatalogView; readonly string dir, logFile; string catalogFile; static readonly object logLock = new object(); Game game; Thread thread; volatile bool stop; volatile bool scanRequested, checkRequested; volatile bool finishing, keepRoll, stopAgain; volatile int finishEnd; 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 Logged; public event Action StateChanged; public event Action
HeaderRead; public event Action InfoChanged; public event Action> 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; string lastError; bool learnedThisRun, waitingForUser, logBroken, catalogBuiltIn; volatile bool levelledStopped; readonly HashSet firstSeen = new HashSet(); void SnapshotOnce(string what) { if (firstSeen.Add(what)) game.Snapshot(what, 0, true); } Header last; public Bot(Settings s, string dir) { S = s; this.dir = 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(); level = s.LastLevel; crewUnequipped = s.CrewGearOff; ownOpenBefore = s.CrewOpenBefore; if (s.ScreenFirst) RobloxWindow.ScreenFirst = true; Discord = new Discord(s, Log); HeaderRead += NoteTicks; Stats = new StatsCard(this, s, Log, SaveSettings); Remote = new RemoteControl(s, Log, SaveSettings, Stats); } public bool Running { get { return thread != null && thread.IsAlive; } } public bool Once; 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) { if (Running) return; scanRequested = scan; checkRequested = check; stop = false; finishing = keepRoll = stopAgain = false; thread = new Thread(Run) { IsBackground = true, Name = "bot" }; thread.SetApartmentState(ApartmentState.MTA); thread.Start(); } public void Stop() { Stop(false); } public void Stop(bool keep) { if (keep) keepRoll = true; if (stop) { stopAgain = true; finishing = false; } stop = true; stopWhy = null; List others; lock (partners) others = partners.ToList(); foreach (var p in others) p.Stop(keep); } public bool StopPressed { get { return stop; } } public bool OwnRollRuns { get { var r = autoRun; if (r != null && r.Running) return true; List others; lock (partners) others = partners.ToList(); return others.Any(p => p.OwnRollRuns); } } volatile Turns turns; readonly List partners = new List(); Bot lead; volatile string who; DateTime idleUntil; bool Rejoins { get { return S.Rejoin && turns == null; } } static readonly TimeSpan TurnLength = TimeSpan.FromMinutes(3); DateTime turnStarted = DateTime.MaxValue; bool TurnUp(string task) { return turns != null && task != "bank" && DateTime.UtcNow - turnStarted >= TurnLength; } void SayTurnUp(List 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 others) { if (Running) return; List made; lock (partners) { partners.Clear(); 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; } who = S.Account; var t = new Turns(new[] { this }.Concat(made)); foreach (var p in made) p.turns = t; 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); Begin(false); } public void StartAllWhenSeen() { new Thread(() => { 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; } } if (Running) return; Log("Turns: the other accounts' Roblox windows weren't read again - " + (S.Account.Length > 0 ? S.Account : "this account") + " plays alone"); Start(); }) { IsBackground = true, Name = "turns again" }.Start(); } Bot Partner(Instance i) { 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)); theirs.Account = i.Name; var p = new Bot(theirs, dir) { lead = this, who = i.Name, pinned = i.Handle }; p.Discord.Pc = S; p.accountIniAt = IniTime(ini); p.FromLead(); p.UseAccountFiles(); p.Logged += line => { var h = Logged; if (h != null) h(line); }; if (copy) { p.Log("Account: " + i.Name + " is new here: it starts with " + S.Account + "'s settings and the game's job list."); p.SaveSettings(); } return p; } DateTime accountIniAt; 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)); } void FromLead() { var l = lead; if (l == null) return; string ini = Accounts.Ini(dir, S.Account), name = S.Account; var at = IniTime(ini); if (at != DateTime.MinValue && at != accountIniAt) { var theirs = Settings.Load(ini); if (theirs.Unreadable == null) { S.TakeAccountFields(theirs); S.Account = name; Log("Settings: " + name + "'s own settings were changed - they're used from this turn on"); } accountIniAt = at; } bool screenFirst = S.ScreenFirst; S.TakePcFields(l.S); S.ScreenFirst |= screenFirst; S.StatsLink = ""; S.RemoteControl = false; } void UseAccountFiles() { catalogFile = Accounts.Jobs(dir, S.Account); catalogBuiltIn = !File.Exists(catalogFile); Catalog = GameData.LoadCatalog(catalogFile); CatalogView = Catalog.Select(j => j.Copy()).ToList(); level = S.LastLevel; } int BarsNeed() { 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)); } void Turn() { var t = turns; if (t == null) { Tick(); return; } if (DateTime.UtcNow < idleUntil) { Sleep(500); return; } if (WaitingInTurns()) return; int need = BarsNeed(); 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; } FromLead(); turnStarted = DateTime.UtcNow; try { 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"); game.Win.Front(); Sleep(400); } Tick(); } finally { t.Pass(this); } } bool WaitingInTurns() { if (!S.WaitWhileBusy || !game.Win.UserActiveWithin(S.IdleSeconds * 1000)) return false; State("Waiting until you stop using the mouse/keyboard (several accounts in turns)"); Sleep(1500); return true; } public event Action> InstancesChanged; public List InstancesSeen { get; private set; } volatile IntPtr pinned; public IntPtr Pinned { get { return pinned; } } public bool PlayOn(Instance which) { if (Running || which == null || which.Name == null) return false; pinned = which.Handle; var seen = InstancesSeen; RobloxWindow.Taken = seen == null ? new IntPtr[0] : seen.Where(i => i.Handle != which.Handle).Select(i => i.Handle).ToArray(); lock (accountLock) { 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 (kept != null) { last = kept; var hr = HeaderRead; if (hr != null) hr(kept); } lookNow.Set(); return true; } 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; g.Win.NoFallback = true; Header head = null; try { 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()) { var v = g.ViewOf(f); if (v.Problem != null) continue; g.SeedMax(S.BarMax); var h = g.ReadHeader(f, v); if (h != null && Accounts.Plausible(h.Name)) head = h; } } } catch (Exception) { } 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()).Where(i => i.Handle == which.Handle).Concat(new[] { which })) { i.Name = name; i.Head = head; } KeepProfile(name); KeepHeader(head); var changed = InstancesChanged; if (changed != null && InstancesSeen != null) changed(InstancesSeen); return true; } readonly Dictionary headers = new Dictionary(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) { Header h; 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; InstancesSeen = now; if (now.Count > 1) { 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; if (h != null) h(now); } void KeepProfile(string name) { if (Directory.Exists(Accounts.Folder(dir, name))) return; try { 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(() => { Native.RealPixels(); var reader = new Game(msg => { }, () => false); var win = reader.Win; win.ScreenOnly = true; string seen = null; while (true) { try { if (!Running) { LookForInstances(reader); win.Pinned = pinned; string state = !win.Find() ? "not running" : win.Minimized ? "minimized" : RobloxWindow.ScreenFirst && !win.InSight() ? "covered" : null; Header h = null; if (state == 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); } } NoteScreenFirst(); if ((state ?? "found") != seen) { seen = state ?? "found"; Info("roblox", seen); } if (h != null) { KeepHeader(h); 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; } var before = last; last = h; var hr = HeaderRead; if (hr != null) hr(h); if (!GuessBar(energyRegen, DateTime.UtcNow, ref h.Energy, ref h.EnergyMax) && before != null) { h.Energy = before.Energy; h.EnergyMax = before.EnergyMax; } } } } catch (Exception e) { if (MemoryShort(e)) { SayMemory(); Thread.Sleep(60000); } } lookNow.WaitOne(Running ? 5000 : 3000); } }) { IsBackground = true, Name = "watch" }; watcher.Start(); } public void Log(string msg) { var w = who; string line = DateTime.Now.ToString("HH:mm:ss") + " " + (w != null ? "[" + w + "] " : "") + msg, problem = null; lock (logLock) { if (logWrites++ % 200 == 0) try { var fi = new FileInfo(logFile); if (fi.Exists && fi.Length > 5 * 1024 * 1024) { string old = Path.Combine(dir, "IdleMafiaBot.1.log"); File.Delete(old); File.Move(logFile, old); } } catch (Exception) { } 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; } } var h = Logged; if (h != null) { h(line); if (problem != null) h(problem); } } public void LogDetails(string text) { lock (logLock) try { File.AppendAllText(logFile, text + Environment.NewLine); } catch (Exception) { } } 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); } void Run() { Native.RealPixels(); 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.SeedMax(S.BarMax); 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)(() => Discord.JpgOf(file)) : null, true, 180); game.Failed = TaskFailed; ShowStuck(); DiscordStart(); firstSeen.Clear(); redone.Clear(); oneJobSaid = null; DateTime now = DateTime.UtcNow; nextOps = nextBosses = nextPlaytime = nextContracts = nextCrates = nextHeists = nextFights = nextBank = nextPoints = nextJobs = nextLearn = nextGameStats = now; nextBosses = BossesDoneUntil(now); ResetExpState(); ResetMenuNumbers(); fullClock.Reset(); ResetFamily(now); LoadDefence(); nextEquip = nextShop = nextSafehouse = nextReroll = nextMarket = now; nextUpgrade = nextCrew = nextCrewSlot = now; if (levelledStopped) { levelledStopped = false; PropertiesLevelUp(); } propRecheck = true; nextBriefcases = nextEvents = nextGlobalBoss = nextKnock = now; ResetGlobalBoss(); badgeSig = eventsSig = null; eventsOn.Clear(); briefcasesSaid = null; rerollCrew = null; rerollSaid = null; lastBank = DateTime.MinValue; learnedThisRun = false; foldDue = true; lastError = null; frontFails = 0; rejoiner = new Rejoiner(); #if LINUX rejoiner.Playing(now); #endif dialogSeen = null; dialogUp = disconnectHint = robloxUsed = false; robloxUsedAt = DateTime.MinValue; focusBack = IntPtr.Zero; gameInput = now; tutorialSince = now; 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(); try { if (ocr != null) { Log("Problem: " + ocr); Info("ocr", ocr); stopWhy = ocr; return; } while (!stop) { 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"); waitingForUser = true; State("Waiting until you stop using the mouse/keyboard"); if (S.WaitWhileBusy) Sleep(S.IdleSeconds * 1000); else { Sleep(1500); Native.POINT a, b; Native.GetCursorPos(out a); int since = Environment.TickCount, start = since; while (!stop && Environment.TickCount - since < 1500 && Environment.TickCount - start < 30000) { Thread.Sleep(250); 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; lastError = e.Message; 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; game.ForgetView(); Sleep(repeats > 2 ? 60000 : 10000); } catch (OutOfMemoryPause e) { MemoryShort(e.InnerException); WaitForMemory(SayMemory()); } catch (Exception e) { bool memory = MemoryShort(e); repeats = e.Message == lastError ? repeats + 1 : 0; lastError = e.Message; 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(); } finally { RerollOnly = false; FightOnly = false; try { FoldTotals(); } catch (Exception) { } var t = turns; if (t != null) { t.Leave(this); if (lead == null) { List others; 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(); } turns = null; } Log("Bot stopped"); if (lead == null) who = null; if (stopWhy != null) { var said = StoppedMessage(stopWhy); Discord.Problem("stopped", said[0], said[1], Shot, false, 0); } stopWhy = null; if (lead != null) Discord.Close(); State("Stopped"); } } 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." }; } void Sleep(int ms) { 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); e = e.InnerException; } return 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; return false; } internal bool MemoryShort(Exception e) { lock (memoryLock) { if (MemoryCause(e) == null && !(PictureFailed(e) && DateTime.UtcNow < memoryAt.AddMinutes(5))) return false; memoryAt = DateTime.UtcNow; return true; } } bool SayMemory() { lock (memoryLock) { if (DateTime.UtcNow < memorySaid.AddMinutes(10)) return false; memorySaid = DateTime.UtcNow; } string text; try { text = Native.MemoryText(); } catch (Exception) { text = "not known"; } Log("Memory: " + text); return true; } public void NoteMemory(Exception e) { try { if (MemoryShort(e)) SayMemory(); } catch (Exception) { } } void WaitForMemory(bool say) { try { Ocr.Release(); System.Runtime.GCSettings.LargeObjectHeapCompactionMode = System.Runtime.GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); } catch (Exception) { } 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"); Sleep(60000); } void Tick() { DiscordSummary(); if (!game.Win.Find()) { dialogUp = false; 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...")); Sleep(4000); return; } if (game.Win.IsForeground && game.Win.UserActiveWithin(5000)) { robloxUsed = true; robloxUsedAt = DateTime.UtcNow; } Header h = null; if (!game.Win.Minimized) { string unreadable = null; bool leftAnimals = false; Game.Disconnect dialog = null; bool looked; using (var f = game.Capture()) { var v = game.ViewOf(f); string other = RobloxWindow.OtherWindow(); robloxCheckUp = other != null; if (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); NoteLoading(f); 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; } } else { h = game.ReadHeader(f, v); NoteRobloxBar(game.RobloxBar); if (S.Events && !game.RobloxBar) NoteBadges(f); NoteMenuNumbers(f); } looked = LookForDisconnect(f, h == null || (h.Energy < 0 && h.Stamina < 0 && h.Health < 0), ref dialog); } if (leftAnimals) { Log("Closing the Animals window left open over the game's menu"); if (game.CloseAnimals()) { game.ForgetView(); return; } } NoteEmptyPictures(); 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); } return; } GameReadable(); if (awaySince != DateTime.MinValue) BackIn(); if (!string.IsNullOrWhiteSpace(h.Name) && NoteAccount(h.Name)) { State("Another account is in Roblox: taking its own settings"); Sleep(2500); return; } var before = last; 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; xpNextLast = 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); } level = h.Level; S.LastLevel = 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"); level = -1; S.LastLevel = -1; } S.BarMax = game.KnownMax; last = h; NoteBarsMoving(h, DateTime.UtcNow); NoteHealth(h.Health, h.HealthMax, DateTime.UtcNow); var hr = HeaderRead; if (hr != null) hr(h); GuessBar(staminaRegen, DateTime.UtcNow, ref h.Stamina, ref h.StaminaMax); GuessBar(energyRegen, DateTime.UtcNow, ref h.Energy, ref h.EnergyMax); NoteFullBars(h); if (h.Energy < 0 && before != null) { h.Energy = before.Energy; h.EnergyMax = before.EnergyMax; } } if (scanRequested) { if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) { State("Waiting until you stop using the mouse/keyboard (scan ready)"); Sleep(1500); return; } if (!Act(delegate { try { Scan(); } catch (NeverPressException) { Log("--- Scan stopped: a press was refused ---"); } })) return; scanRequested = false; stop = true; return; } if (checkRequested) { if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) { State("Waiting until you stop using the mouse/keyboard (check ready)"); Sleep(1500); return; } if (!Act(delegate { CheckGame(); })) return; checkRequested = false; stop = true; return; } if (h == null) { 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"); Sleep(1500); return; } if (frontFails == 0) Log("Roblox is minimized - restoring it"); Act(delegate { }); return; } if (SweepRobloxBar()) return; if (RerollOnly) { if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) { State("Waiting until you stop using the mouse/keyboard (only rerolling)"); Sleep(1500); return; } bool goDue = DateTime.UtcNow >= nextReroll; if (!goDue && !RollStopDue(DateTime.UtcNow)) { State("Only rerolling: the next go at " + nextReroll.ToLocalTime().ToString("HH:mm")); Sleep(2000); return; } if (DisconnectBeforeRound()) return; State("Working: only rerolling the crew"); Act(delegate { if (goDue) Log("Doing: only rerolling the crew" + (h.Cash >= 0 ? " (" + M(h.Cash) + " on hand)" : "")); CloseEventWindow(); if (GearBackDue(DateTime.UtcNow)) RunTask("gear back", h); if (RollStopDue(DateTime.UtcNow)) RunTask(RollStopTask, h); if (!goDue) return; var brk = rerollerBreak; rerollerBreak = null; 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); } taskNow = "rerolls"; failedNow = neutralNow = false; try { DoRerolls(true); if (!failedNow && !neutralNow) TaskWorked("rerolls"); } finally { taskNow = null; if (failedNow) StreakCheck("rerolls"); } }); return; } if (FightOnly) { if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) { State("Waiting until you stop using the mouse/keyboard (only fighting)"); Sleep(1500); return; } bool goDue = DateTime.UtcNow >= nextFights; if (!goDue && !RollStopDue(DateTime.UtcNow)) { State("Only fighting: the next go at " + nextFights.ToLocalTime().ToString("HH:mm")); Sleep(2000); return; } if (DisconnectBeforeRound()) return; State("Working: only fighting players"); Act(delegate { if (goDue) Log(string.Format("Doing: only fighting players (stamina {0}/{1}, health {2}/{3})", h.Stamina, h.StaminaMax, h.Health, h.HealthMax)); CloseEventWindow(); if (GearBackDue(DateTime.UtcNow)) RunTask("gear back", h); if (RollStopDue(DateTime.UtcNow)) RunTask(RollStopTask, h); if (!goDue) return; taskNow = "fights"; failedNow = neutralNow = false; int fought = 0; try { fought = DoFights(100, S.OnlyFightsSpendAll ? 0 : AboveKept); if (!failedNow && !neutralNow) TaskWorked("fights"); } finally { taskNow = null; if (failedNow) StreakCheck("fights"); } AfterFightOnly(fought); }); return; } LookForTutorial(); var due = WhatIsDue(h); if (due.Contains("learn jobs")) due = new List { "learn jobs" }; if (due.Count == 0) { if (Once) { 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)); stop = true; return; } State("Running - nothing to do right now"); if (turns != null) { idleUntil = DateTime.UtcNow.AddSeconds(60); return; } Sleep(2500); 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)"); waitingForUser = true; State("Waiting until you stop using the mouse/keyboard (" + string.Join(", ", due) + " ready)"); Sleep(1500); return; } 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)); CloseEventWindow(); var left = new List(); 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)); game.Check(); if (i > 0) TerritoryCheck(); RunTask(due[i], h); BossesAfter(due, i); BankNext(due, i); } SayTurnUp(left); if (!due.Contains("learn jobs") || RerollOnly || FightOnly) return; Header h2; using (var f = game.Capture()) h2 = game.ReadHeader(f); if (h2.Energy < 0 && last != null) { h2.Energy = last.Energy; h2.EnergyMax = last.EnergyMax; } last = h2; 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)); left.Clear(); 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)); game.Check(); RunTask(more[i], h2); BossesAfter(more, i); BankNext(more, i); } SayTurnUp(left); }); if (acted) waitingForUser = false; if (Once && acted) { Log("Test round finished: " + Count); stop = true; } } int loadingLookAt = Environment.TickCount - 60000; string versionSaid; void NoteLoading(Frame f) { if (Environment.TickCount - loadingLookAt < 30000) return; loadingLookAt = Environment.TickCount; bool updated; if (!Game.GameLoading(f, out updated)) return; string v = Game.LoadingVersion(f); if (v == null || v == versionSaid) return; versionSaid = v; Log("The game is loading" + (updated ? " its new version " : ", version ") + v); } static string Doing(List 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" : "")); } bool acting; bool Act(Action work) { 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)); return false; } if (frontFails > 0) { Log("Roblox is in front again (after " + frontFails + " tries)"); frontFails = 0; } bool userBack = false; acting = true; try { work(); } catch (UserBusyException) { userBack = true; throw; } finally { acting = false; rejoiner.Playing(DateTime.UtcNow); game.Win.EndAct(S.GiveFocusBack && !stop, !stop && !userBack); } return true; } bool Can(Tab t) { return game == null || !game.NotInMenu(t); } internal static void FullBarsFirst(List 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>[] hour = { new List>(), new List>() }; 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; lastRead = now; 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(now, gap)); total[i] += gap; } was[i] = isFull; 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) { fullShown = now; 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 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(); bool rollStop = RollStopDue(now); if (rollStop) due.Add(RollStopTask); if (GearBackDue(now)) due.Add("gear back"); if (S.GlobalBoss && globalBossUp) { if (ExpLastCall(now)) { 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"); } else { expSaid.Remove("gb"); 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"); NoteGiveWhy(h); 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"); FullBarsFirst(due, h); if (cashOut && S.Bank && Can(Tab.Bank) && (!AutoRollHoldsBank() || rollStop)) { due.Remove("bank"); 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); due.Insert(at, "bank"); } return due; } sealed class MenuNumber { public DateTime Red = DateTime.MinValue, Plain = DateTime.MinValue; public int RedReads; public DateTime Visited = DateTime.MinValue; public bool Anyway = true; public DateTime IgnoreUntil = DateTime.MinValue; public DateTime Safety = DateTime.MinValue; } readonly Dictionary menuNumbers = new Dictionary(); void ResetMenuNumbers() { 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; var v = game.MenuNow(f); 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) { MenuNumber n; 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)) { MenuNumber n; 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.Visited = now; n.Anyway = anyway; n.Safety = safety; n.RedReads = 0; n.Red = DateTime.MinValue; } sealed class Streak { public DateTime Since; public int Fails; public string Why = ""; public bool Said; } readonly Dictionary streaks = new Dictionary(); string taskNow; bool failedNow; bool neutralNow; void Neutral() { neutralNow = true; } static string TaskTitle(string task) { switch (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) { string task = taskNow; if (task == null || string.IsNullOrWhiteSpace(why)) return; Streak s; if (!streaks.TryGetValue(task, out s)) streaks[task] = s = new Streak(); var now = DateTime.UtcNow; if (failedNow) { if (own) { s.Why = why.Trim(); if (s.Said) ShowStuck(); } return; } failedNow = true; if (s.Fails == 0) s.Since = now; s.Fails++; s.Why = why.Trim(); } void StreakCheck(string task) { Streak s; if (!streaks.TryGetValue(task, out s) || s.Said || (s.Fails < 3 && DateTime.UtcNow - s.Since < TimeSpan.FromHours(1))) return; s.Said = true; string line = StuckLine(task, s); Log(line); ShowStuck(); 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) { Streak s; if (!streaks.TryGetValue(task, out s)) return; streaks.Remove(task); if (!s.Said) return; Log(TaskTitle(task) + ": working again (stuck from " + s.Since.ToLocalTime().ToString("HH:mm") + ")"); ShowStuck(); } 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)" : ""); } void ShowStuck() { Info("stuck", string.Join("\n", streaks.Where(kv => kv.Value.Said).Select(kv => StuckLine(kv.Key, kv.Value)))); } void RunTask(string task, Header h) { taskNow = task; 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) { redone.Remove(task); Later(task, 10); 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); } throw; } catch (NeverPressException) { redone.Remove(task); Later(task, 30); } catch (StopException) { throw; } catch (NeedUserException) { throw; } catch (Exception e) { redone.Remove(task); 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); } finally { rejoiner.Playing(DateTime.UtcNow); if (SpendsEnergy.Contains(task)) energyRegen.Forget(); if (SpendsStamina.Contains(task)) staminaRegen.Forget(); } } static readonly HashSet SpendsEnergy = new HashSet { "jobs", "heists", "tutorial", "skill points", "learn jobs", "animals" }; static readonly HashSet SpendsStamina = new HashSet { "bosses", "fights", "takedown", "give stamina", "tutorial", "skill points", "global boss" }; void Later(string task, int minutes) { var when = DateTime.UtcNow.AddMinutes(minutes); switch (task) { 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 redone = new HashSet(); void DueAgain(string task) { var now = DateTime.UtcNow; switch (task) { 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) { switch (task) { case "operations": DoOperations(); break; case "bosses": DoBosses(); break; case "2x exp": if (ClaimHeldExp(S.Bosses && Can(Tab.Bosses) ? Tab.Bosses : Tab.Safehouse, "the bosses")) bossesNow = true; else { Neutral(); expClaimAgain = DateTime.UtcNow.AddMinutes(5); } break; case "playtime rewards": { 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); break; } case "contracts": { 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); break; } 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; case "gear back": { game.OpenTab(Tab.Crew); game.ScrollPage(0.62, 40); if (DoEquip() || !crewUnequipped) break; gearBackFails++; 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")); break; } case "gear": DoGear(); break; case "read safehouse": ReadSafehouse(); break; case "animals": DoAnimals(h); break; case "game stats": DoGameStats(); break; case "skill points": { 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}"); if (game.PointsAtStop) { 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); break; 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; case "bank": if (BankJustRan(DateTime.UtcNow)) { if (nextBank < lastBank.AddSeconds(15)) nextBank = lastBank.AddSeconds(15); Neutral(); break; } DoBank(); break; 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)); return n; } DateTime nextMarket; void DoRelist() { 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; int used = shop.Used; 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)"); Neutral(); return; } var done = new List(); var turned = new HashSet(); 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; } if (last == null) break; if (!last.ForSale) { 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); break; } 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); break; } S.MarketPending = last.Name + "|" + last.Left + "|" + last.Price; SaveSettings(); var removed = game.RemoveListing(last, used); if (removed == false) { S.MarketPending = ""; SaveSettings(); game.NoteFailure("Black Market: " + last.Name + " wasn't taken out"); break; } if (removed == null) { 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); break; } int listed; 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; } if (why != null) { 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); break; } S.MarketPending = ""; SaveSettings(); 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)); break; } } if (done.Count > 0) Log("Black Market: relisted " + string.Join(", ", done) + " - back on top"); } bool PendingBackUp() { if (S.MarketPending.Length == 0) return true; var p = S.MarketPending.Split('|'); int qty, price; if (p.Length != 3 || !int.TryParse(p[1], out qty) || !int.TryParse(p[2], out price) || qty <= 0 || price <= 0 || p[0].Length < 3) { S.MarketPending = ""; SaveSettings(); return true; } string name = p[0]; 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"); S.MarketPending = ""; checkUndoneAt = DateTime.MinValue; SaveSettings(); return true; } 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); return false; } checkUndoneAt = DateTime.MinValue; int listed; 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; } if (why == null) { Log("Black Market: put " + name + " back up (" + listed + " at " + price + " each)"); S.MarketPending = ""; SaveSettings(); return true; } if (why == Game.NotTradable) { Log("Black Market: " + name + " isn't among your tradable items any more - nothing to put back up"); S.MarketPending = ""; SaveSettings(); return true; } 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"); MarketPing(name, why); nextMarket = DateTime.UtcNow.AddMinutes(MarketRetryMinutes); return false; } 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; #if LINUX && !MAC const string CheckWhere = "on the Live page with the mouse on (http://127.0.0.1:8090/?control)"; #else const string CheckWhere = "in the game"; #endif DateTime checkUndoneAt { get { DateTime at; 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) { Sleep(15000); if (stop) break; 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; } } catch (Exception) { } } return false; } 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; SaveSettings(); nextMarket = DateTime.UtcNow.AddMinutes(MarketRetryMinutes); } const int MarketRetryMinutes = 30; bool NeedLearn() { 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; } bool JobsReadRecently() { DateTime at; 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); } void LearnJobs() { 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 fold = FoldCities(); 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(); if (other != null) { Catalog = kept; CatalogView = Catalog.Select(j => j.Copy()).ToList(); var cc = CatalogChanged; if (cc != null) cc(); 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"); return; } GameData.Renumber(Catalog); SaveCatalog(); if (FoldCities().Any(c => !fold.Contains(c))) foldDue = true; learnedThisRun = 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)); ReportTarget(); } string OtherOnTop() { Header h; 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 + ")"; return null; } void SaveCatalog() { CatalogView = Catalog.Select(j => j.Copy()).ToList(); try { 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); } var h = CatalogChanged; if (h != null) h(); } public JobInfo PickJob(Header h) { return PickJob(h, Catalog); } public JobInfo PickJob(Header h, List catalog) { if (S.JobMode == JobMode.OneJob) { JobInfo one; return OneJobWhy(h, catalog, out one) == null && !JobAside(one) ? one : null; } int lv = JobLevel(h); 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(); } bool foldDue = true; HashSet FoldCities() { var picked = PickJob(last); int keep = picked == null ? -1 : GameData.CityIndex(picked.City); return new HashSet(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; string oneJobSaid; public string OneJobWhy(Header h, List 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; bool TakeLevel(int read) { if (level <= 0 || read <= level + 5) { jumpLevel = -1; return true; } var now = DateTime.UtcNow; if (read != jumpLevel) { 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 JobLevel(Header h) { 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 catalog, int level, int energyMax, out JobInfo job) { job = null; 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; return null; } public const string OneJobNone = "no job picked"; public DateTime JobAsideUntil(string name) { DateTime until; lock (jobAside) return name != null && jobAside.TryGetValue(name, out until) && DateTime.UtcNow < until ? until : DateTime.MinValue; } void NoteOneJob(Header h) { JobInfo job; 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)"); else if (why != null) 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)"); oneJobSaid = said; Info("job", why ?? job.Name); } readonly Dictionary jobAside = new Dictionary(); readonly Dictionary jobMisses = new Dictionary(); bool JobAside(JobInfo j) { DateTime until; lock (jobAside) return jobAside.TryGetValue(j.Name, out until) && DateTime.UtcNow < until; } void JobMissed(JobInfo job, string why) { int n; jobMisses.TryGetValue(job.Name, out n); jobMisses[job.Name] = ++n; game.NoteFailure("\"" + job.Name + "\" " + why); if (n < 2) return; 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")); ReportTarget(); nextJobs = DateTime.UtcNow; } bool JobReady(Header h) { var job = PickJob(h); 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; } void ReportTarget() { if (S.Jobs && S.JobMode == JobMode.OneJob) NoteOneJob(last); var job = PickJob(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" : "")); } void DoJobs() { var job = PickJob(last); 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 (foldDue) { foldDue = false; var fold = FoldCities(); if (fold.Count > 0) game.SetCities(new int[0], fold); } var card = game.FindJob(job, Catalog); if (card == null) { int ci = GameData.CityIndex(job.City); if (ci >= 0 && game.ExpandCity(ci)) card = game.FindJob(job, Catalog); } if (card == null) { 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"); return; } var lm = Regex.Match(card.ButtonText ?? "", @"LEVEL\s*([0-9OIlS]+)", RegexOptions.IgnoreCase); int opensAt; 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)); job.TierLevel = opensAt; int by = JobLevel(last); 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"); } SaveCatalog(); ReportTarget(); nextJobs = DateTime.UtcNow; return; } 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"); ReportTarget(); nextJobs = DateTime.UtcNow; return; } } 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"); ReportTarget(); nextJobs = DateTime.UtcNow; return; } nextJobs = DateTime.UtcNow.AddMinutes(5); JobMissed(job, "shows \"" + (card.ButtonText ?? "").Trim() + "\" where DO JOB should be"); return; } 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); Count.Jobs += done; 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; } if (done > 0) { 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"); SaveCatalog(); ReportTarget(); nextJobs = DateTime.UtcNow.AddSeconds(done > 0 ? 2 : 30); } void DoHeists() { 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); } return; } game.HeistsTop(); CollectHeistTake(); DismissFailedHeist(); 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 (h.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; int favors; List rows; 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) { int again; using (var f = game.Capture()) again = game.ReadFavors(f, 1); if (again < 0) using (var f = game.Capture()) again = game.ReadFavors(f); if (again != favors) { int third; game.Wait(600); 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()); } if (favors == 0) { 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")); return; } if (favors < 0) { Tab? on; using (var f = game.Capture()) on = game.SelectedTab(f); if (on != null && on != Tab.Heists && !reopened) { reopened = true; Log("Heists: the " + View.TabLabels[on.Value] + " page was showing, not HEISTS - opening it again"); if (!game.OpenTab(Tab.Heists)) return; game.HeistsTop(); i--; continue; } 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); return; } 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 (best == null) { if (i == 0) { nextHeists = DateTime.UtcNow.AddMinutes(10); return; } if (lookedAgain) return; lookedAgain = true; game.Wait(1500); game.HeistsTop(); continue; } lookedAgain = false; if (Math.Min(h.Energy, energyLeft) < Math.Max(helpEnergy, best.Energy)) return; game.HelpHeist(best); string toast = game.ReadToast(900, t => Game.Refused(t) || Parse.Has(t, "HEIST") || Parse.Has(t, "HELP") || Parse.Has(t, "FAVOR")); if (Game.Refused(toast)) { Log("Heist help refused: " + toast); nextHeists = DateTime.UtcNow.AddMinutes(10); return; } 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)); favorsLeft = favors - 1; Count.Helps++; 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); } } DateTime nextHeistStart; const int HelpEnergy = 6; int helpEnergy = HelpEnergy; int heistEnergy; 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; heistEnergy = 0; S.HeistsToday = HeistDay() + "|" + HeistsPerDay; SaveSettings(); 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"); return true; } int HeistsToday() { var p = (S.HeistsToday ?? "").Split('|'); int n; 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); } void CollectHeistTake() { OcrLine button; string heist, haul; using (var f = game.Capture()) button = game.CollectTheTake(f, out heist, out haul); if (button == null) return; game.PressCollectTheTake(button); OcrLine still = null; string h2, haul2; 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); } if (still != null) { Log("Heists: COLLECT THE TAKE didn't take" + (heist.Length > 0 ? " on " + heist : "") + " - again next round"); game.Snapshot("heist take not collected", 60); return; } 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; OcrLine button; string heist, how; using (var f = game.Capture()) button = game.HeistFailedDismiss(f, out heist, out how); if (button == null) return; game.PressHeistDismiss(button); OcrLine still = null; string h2, how2; 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); } if (still != null) { 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); return; } bool free; 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"; return "other"; } void StartHeist() { nextHeistStart = DateTime.UtcNow.AddMinutes(10); if (HeistsToday() >= HeistsPerDay) { heistEnergy = 0; nextHeistStart = DateTime.UtcNow.Date.AddDays(1).AddMinutes(2); return; } OcrLine button; string own = ""; using (var f = game.Capture()) { button = game.NoActiveHeist(f); own = game.OwnHeistWords(f); } if (button != null) { game.Wait(800); using (var f = game.Capture()) { button = game.NoActiveHeist(f); own = game.OwnHeistWords(f); } } if (HeistsUsedUpSaid(own)) return; if (button == null) { 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); } heistEnergy = 0; return; } var h = ReadBarsSettled(5000, x => x.Energy >= 0 && x.Cash >= 0); if (h.Energy < 0) { heistEnergy = 0; Log("Heists: couldn't read the energy - no heist started (again in 10 minutes)"); SnapshotOnce("heist start energy unread"); return; } if (heistEnergy > 0 && h.Energy < heistEnergy && (h.EnergyMax <= 0 || heistEnergy <= h.EnergyMax)) { nextHeistStart = DateTime.UtcNow.AddMinutes(2); return; } game.PressStartAHeist(button); List offers = null; OcrLine close = null; HeistOffer pick = 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(); } if (offers == null) { string now; 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"); heistEnergy = 0; return; } Log("Heists: the START A HEIST window didn't show"); game.Snapshot("heist start window not read", 60); nextHeistStart = DateTime.UtcNow.AddMinutes(60); return; } if (pick == null) { 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); return; } if (!Game.SameHeist(pick.Name, S.StartHeist)) SayOnce("heist lower " + pick.Name, "Heists: " + S.StartHeist + " is above your level - starting " + pick.Name + " instead"); if (pick.Energy <= 0) { 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); return; } 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); return; } bool took = false; try { if (pick.Start == null || pick.Stake > 0 && (h.Cash < 0 || h.Cash < pick.Stake)) { game.CloseHeistOffers(close); took = WithdrawFor(pick.Stake, false); double cash = CashNow(); if (cash < pick.Stake) { 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); return; } if (!game.OpenTab(Tab.Heists)) return; game.HeistsTop(); using (var f = game.Capture()) button = game.NoActiveHeist(f); if (button == null) return; game.PressStartAHeist(button); pick = 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) 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(); } if (pick == null) { 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); return; } } 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; } pick = row; } 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); return; } game.PressHeistStart(pick); game.Wait(1500); List still; 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++) { game.Wait(1500); using (var f = game.Capture()) still = game.ReadHeistOffers(f, out close); } OcrLine none; using (var f = game.Capture()) none = game.NoActiveHeist(f); if (still == null && none == null) { heistEnergy = 0; S.HeistsToday = HeistDay() + "|" + (HeistsToday() + 1); SaveSettings(); if (last != null && last.Energy >= 0) last.Energy = Math.Max(0, Math.Min(last.Energy, h.Energy) - pick.Energy); Count.HeistsStarted++; Log("Heists: started " + pick); SnapshotOnce("heist started"); nextHeistStart = DateTime.UtcNow.AddMinutes(Math.Max(10, pick.Minutes > 0 ? pick.Minutes : 30)); return; } heistEnergy = 0; if (toast == null) toast = game.ReadToast(1000, t => Parse.Has(t, "HEIST")); if (HeistsUsedUp(toast)) { if (still != null) game.CloseHeistOffers(close); HeistsUsedUpSaid(toast); return; } 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 catalog) { if (S.JobMode == JobMode.OneJob) { JobInfo one; return OneJobWhy(h, catalog, out one) == null ? one.XpPerEnergy : 0; } int lv = JobLevel(h); int maxE = h != null && h.EnergyMax > 0 ? h.EnergyMax : int.MaxValue; double best = 0; foreach (var j in catalog) if (j.TierLevel <= lv && j.Cost > 0 && j.Cost <= maxE && j.Xp > 0) best = Math.Max(best, j.XpPerEnergy); return best; } int HelpCost(HeistRow r) { return r.Energy > 0 ? r.Energy : helpEnergy; } void DoFights() { 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 missed = false; bool rolledUp = false; bool refreshed = false; int moved = 0; int calledOff = 0; bool respectSaid = false; int scoutsPaid = 0; for (int i = 0; i < max; i++) { var h = ReadBarsSettled(i == 0 ? 6000 : 5000); List rows; 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; } continue; } bool scout = S.ScoutFights && !Lowering; if (BankAtMidFights(h)) { Log("Fights: the cash on hand is " + M(h.Cash) + (S.BankAt > 0 ? ", over the " + M(S.BankAt) + " to bank at" : "") + " - banking between attacks"); DoBank(); 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; } continue; } 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) { rolledUp = true; game.ScrollPage(0.5, 10); game.Wait(500); using (var f = game.Capture()) rows = game.ReadFightRows(f, out war); } SayWar(war, rows.Count); 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) { barFixTries++; 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; } break; } 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); return fought; } 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)) { refreshed = true; bool pressed; if (NewFightList(rows, out pressed) || pressed) { i--; continue; } } if (notHit.Count > 0) ready = notHit; if (ready.Count == 0) { 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"); break; } if (fought == 0) { 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"); } break; } 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 (known.Count == 0) { 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")); break; } ready = known; } 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(); if (Program.Whale) { } 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) { respectSaid = true; 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; string why; 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); return fought; } if (p == null) { 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"); continue; } 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")); continue; } if (!game.Attack(target)) { if (++moved >= 3) { 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); return fought; } Log("Fights: the list changed (no ATTACK where it was) - reading it again"); game.Wait(1000); i--; continue; } moved = 0; string result = game.FinishFight(15000); if (result == null) { bool inHospital, banner; 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 (inHospital) { if (HealWithItem() && game.OpenTab(Tab.Fight)) continue; Hospital(h, fought > 0); return fought; } 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"); continue; } if (Game.DailyLimit(why)) { DailyLimit(target); continue; } int shield = Game.ShieldSeconds(why); if (shield >= 0) { 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"); continue; } if (game.FightCalledOff) { 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); if (++calledOff >= 3) { 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); return fought; } continue; } 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); return fought; } why = why ?? game.LastFightToast; if (!missed) { missed = true; fightSkip[FightKey(target)] = DateTime.UtcNow.AddMinutes(2); game.Snapshot("attack did not start", 60, failure: false, picture: game.AfterAttack); continue; } 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); return fought; } game.ForgetAfterAttack(); fought++; missed = false; calledOff = 0; refreshed = false; hitOnList.Add(FightKey(target)); staminaLeft = h.Stamina - target.StaminaCost; Count.Fights++; 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); } return fought; } 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)"); int more = 0; 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 read = null; while (true) { 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(); game.Wait(2000); } if (e != null && e.Limit && e.Again == null) { DailyLimit(target); break; } if (e == null) { 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); break; } 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; game.AttackAgain(e); string result = game.FinishFight(15000); if (result == null) { bool inHospital; 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); } break; } more++; staminaLeft = h.Stamina - target.StaminaCost; Count.Fights++; 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(); game.OpenTab(Tab.Fight); return more; } FightMemory memory; 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) { Won(r, result, 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; var sc = ScoutedNow(r); 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; var s = ScoutedNow(r); if (s == 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); try { Directory.CreateDirectory(Accounts.Folder(dir, account)); if (!File.Exists(mine) && File.Exists(shared)) File.Copy(shared, mine); } catch (Exception) { return shared; } return mine; } 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"); return true; } 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; } bool Spared(FightRow r) { 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")); return true; } internal static string NeverAttack(string list, FightRow r) { var entries = NeverAttackEntries(list); if (entries.Count == 0) return null; int badge; string name = Parse.Key(Game.SplitPrestige(r.Name, out badge)), fam = Parse.Key(r.Family); foreach (var e in entries) { string k = Parse.Key(e); 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; return ""; } static bool FamilyHolds(string fam, string k) { if (fam == k || (k.Length >= 4 && fam.Contains(k))) return true; if (k.Length >= 6) 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; return false; } static bool RobloxNameLike(string e) { return Regex.IsMatch((e ?? "").Trim(), @"^[A-Za-z0-9_]{3,20}$"); } internal static List NeverAttackEntries(string list) { return (list ?? "").Split(new[] { ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()) .Where(x => x.Length > 0).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } bool atWar; void SayWar(bool war, int rows) { if (war == atWar || (!war && rows == 0)) return; atWar = war; 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 fightSkip = new Dictionary(); readonly HashSet hitOnList = new HashSet(); DateTime refreshBrokenUntil = DateTime.MinValue; bool HitOnList(FightRow r) { return HitBefore(hitOnList, r); } internal static IEnumerable TopDown(IEnumerable 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 hit, FightRow r) { string k = FightKey(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 before, out bool pressed) { pressed = false; if (DateTime.UtcNow < refreshBrokenUntil) return false; var was = new HashSet(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); NoNewList(); return false; } pressed = true; lastRefreshPress = DateTime.UtcNow; int start = Environment.TickCount; while (true) { List rows; bool loading; 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 (fresh > 0) { 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"); return true; } int ms = Environment.TickCount - start; if (ms >= 30000 || (!loading && ms >= 3000)) break; game.Wait(600); } 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); NoNewList(); return false; } void NoNewList() { if (Program.Whale) hitOnList.Clear(); else refreshBrokenUntil = DateTime.UtcNow.AddMinutes(2); } sealed class Scouted { public int Attack, Defense; public DateTime At; } readonly Dictionary scouted = new Dictionary(); int ownAttack = -1, ownDefense = -1; DateTime ownPowerAt = DateTime.MinValue; Scouted ScoutedNow(FightRow r) { Scouted s; return scouted.TryGetValue(FightKey(r), out s) && DateTime.UtcNow - s.At < TimeSpan.FromHours(1) ? s : null; } bool BadOdds(Scouted s) { 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 pointsSaid; string pointsStopped; string PointsStopKey() { return S.Account + "|" + S.PointsStat + "|" + S.PointsStop(S.PointsStat); } bool feeSaid; int bankUnconfirmed; bool bankHiddenSaid; int bankFee = -1; string feeLookedUp; int BankFee() { 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; feeLookedUp = name; var p = Profile.Load(Profile.FileFor(dir, name)); if (p != null && p.BankFee >= 0) bankFee = p.BankFee; return 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 cashOut; 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; double cash = CashNow(); if (cash < 0 || cash >= need) return false; double banked; using (var f = game.Capture()) banked = game.ReadHeader(f).Banked; long amount; bool all; double after; if (!WithdrawPlan(need, upTo, cash, banked, keep, out amount, out all, out after)) return false; nextBank = DateTime.UtcNow; if (amount > 0) { string why; bool seen; 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"); cashOut = true; floor = after; outOnHand = cash + amount; outRead = cash; return true; } if (typed != null) { cashOut = true; 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); } else { 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); } return false; } if (!all) return false; 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); return false; } Log("Bank: took the cash out for a purchase (" + msg.Trim() + ")"); SnapshotOnce("bank withdraw"); cashOut = true; floor = after; outOnHand = outRead = cash + banked; return true; } internal static bool WithdrawPlan(double need, double upTo, double cash, double banked, double keep, out long amount, out bool all, out double floor) { amount = 0; all = false; 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); if (amount > 0) { floor = KeepOnHand(keep, banked - amount); if (floor > 0 && FreeCash(cash, banked, keep) < need * 1.01) { amount = 0; floor = Math.Max(0, keep); return false; } return true; } amount = 0; if (cash + banked < (need + Math.Max(0, keep)) * 1.005) return false; all = true; return true; } 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 (keep <= 0) return 0; if (!CanWithdraw() || (!anyFee && BankFee() != 0)) return keep; double banked; 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; if (x < 1) x = 1; 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; return (long)x; } void DepositBack(double spent = -1) { double onHand = outOnHand, read = outRead; outOnHand = outRead = -1; if (BankFee() != 0) { nextBank = DateTime.UtcNow; return; } var bankedAt = lastBank; DoBank(); double back = game.DepositCashBefore, gone = lastBank != bankedAt ? OnHandGone(onHand, read, spent, back) : 0; if (gone > 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 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); } void DoBank() { if (AutoRollHoldsBank()) { 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); return; } int fee; 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); if (fee > 0 && !feeSaid) { Log("Bank: each deposit costs " + fee + "% without the No Bank Fees pass"); feeSaid = true; } if (msg == null) { if (game.DepositCashBefore == 0) cashOut = false; return; } if (msg == Game.BankHidden) { cashOut = false; if (!bankHiddenSaid) Log("Bank: DEPOSIT ALL pressed " + msg); bankHiddenSaid = true; Neutral(); return; } 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; return; } bankUnconfirmed = 0; cashOut = false; if (NothingToDeposit(msg)) { Log("Bank: nothing to deposit"); return; } Count.Deposits++; 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)"; Log("Bank: " + line); NoteBankFee(msg); } 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) { msg = 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; } string feeNotedFor; 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; feeNotedFor = name; string file = Profile.FileFor(dir, name); var p = Profile.Load(file); if (p == null || p.BankFee >= 0) return; p.BankFee = fee; p.BankFeeFrom = "a deposit"; p.BankLine = msg.Trim(); 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)"); var h = ProfileChanged; if (h != null) h(p); } public event Action AccountChanged; string accountSeen; int accountVotes; DateTime accountFirstVote; readonly object accountLock = new object(); bool NoteAccount(string name) { if (!Accounts.Plausible(name)) return false; name = Accounts.Known(dir, name.Trim()); lock (accountLock) { string whose = Whose(); if (!IsOther(name, whose)) { accountSeen = null; accountVotes = 0; if (S.Account.Length == 0) Adopt(name); return false; } 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; } accountVotes++; if (accountVotes < (strict ? 8 : 3) || now - accountFirstVote < TimeSpan.FromSeconds(strict ? 60 : 10)) return true; accountSeen = null; accountVotes = 0; if (strict) { 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"); return true; } } if (S.Account.Length == 0) S.Account = whose; if (!known && Accounts.Part(name, S.Account)) RenameAccount(name); else SwitchAccount(name); return true; } } bool OtherAccount(string name) { if (!Accounts.Plausible(name)) return false; lock (accountLock) return IsOther(Accounts.Known(dir, name.Trim()), Whose()); } string 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 NameAgain = null; string ReadNameAgain() { if (game == null) return null; using (var f = game.Capture()) return game.NameAgain(f); } string ownerHint; bool ownerLooked; void Adopt(string name) { string ini = Accounts.Ini(dir, name), jobs = Accounts.Jobs(dir, name); bool back = File.Exists(ini); if (back) { var o = Settings.Load(ini); S.TakeAccountFields(o); if (o.Unreadable != null) S.Unreadable = o.Unreadable; level = S.LastLevel; crewUnequipped = S.CrewGearOff; ownOpenBefore = S.CrewOpenBefore; } if (catalogBuiltIn && File.Exists(jobs)) { catalogBuiltIn = false; Catalog = GameData.LoadCatalog(jobs); CatalogView = Catalog.Select(j => j.Copy()).ToList(); var cc = CatalogChanged; if (cc != null) cc(); } S.Account = name; SaveAccount(); 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) + ")."); var h = AccountChanged; if (h != null) h(name); } void SaveAccount() { if (S.Account.Length == 0) return; try { Directory.CreateDirectory(Accounts.Folder(dir, S.Account)); S.Save(Accounts.Ini(dir, S.Account), true); SavedOwn(); 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; FoldTotals(false); SaveAccount(); 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."); } S.Account = name; lock (totalsLock) totalsFolded = Count.Copy(); catalogBuiltIn = !File.Exists(jobs); Catalog = GameData.LoadCatalog(jobs); CatalogView = Catalog.Select(j => j.Copy()).ToList(); level = S.LastLevel; ResetAccountState(before); SaveAccount(); 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."); var cc = CatalogChanged; if (cc != null) cc(); var h = AccountChanged; if (h != null) h(name); } 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); } S.Account = name; memory = new FightMemory(MemoryFile(S.Account)); SaveAccount(); 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) + ")."); var h = AccountChanged; if (h != null) h(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); ResetExpState(); ResetMenuNumbers(); 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; ResetFamily(now); LoadDefence(); lastBank = DateTime.MinValue; learnedThisRun = false; foldDue = true; last = null; 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; saidOnce.Clear(); 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; void LookForTutorial() { if (tutorialOver || game == null || (level > 8 && tutorialHint == null) || DateTime.UtcNow < nextTutorialLook) return; nextTutorialLook = DateTime.UtcNow.AddSeconds(15); Game.TutorialBox box; using (var f = game.Capture()) box = game.ReadTutorial(f); if (box == null) { if (tutorialHint != null && tutorialFought) { tutorialOver = true; Log("Tutorial: finished"); } tutorialHint = null; tutorialAction = null; tutorialItem = null; return; } bool changed = box.Hint != tutorialHint; tutorialHint = box.Hint; 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; } if (!changed) return; tutorialItem = buy.Success ? buy.Groups[1].Value.Trim() : null; tutorialAction = 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 tutorialLogged; void DoTutorial() { string what = tutorialAction; tutorialAction = null; nextTutorialLook = DateTime.UtcNow; if (what == "fight") { int n = DoFights(1, 0); tutorialFought = n > 0; if (!tutorialFought) nextTutorialLook = DateTime.UtcNow.AddMinutes(2); return; } Game.TutorialBox box; using (var f = game.Capture()) box = game.ReadTutorial(f); if (box == null) return; 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)"); } } DateTime nextProperties; double propPrice; int propLotsOpen = -1; readonly Dictionary propWontBuild = new Dictionary(); bool WontBuild(Game.PropertyCard c) { DateTime until; return c != null && propWontBuild.TryGetValue(Parse.Key(c.Name), out until) && DateTime.UtcNow < until; } void DoProperties() { nextProperties = DateTime.UtcNow.AddMinutes(2); propRecheck = false; propReplace = false; bool took = false; try { 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); return; } 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(); } int open; 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); return; } propLotsOpen = open; 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); if (plan.Target == null) { 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."); return; } 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); return; } 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; string why; bool grey; 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"); propRecheck = true; if (plan.Replace != null) propLotsOpen = Math.Max(1, propLotsOpen); nextProperties = DateTime.UtcNow; return; } 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); return; } PropertyBuilt(plan.Target, worst); if (propLotsOpen > 0) propLotsOpen--; replaceSaid = null; propRecheck = true; nextProperties = DateTime.UtcNow; } catch (Exception) { propRecheck = true; throw; } finally { if (took) DepositBack(); } } double SpendNow() { double cash = CashNow(); if (cash < 0) return -1; if (!CanWithdraw()) return cash; double banked; 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; public string Why; } internal static PropertyPlan PlanProperty(List cards, List lots, int lotsOpen, int goldLots, Func 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 (pickedGold != null) { 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(); if (cash.Count == 0) 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 covered = c => spend >= 0 && c.Price * 1.01 <= spend; Func, Func, 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() : cash.Where(c => c.Income > worstCash.Income && !SameProperty(c.Name, worstCash.Name)).ToList(); Func, 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 ownedNow = () => { if (owned == int.MinValue) owned = goldLots > 0 && gold.Count > 0 ? goldOwned() : 0; return owned; }; if (lotsOpen > 0) { if (cashDone && goldLots > 0 && gold.Count > 0) { int n = ownedNow(); 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) { int n = ownedNow(); 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 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 (x == y) return true; 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; } bool propReplace; bool propRecheck; string replaceSaid; Game.BlockLot SecondBlockRead(Game.BlockLot worst, List lots) { int open; 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); return null; } if (worst.Gold <= 0) { 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"); worst = lower; } } return worst; } bool Replace(Game.BlockLot worst, Game.PropertyCard target, ref bool took) { double buyOut; bool ready = game.BuildReady(target, out buyOut); if (!ready && buyOut > 0) { if (WithdrawFor(buyOut * 1.01, true)) took = true; ready = game.BuildReady(target); } if (!ready) { Log("Properties: didn't replace a " + worst + " - " + target.Name + "'s BUILD isn't showing gold yet"); nextProperties = DateTime.UtcNow.AddMinutes(10); return false; } string why; if (!game.DemolishLot(worst, out why)) { Log("Properties: didn't replace a " + worst + " - " + why); nextProperties = DateTime.UtcNow.AddMinutes(30); return false; } Log("Properties: demolished a " + worst + " to build " + target); propLotsOpen = Math.Max(1, propLotsOpen); return true; } void PropertyBuilt(object p, object instead) { Log("Properties: built " + p); Count.Properties++; 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; replaceSaid = what; Log("Properties: " + what); } double PropertyKeep() { return S.Properties && propLotsOpen > 0 && propPrice > 0 && !TutorialOn ? propPrice : 0; } double SlotKeep() { double keep = S.Properties && propPrice > 0 && !TutorialOn ? propPrice : 0; if (S.SafehouseUpgrade && upgradePrice > 0) keep += upgradePrice; return keep; } DateTime nextUpgrade; double upgradePrice = -1; int upgradeNeedsLevel = -1; string upgradeSaid = ""; 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)) { took = true; DoSafehouseUpgradeHere(true, floor); } if (took) DepositBack(); } string DoSafehouseUpgradeHere(bool sayWaiting, double keep) { nextUpgrade = DateTime.UtcNow.AddMinutes(10); var r = game.UpgradeSafehouse(keep); if (r.Upgraded) { var a = r.After; 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; return null; } if (r.Why == "top level") { upgradePrice = 0; nextUpgrade = DateTime.UtcNow.AddHours(24); return r.Why; } if (r.NeedLevel > 0) { upgradePrice = -1; 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."); } return r.Why; } upgradeNeedsLevel = -1; 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) + ")"); } return r.Why; } 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); upgradePrice = -1; return null; } DateTime nextCrew, nextCrewSlot; bool crewQuiet; double crewSlotPrice = -1; double[] crewHirePrice = { 10000, 10e6, 10e9 }; string crewSaid = ""; double CashNow() { for (int i = 0; i < 3; i++) { if (i > 0) game.Wait(400); double c; using (var f = game.Capture()) c = game.ReadCash(f); if (c >= 0) return c; } return -1; } double CashFor(double need) { double cash = CashNow(); if (cash >= need) return cash; game.Wait(500); double again; using (var f = game.Capture()) again = game.ReadCash(f, true); return Math.Max(cash, again); } double CashSure() { double a = CashNow(); if (a < 0) return -1; game.Wait(500); double b; 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 CrewRead; CrewInfo crewInfo; bool BuySlots { get { return S.CrewSlots && S.CrewFill; } } void DoCrew() { var info = new CrewInfo { At = DateTime.Now, SlotPrice = crewSlotPrice }; List crew = null; rerollCrew = null; try { DoCrewHere(info, out crew); } finally { ShowCrewInfo(info, crew); } } void ShowCrewInfo(CrewInfo info, List 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; crewInfo = info; var h = CrewRead; if (h != null) h(info); } DateTime slotWithdrawAfter = DateTime.MinValue; void DoCrewHere(CrewInfo info, out List crew) { crew = null; nextCrew = DateTime.UtcNow.AddMinutes(30); nextCrewSlot = DateTime.UtcNow.AddMinutes(5); Game.CrewPage page; 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(); if (BuySlots) { 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; } if (paid <= 0) { 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; } if (paid > 0) { 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) + ")"); } } } if (S.CrewFill) for (int n = 0; n < 5; n++) { Game.CrewPage p; 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]; CrewRoll used; double paid; var hire = RollNow(); 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) { Count.Hired++; Count.CrewCash += paid; changed = more = true; 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); break; } if (got == null) { 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"); } break; } Count.Hired++; Count.CrewCash += paid; changed = true; crew.Add(got); 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(); bool sorted = true; 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 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()); LoadTrained(); 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"); } int started = 0; string why = null; 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; string waiting = null; int opened = 0; try { foreach (var pick in picks) { if (busy >= Math.Min(S.TrainAtOnce, Settings.MaxTraining) || started >= 5) break; double known; if (pick.TrainLevel > 0 && trainPrice.TryGetValue(pick.TrainLevel + 1, out known) && known >= tooDear) continue; if (opened >= 4) break; Game.CrewPage p; 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; double cash = CashNow(); if (cash < 0) { why = "the cash on hand didn't read"; break; } Game.TrainWindow offer; opened++; 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); game.OpenTab(Tab.Crew); } if (offer != null && offer.Price > 0 && offer.ToLevel > 0 && offer.Names(pick.Name)) trainPrice[offer.ToLevel] = offer.Price; if (got == 1) { if (offer.ToLevel > 0) NoteTrained(pick, offer.ToLevel); busy++; started++; 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; continue; } 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); continue; } 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"); break; } } finally { 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; else if (why != 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); } } if (soonest > 0) { 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 TrainFirst(IEnumerable rows, TrainOrder order, Func 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(); } string trainSaid; string trainSkipSaid; readonly Dictionary trainPrice = new Dictionary(); readonly Dictionary trainedTo = new Dictionary(); string trainedFor; void LoadTrained() { if (trainedFor == S.Account) return; trainedFor = S.Account; trainedTo.Clear(); 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) { LoadTrained(); 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)); SaveSettings(); } string trainNoTimerSaid; bool RerollWouldReplace(Game.CrewRow r, int best) { return S.CrewReroll && r.Rarity < best; } int dismissFails; string dismissFailsOn; int findFails; int rerollHireFails; 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; } rerollHireFails++; 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"); game.Wait(wait); return rerollHireFails >= 3 ? 3 : fails; } string findFailsOn; readonly List ownStats = new List(); DateTime ownReadAt = DateTime.MinValue, gearBackHold = DateTime.MinValue; DateTime ownConfirmAt = DateTime.MinValue; bool crewUnequipped; int gearBackFails; int ownUnknownLeft = -1; bool ownJustRead; bool CrewUnequipped { get { return crewUnequipped; } set { crewUnequipped = value; if (!value) gearBackFails = 0; if (S.CrewGearOff == value) return; S.CrewGearOff = value; SaveSettings(); } } readonly object totalsLock = new object(); Counters totalsFolded = new Counters(); DateTime totalsFoldedAt = DateTime.UtcNow; public Counters AllTime { get { lock (totalsLock) { var t = Counters.Parse(S.Totals); t.Add(Count, totalsFolded); return t; } } } public void FoldTotals(bool save = true) { lock (totalsLock) { var t = Counters.Parse(S.Totals); t.Add(Count, totalsFolded); S.Totals = t.Text(); totalsFolded = Count.Copy(); totalsFoldedAt = DateTime.UtcNow; } if (save) SaveSettings(); } internal void SaveSettings() { try { 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); } } int ReadOwnStats() { ownReadAt = DateTime.UtcNow; bool pressed; string why; int read = 0; CrewUnequipped = true; bool off = game.UnequipAll(out pressed, out why); if (!pressed) CrewUnequipped = false; if (off) { Game.CrewPage page; foreach (var r in game.ReadWholeCrew(out page)) if (!r.Empty && !r.Locked && r.Attack >= 0 && r.Defense >= 0) { Game.NoteOwn(ownStats, r); read++; } if (read > 0) 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"); else { 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); } return read; } 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)); } DateTime nextReroll; List rerollCrew; DateTime rerollCrewAt; bool rerollEmpty; DateTime emptyFootSaidAt; int emptiedRarity = -1; bool emptiedByStats; double rerollCash = -1; string pausedIdle; DateTime pausedIdleAt; bool rerollShort; DateTime rerollShortAt; string rerollSaid; int ownOpenBefore = -1; int OwnOpenBefore { get { return ownOpenBefore; } set { ownOpenBefore = value; } } void KeepOwnOpenBefore() { int keep = S.CrewFill ? -1 : ownOpenBefore; if (S.CrewOpenBefore == keep) return; S.CrewOpenBefore = keep; SaveSettings(); } 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 OpenBeforeDismiss() { int open = OpenSlots(game.CrewPageSeen); return open == 0 ? 0 : OpenSlots(game.LookAtCrew()); } bool OpenedSince(int before, out string count) { count = null; if (before < 0) return false; var now = game.LookAtCrew(); if (OpenSlots(now) != before + 1) return false; count = now.Henchmen + " of " + now.Slots; return true; } bool RerollCashOk(Header h, DateTime now) { if (!rerollShort) return true; var roll = RollNow(); 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; } StartRerollOnlyGo(); } double rerollerBase; 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) { if (!RerollOnly) return; Log("Only rerolling: " + why + " - the bot stops"); RerollOnly = false; stop = true; stopWhy = (finished ? "Only rerolling is over: " : "Only rerolling gave up: ") + why; } public volatile bool FightOnly; public void StartFightOnly() { RerollOnly = false; onlyFightFails = 0; onlyFightProgressAt = DateTime.UtcNow; nextFights = DateTime.UtcNow; FightOnly = true; if (!Running) Begin(false); } public void StopFightOnly() { if (!FightOnly) return; FightOnly = false; stop = true; } void EndFightOnly(string why) { if (!FightOnly) return; Log("Only fighting: " + why + " - the bot stops"); FightOnly = false; stop = true; stopWhy = "Only fighting gave up: " + why; } int onlyFightFails; 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)); if (noStamina) { Log("Only fighting: waiting for stamina (" + h.Stamina + (h.StaminaMax > 0 ? "/" + h.StaminaMax : "") + ")"); if (nextFights < now.AddMinutes(1)) nextFights = now.AddMinutes(1); return; } onlyFightFails++; if (onlyFightFails >= 3 || now - onlyFightProgressAt > TimeSpan.FromHours(1)) { EndFightOnly("nothing left worth attacking (after " + onlyFightFails + " tries)"); return; } 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 crew = null; try { RerollsHere(info, only, out crew); } finally { KeepOwnOpenBefore(); ShowCrewInfo(info, crew); } } List ReadCrewForRerolls(CrewInfo info, int step = 4) { Game.CrewPage page; 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 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); return rows; } List ReadAllHenchmen(CrewInfo info, out string why) { why = null; 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; List rows; 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); if (merged != null) { 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"); DropPages(pages); ListReadWhole(); return merged; } } 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); game.NoteFailure(why); return null; } static void DropPages(string dir) { if (dir != null) try { if (Directory.Exists(dir)) Directory.Delete(dir, true); } catch (Exception) { } } int listShortFails; DateTime listShortSince; int hiredRead = -1; DateTime hiredReadAt = DateTime.MinValue, slotBoughtAt = DateTime.MinValue; bool SlotStandsEmpty(out string count) { count = null; Func 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; return true; } void ListReadWhole() { if (listShortFails > 0) Log("Crew: the whole list reads right again (it didn't from " + listShortSince.ToLocalTime().ToString("HH:mm") + ")"); listShortFails = 0; } void RerollsHere(CrewInfo info, bool only, out List crew) { crew = null; var start = DateTime.UtcNow; nextReroll = start.AddMinutes(1); bool shortBefore = rerollShort; rerollShort = false; rerollCash = -1; var roll = RollNow(); int best = Game.BestRoll(roll); double keep = only ? 0 : PropertyKeep(); bool fromCache = false; if (rerollCrew != null && autoRun == null && start - rerollCrewAt < TimeSpan.FromMinutes(10) && game.OpenTab(Tab.Crew)) { crew = rerollCrew; fromCache = true; if (crewInfo != null) { info.Henchmen = crewInfo.Henchmen; info.Slots = crewInfo.Slots; info.Attack = crewInfo.Attack; info.Defense = crewInfo.Defense; } } else crew = ReadCrewForRerolls(info); rerollCrew = null; if (crew == null) { if (only) OnlyGo(false, "couldn't read the crew page", 0, keep); return; } if (rerollEmpty && !S.CrewFill && OwnOpenBefore == -1) rerollEmpty = false; string rollWait; bool rollEnds = AutoRollHere(info, keep, fromCache, ref crew, out rollWait); NoteRun(); if (rollEnds) { info.Status = rollWait + "."; if (only) OnlyGo(false, rollWait, 0, keep); return; } double keepAll = keep; if (AutoRollMay(false, -1, best) && crew.Any(r => r.Rarity >= 0 && r.Rarity < best && Dismissable(r))) { double rollFloor; RollCash(roll, keep, autoRun != null ? autoRun.SpentBefore + Math.Max(0, autoRun.Spent) : 0, out rollFloor); keep = rollFloor; } int rolls = 0, fails = 0, readAt = Environment.TickCount - 60000, badgesAt = Environment.TickCount; int emptyHolds = 0; double cashEst = -1; double spent = 0; Game.CrewRow lastHired = null; var better = new List(); string stop = null, pausedFor = null, idleFor = null; int idleUntil = 0; bool done = false, readAgain = false, reread = false, statsWait = false, statsSoon = false, listShort = false, skipWait = false, awayWait = false; bool counted = false; bool finished = false; var clock = start; rerollsYield = false; try { while (true) { if (!only && S.Events && EventOn(GameEvent.RecruitLuck) && Environment.TickCount - badgesAt > 30000) { badgesAt = Environment.TickCount; BadgesInRerolls(); } 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; } else if (!only) { OtherTaskDue(ref readAt, true); var now = DateTime.UtcNow; string full = null; 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; } if (rolls >= 500) break; if (fails >= 3) break; if (RollNow() != roll) { 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"; break; } if (readAgain) { readAgain = false; var again = ReadCrewForRerolls(info); if (again == null) { stop = "couldn't read the crew page"; fails = 3; break; } crew = again; counted = false; } 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)) { hold = 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) { double floor; 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; } } } rerollCash = cash; 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)"; rerollShort = true; rerollShortAt = DateTime.UtcNow; nextReroll = DateTime.UtcNow.AddSeconds(ShortWaitSeconds(shortBefore, spent)); break; } Game.CrewPage p; CrewRoll used; double paid; Game.CrewRow got; if (rerollEmpty) { 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) { game.Wait(900); empty = game.FindCrewRow(r => r.Empty, out p, true); } if (empty == null) { 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; if (!only) break; continue; } 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"); continue; } if (got == null) { if (paid > 0) readAgain = true; fails = HireNoRecruit(roll, paid, fails, out stop); if (!only) break; continue; } rerollEmpty = false; OwnOpenBefore = -1; fails = 0; rerollHireFails = 0; Count.Hired++; Count.CrewCash += paid; spent += paid; counted = false; 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; got.Recruit = true; crew.Add(got); Game.NoteOwn(ownStats, got); Log("Crew: hired " + got + " into the empty slot with a " + used + " hire (" + M(paid) + ")"); continue; } 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; } if (Skipped(worst)) { var next = order.FirstOrDefault(r => r.Rarity == worst.Rarity && !Skipped(r)); if (next == null) { 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"; skipWait = true; break; } worst = next; } 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"; awayWait = true; break; } bool byStats = false; 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)) { ReadOwnStats(); ownJustRead = true; readAgain = true; if (crewUnequipped) { stop = "the crew's gear isn't back on yet - that goes first"; break; } continue; } 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"); ReadOwnStats(); ownJustRead = true; readAgain = true; if (crewUnequipped) { stop = "the crew's gear isn't back on yet - that goes first"; break; } continue; } if (weak == null) { 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; } break; } bool sorted = false; 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; } worst = weak; weakOwn = Game.OwnOf(weak, ownStats); byStats = true; 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) { string why; var all = ReadAllHenchmen(info, out why); if (all == null) { stop = "no Mythic or better dismissed: " + why; listShort = true; break; } crew = all; counted = true; lastHired = null; continue; } var row = !byStats && ReferenceEquals(worst, lastHired) ? worst : game.FindCrewRow(r => Game.NamedLike(r, worst), out p, true); if (row == null) { 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); game.NoteFailure(stop); fails++; readAgain = true; findFails = findFailsOn == worst.Key ? findFails + 1 : 1; findFailsOn = worst.Key; if (findFails >= 3) { 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; continue; } if (!only) break; continue; } findFails = 0; if (!Dismissable(row)) { worst.Training |= row.Training; worst.Stationed |= row.Stationed; worst.Recovering |= row.Recovering; continue; } Game.CrewRow emptied; bool sure = (worst.Rarity < best && crew.All(r => r.Rarity >= worst.Rarity)) || (byStats && weakOwn != null && worst.Rarity == best && crew.All(r => r.Rarity >= best)); string open; int openBefore = S.CrewFill ? -1 : OpenBeforeDismiss(); if (!S.CrewFill) { 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"); rerollEmpty = true; continue; } if (!game.DismissHenchman(row, worst, sure, out emptied)) { game.Wait(1200); var late = game.EmptyRowNear(row.Anchor.CenterY); emptied = late != null && late.Anchor.CenterY >= row.Top && late.Anchor.CenterY <= row.Bottom ? late : null; string nowOpen; if (emptied == null && (S.CrewFill ? SlotStandsEmpty(out nowOpen) : OpenedSince(openBefore, out nowOpen))) { rerollEmpty = true; OwnOpenBefore = S.CrewFill ? -2 : openBefore; counted = false; if (game.FindCrewRow(r => Game.NamedLike(r, worst), out p) == null) { dismissFails = 0; crew.Remove(worst); 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"); continue; } if (emptied == null) { var again = game.FindCrewRow(r => Game.NamedLike(r, worst), out p); if (again == null || !game.DismissHenchman(again, worst, sure, out emptied)) { string lateOpen; if (Program.Reroller && S.CrewFill) { game.Wait(1000); } if (Program.Reroller && S.CrewFill && SlotStandsEmpty(out lateOpen)) { dismissFails = 0; crew.Remove(worst); 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"); continue; } stop = "DISMISS didn't take on " + worst; game.Snapshot("crew dismiss failed", 60); game.NoteFailure(stop); dismissFails = dismissFailsOn == worst.Key ? dismissFails + 1 : 1; dismissFailsOn = worst.Key; fails++; if (dismissFails >= 3) { 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; continue; } if (!only) break; continue; } } } dismissFails = 0; crew.Remove(worst); if (byStats) { ownStats.Remove(weakOwn); Log("Crew: dismissed " + worst.Name + " (" + worst.RarityName + ", own " + weakOwn + ", under " + Settings.PerfectFor(worst.Rarity) + ") to roll a better one"); } rerollEmpty = true; 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"; break; } if (!game.CrewClear()) { 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); fails = 3; break; } 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"); continue; } if (got == null) { if (paid > 0) { rerollEmpty = false; readAgain = true; emptiedRarity = -1; } fails = HireNoRecruit(roll, paid, fails, out stop); if (!only) break; continue; } 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; got.Recruit = true; crew.Add(got); Game.NoteOwn(ownStats, got); lastHired = 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"); } } finally { 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; else if (stop != null) { 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); if (listShort) { 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 CrewRoll Tier; public int Target; public string Replaced = ""; public int ReplacedRarity = -1; public List Before; public bool Running; public DateTime StartedAt; public double Spent = -1, SpentBefore; public int Hires = -1, HiresBefore; public string Now = ""; public int Starts; public bool Restored; public int Misses; } AutoRun autoRun; bool autoRollOff; int autoRollGrey; 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; int rollStopTries; 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); } void DoRollStop() { var run = autoRun; if (run == null || !run.Running) return; string why; bool done; 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"); EndRun(run); return; } 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) { why = null; var run = autoRun; 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; } string told; var row = FindOwnRoll(run, out told); if (row == null) { if (told == "over") { Log("Auto Roll: " + run.Replaced + "'s slot isn't rolling any more - " + reason); EndRun(run); return true; } 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); return false; } 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" : "")); EndRun(run); return true; } 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) { told = null; Func mine = r => RunMayBe(r.RollFor, r.RollTierRead, r.RollTier, r.RollHires, r.RollSpent, run); Game.CrewPage page; var any = game.FindRollingRow(out page); if (any == null) { if (page != null && page.Page && page.TopInSight && !page.AutoRolls) told = "over"; return null; } if (mine(any)) return any; var row = game.FindRollingRow(out page, mine); if (row == null && page != null && page.Page && page.TopInSight) told = "yours"; return row; } void EndRun(AutoRun run) { Count.CrewCash += run.SpentBefore + Math.Max(0, run.Spent); if (autoRun == run) autoRun = null; rollStopTries = 0; rollStopNext = DateTime.MinValue; NoteRun(); } void LeaveRun(AutoRun run, string why) { Log("Auto Roll: " + why + " - left to you"); if (autoRun == run) autoRun = null; rollStopTries = 0; rollStopNext = DateTime.MinValue; NoteRun(); } bool Finishing { get { return finishing && !stopAgain && finishEnd - Environment.TickCount > 0; } } void StopRollAtEnd() { var run = autoRun; if (run == null || !run.Running) return; string why = null; bool done = false, turn = false; var t = turns; if (!stopAgain) { finishEnd = Environment.TickCount + RollStopSeconds * 1000; finishing = true; State("Stopping: the game's Auto Roll first"); try { 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"; else { acting = true; try { 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; } finally { finishing = false; if (turn) t.Pass(this); } } 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"); } void NoteRun() { var r = autoRun; var now = DateTime.UtcNow; string want = r != null && r.Running ? RunText(r, now) : ""; string had = S.AutoRollMine ?? ""; if (want == had) return; DateTime seen; if (want.Length > 0 && had.Length > 0 && RunBody(want) == RunBody(had) && RunSeen(had, out seen) && now - seen < TimeSpan.FromMinutes(10)) return; S.AutoRollMine = want; SaveSettings(); } void RestoreRun() { var r = RunFrom(S.AutoRollMine, DateTime.UtcNow); if (r == null) { if (!string.IsNullOrEmpty(S.AutoRollMine)) { S.AutoRollMine = ""; SaveSettings(); } return; } autoRun = r; 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; DateTime seen; 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 crew) { if (emptied == null) return false; if (emptied.Auto == null) { game.Wait(600); emptied = game.EmptyRowNear(emptied.Anchor.CenterY) ?? emptied; } bool started = game.StartAutoRoll(emptied, null, roll, best); autoRollNext = DateTime.UtcNow.AddMinutes(AutoRollMinutes); if (!started) { autoRun = null; if (game.AutoRollNoPass) { if (++autoRollGrey >= 3) { autoRollOff = true; Log("Auto Roll: " + game.AutoRollWhy + " - three looks in a row, so off until the bot is started again; the bot hires itself as before"); } else { autoRollNext = DateTime.UtcNow.AddMinutes(10); Log("Auto Roll: " + game.AutoRollWhy + " (look " + autoRollGrey + " of 3) - looking again in 10 minutes; the bot hires itself meanwhile"); } } else { autoRollGrey = 0; 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)"); } NoteRun(); return false; } autoRollGrey = 0; 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.Running = true; run.Restored = false; run.Misses = 0; run.StartedAt = DateTime.UtcNow; run.Starts++; autoRun = run; 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"); NoteRun(); SnapshotOnce("crew auto roll started"); return true; } bool AutoRollHere(CrewInfo info, double keep, bool fromCache, ref List crew, out string wait) { wait = null; bool rolls; Game.CrewRow rolling = null; if (fromCache) { 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)) { case "yours": 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); return true; case "watch": { Game.CrewPage page; var run = autoRun; if (run.Restored) { string told; rolling = FindOwnRoll(run, out told); if (rolling == null) { 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); return true; } 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); return true; } 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); if (rolling == null) { 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); return true; } 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; if (stopWhy != 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" : "")); Count.CrewCash += total; 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; } autoRun = null; autoRollRest = now.AddHours(1); nextReroll = autoRollRest; wait = "the game's Auto Roll stopped at its limit - the next in an hour"; return true; } 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 : ""); return true; } case "ended": { if (fromCache) { crew = ReadCrewForRerolls(info); if (crew == null) { wait = "couldn't read the crew page"; return true; } } int fresh; 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); autoRun.Running = false; if (got == null) { 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"); Count.CrewCash += total; autoRun = null; autoRollNext = DateTime.MinValue; return false; } 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); autoRun = null; autoRollNext = DateTime.MinValue; return false; } 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"); Count.CrewCash += total; autoRun = null; autoRollRest = now.AddHours(1); nextReroll = autoRollRest; wait = "the game's Auto Roll spent its limit on that slot - the next in an hour"; return true; } 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"))); if (now < autoRollNext) { nextReroll = autoRollNext; wait = "the game's Auto Roll starts again at " + autoRollNext.ToLocalTime().ToString("HH:mm"); return true; } return false; } } return false; } 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 before, List now, out int fresh) { fresh = 0; if (before == null || now == null) return null; var left = before.ToList(); var news = new List(); foreach (var r in now) { 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); else news.Add(r); } fresh = news.Count; 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; double cash = CashNow(); if (cash < 0) return false; double banked; 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 why; bool seen; 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); return true; } 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") + ")"); return false; } bool RerollsLead() { return S.RerollsFirst || (S.Events && EventOn(GameEvent.RecruitLuck)); } CrewRoll RollNow() { 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; if (RerollOnly) { 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; bool rerollsYield; internal static string FullBarBreak(List 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(); if (bank) tasks.Add("bank"); if (shopDue) tasks.Add("shop"); return string.Join(", ", tasks); } void RerollsFirstDone(string why) { if (!S.RerollsFirst) return; S.RerollsFirst = false; SaveSettings(); Log("Crew: " + why + " - the rerolls went back from First to On"); } readonly List> rerollSkip = new List>(); 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 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)); } int onlyFails; DateTime onlyProgressAt; 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 (rerollShort) { game.Wait(1500); double cash = CashNow(); if (cash < 0 || cash >= crewHirePrice[(int)RollNow()] + keep) { nextReroll = now; return; } EndRerollOnly(why, true); return; } onlyFails++; 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) { game.Wait(400); 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); return got; } 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); NoteTicks(fresh); NoteHealth(fresh.Health, fresh.HealthMax, DateTime.UtcNow); read = true; if (last == null) last = fresh; else { 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) { done = false; sortWasStuck = false; int moves = 0, noArrow = 0; var movedUp = new List(); bool autoTried = ByOwnStats || DateTime.UtcNow < autoSortOffUntil, restart = false, autoPressed = false; try { for (int pass = 0; pass < 6 && moves < maxMoves; pass++) { int before = moves; string lastSig = null; Game.CrewRow lastMover = null; bool top = false; restart = false; game.CrewListEnd(-1); for (int look = 0; look < 120; look++) { Game.CrewPage p; 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(); int missing; var mover = Game.NextToMoveUp(inSight, out missing, ByOwnStats ? ownStats : null, SortSkipped); noArrow += missing; if (mover != null && !autoTried) { autoTried = true; string why; 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 (mover != null) { 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))); sortWasStuck = true; 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"); lastMover = null; continue; } game.CrewMoveUp(mover); string who = (mover.Name.Length > 0 ? mover.Name : "a henchman") + " (" + mover.RarityName + ")"; if (!movedUp.Contains(who)) movedUp.Add(who); lastMover = mover; moves++; continue; } string sig = string.Join(",", p.Rows.Select(r => r.Empty ? "empty" : r.Locked ? "locked" : r.Key)); if (sig == lastSig) { top = true; break; } lastSig = sig; game.ScrollPage(0.62, game.ListNotches(3)); } if (restart) continue; if (!top) break; if (moves == before) { done = true; break; } } } finally { 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"); } if (moves > 0) 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"); } return moves; } bool ByOwnStats { get { return S.CrewReroll && S.CrewRerollStats > 0; } } readonly List> sortStuck = new List>(); DateTime autoSortOffUntil; bool sortWasStuck; int sortStuckRounds; 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 eventsOn = new Dictionary(); string briefcasesSaid; void NoteBadges(Frame f) { string sig = Game.BadgeSignature(Game.FindBadges(f)); noBadges = sig.Length == 0 ? noBadges + 1 : 0; if (sig.Length == 0 && noBadges < 3) return; badgeSig = sig; if (badgeSig.Length == 0 && eventsOn.Count > 0) { eventsOn.Clear(); eventsSig = ""; Log("Events: none running now"); } } int noBadges; bool EventOn(GameEvent e) { DateTime until; return eventsOn.TryGetValue(e, out until) && DateTime.UtcNow < until; } void BadgesInRerolls() { List badges; using (var f = game.Capture()) badges = Game.FindBadges(f); if (badges.Count == 0) { game.Wait(1000); using (var f = game.Capture()) badges = Game.FindBadges(f); } string sig = Game.BadgeSignature(badges); if (sig == eventsSig) return; badgeSig = sig; 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"); } bool BossDamageHold() { DateTime until; 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; DateTime at; if (!DateTime.TryParse(S.CratesOpenedAt, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out at)) return false; return now < at.AddHours(24); } void DoEvents() { var now = DateTime.UtcNow; nextEvents = now.AddMinutes(30); List 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(eventsOn.Where(kv => kv.Value > now).Select(kv => kv.Key)); eventsOn.Clear(); foreach (var e in read.Where(e => e.Kind != GameEvent.Other)) { var until = now.AddSeconds(e.SecondsLeft > 0 ? e.SecondsLeft : UnreadEventSeconds); DateTime had; 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; bool expHeld; DateTime opsReadyAt = DateTime.MinValue; bool opsKnown; bool opsWaiting; bool bossesNow; readonly Dictionary expSaid = new Dictionary(); int expMissing; int expClaimFails; int expUnread; DateTime expClaimAgain; 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) { DateTime t; return DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out t) ? t : DateTime.MinValue; } void ExpSay(string key, string line) { string was; if (expSaid.TryGetValue(key, out was) && was == line) return; expSaid[key] = line; Log(line); } string ExpWhen(DateTime now) { DateTime at; 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)"; } void ResetExpState() { expHeld = opsKnown = opsWaiting = bossesNow = false; opsReadyAt = DateTime.MinValue; expSaid.Clear(); 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.ExpDay = UtcDay(now); S.ExpCardAt = ""; S.ExpUsed = S.ExpMostWantedDone = false; expHeld = opsWaiting = false; expSaid.Clear(); expMissing = expClaimFails = expUnread = 0; if (had) nextPlaytime = now; SaveSettings(); } bool ExpPending(DateTime now) { ExpNewDay(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) { at = DateTime.MinValue; 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; at = opsReadyAt; return ExpRoute.Operations; } at = card < now ? now : card; return ExpRoute.Bosses; } 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; SaveSettings(); 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) { why = null; 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; } } return true; } 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; DateTime at; 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; DateTime at; 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; DateTime at; 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; string why; 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"); string claimWas; if (!expSaid.TryGetValue("claim", out claimWas) || !claimWas.StartsWith(claimWait)) ExpSay("claim", claimWait + " (" + why.Split(' ')[0] + " now " + why.Split(' ')[1] + ")"); return false; } void NoteExpCard(DateTime now) { ExpNewDay(now); int s = game.PlaytimeExpSeconds; string card = S.ExpCardAt; bool used = S.ExpUsed; 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; SaveSettings(); 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"); return; } 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; if (got > 0) { expHeld = opsWaiting = false; playtimeClaimed = now; Count.Rewards += got; S.ExpUsed = true; S.ExpUntil = UtcText(now.AddMinutes(ExpMinutes)); SaveSettings(); nextJobs = now; int hp = HealthNow(now); 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"); } else { NoteExpCard(now); if (!S.ExpUsed && game.PlaytimeExpSeconds <= 0 && ++expClaimFails >= 3) { S.ExpUsed = true; SaveSettings(); 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); } } game.OpenTab(back); return got > 0; } void BossesAfter(List due, int i) { if (!bossesNow) return; bossesNow = false; 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) { switch (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" : ""; default: return ""; } } void DoBriefcases() { 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"); briefcasesSaid = "off"; } return; } 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(); } } void PickBriefcases() { 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); break; } 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)")); break; } 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); break; } 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); return; } if (paid == null) { nextBriefcases = DateTime.UtcNow.AddMinutes(10); break; } Count.Rewards++; Log("Lucky Briefcases: a free pick paid " + (paid.Length > 0 ? CultureInfo.InvariantCulture.TextInfo.ToTitleCase(paid.ToLowerInvariant()) : "something (its words didn't read)")); } } bool briefcasesStopped; 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() { Game.BriefcasePage a, b; using (var f = game.Capture()) a = game.ReadBriefcases(f); if (!a.Page) return a; game.Wait(1000); using (var f = game.Capture()) b = game.ReadBriefcases(f); if (!b.Page) return b; return a.ButtonFree == b.ButtonFree ? b : null; } DateTime nextGlobalBoss; string globalBossSaid; DateTime globalBossPinged = DateTime.MinValue; bool globalBossUp; bool globalBossWaits; int globalBossFails; int globalBossHits; 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; return globalBossUp; } 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"; } void DoGlobalBoss() { 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"); globalBossSaid = "off"; } return; } try { ReadGlobalBoss(now); } finally { if (!globalBossUp || ExpLastCall(DateTime.UtcNow)) game.FoldEvents(); } } void GlobalBossFailed(string why) { globalBossFails++; 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); globalBossUp = false; globalBossFails = 0; nextGlobalBoss = DateTime.UtcNow.AddMinutes(GlobalBossMinutes); } void ReadGlobalBoss(DateTime now) { Game.GlobalBossPage a, b; using (var f = game.Capture()) a = game.ReadGlobalBoss(f); game.Wait(1000); 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"); return; } string who = b.Boss != null ? CultureInfo.InvariantCulture.TextInfo.ToTitleCase(b.Boss.ToLowerInvariant()) : null; if (b.State != Game.BossState.Live && globalBossUp) { globalBossUp = false; Log("Global Boss: it's over" + (globalBossHits > 0 ? " - " + globalBossHits + " hits on it" : "") + " - the rest plays again"); } if (b.State != Game.BossState.Live) globalBossHits = 0; switch (b.State) { 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); break; 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"); break; case Game.BossState.Live: if (!globalBossUp) { globalBossUp = true; globalBossWaits = false; globalBossHits = 0; 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"); } globalBossSaid = "live"; if (now >= globalBossPinged.AddHours(12)) { globalBossPinged = now; 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); } HitGlobalBoss(b); break; } } void ClaimGlobalBoss(Game.GlobalBossPage p, DateTime now) { if (now < globalBossClaimAfter) return; SnapshotOnce("global boss claim drops"); var after = game.ClaimGlobalBossDrops(p); if (after == null) { if (++globalBossClaimFails < 3) { Log("Global Boss: pressed CLAIM DROPS, the button stayed - tried again at the next look"); return; } 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); return; } 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) { int hits; string stop = GlobalBossHits(p, out hits); globalBossHits += 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"; switch (stop) { case "stamina": SayOnce("global boss stamina " + now.ToString("yyyyMMddHH"), "Global Boss: no stamina left - the rest plays until a point has refilled"); nextGlobalBoss = now.AddSeconds(20); break; case "health": SayOnce("global boss health " + now.ToString("yyyyMMddHH"), "Global Boss: health is low - the rest plays until it has refilled"); nextGlobalBoss = now.AddMinutes(2); break; 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"; Header before; 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"); return true; } } bool none; string used; try { used = game.UseConsumable(stamina, out none); } finally { game.FoldEvents(); } if (used == null) { 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"); return false; } 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")); if (!worked) { game.Snapshot("consumable used no change", 60); if (stamina) consumablesNoneUntil0 = now.AddMinutes(30); else consumablesNoneUntil1 = now.AddMinutes(30); return false; } return true; } string GlobalBossHits(Game.GlobalBossPage p, out int hits) { hits = 0; 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 (p.Attack == null) { if (looks++ >= 3) { hits += presses; return "no attack"; } game.Wait(700); using (var f = game.Capture()) p = game.ReadGlobalBoss(f); if (!p.Page || p.State != Game.BossState.Live) { hits += presses; return "over"; } continue; } looks = 0; game.GlobalBossAttack(p); presses++; stamina--; game.Wait(500); bool refused = false; 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) { game.Wait(800); 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 (spent <= 0) { 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; presses = 0; } } hits += presses; return null; } DateTime nextEquip, nextShop, nextSafehouse; bool saidBestGear, saidDeposit; public event Action SafehouseRead; public PlayerStats LastGameStats; public event Action GameStatsRead; void DoGear() { 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); bool done; 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(); } Count.Crates += opened; if (done) S.CratesOpenedAt = now.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); } if (bought > 0 || opened > 0 || gearDue) EquipThenDeposit(); } string shopGoldSaid; string shopRaritySaid; string shopLimitSaid; string shopKeepSaid; 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; shopKeepSaid = say; return line; } int BuyFromShop() { int stock; double keepAll = ShopKeep(true), keep = KeepFloor(keepAll, true); string keepLine = ShopKeepLine(keep); if (keepLine != null) Log(keepLine); var dear = new List(); var goldLeft = new HashSet(); var overLimit = new HashSet(); Func want = i => { if (i.Gold) { if (i.Price <= S.ShopGoldMax) return true; goldLeft.Add(i.ToString()); return false; } 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; } return true; }; 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(); 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); try { int again; if (took) { var more = game.BuyFromShop(out again, floor, null, null, cheapest * 0.99, i => want(i) && S.ShopBankFor(i.Rarity)); bought.AddRange(more); 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); shopGoldSaid = 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 rarity = r => Game.Rarities[r].Substring(0, 1) + Game.Rarities[r].Substring(1).ToLowerInvariant(); if (good.Count > 0) 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); return bought.Count; } void EquipThenDeposit() { 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"); S.MarketPending = ""; SaveSettings(); } if (S.MarketPending.Length > 0) { Log("Collection: no deposit while " + S.MarketPending.Split('|')[0] + " waits to go back up on the Black Market"); return; } DepositCollection(); } bool DoEquip() { nextEquip = DateTime.UtcNow.AddHours(6); rerollCrew = null; var r = game.EquipBest(); if (r == null) return false; var a = r.Before; 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; } if (!r.GearOn) { 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); return false; } CrewUnequipped = false; 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 + ")"); saidBestGear = true; SnapshotOnce("crew auto equip best"); return true; } void DepositCollection() { var r = game.DepositCollection(); if (r == null) return; var a = r.Before; 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 || stronger) { 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 + "\"" : "")); saidDeposit = true; SnapshotOnce("collection deposit"); } void ReadSafehouse() { 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); return; } 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; SaveSettings(); } if (info.Prestige >= 0 && info.Prestige != S.LastPrestige) { S.LastPrestige = info.Prestige; SaveSettings(); 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"); var h = SafehouseRead; if (h != null) h(info); } void DoGameStats() { 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); return; } LastGameStats = st; 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)"); level = st.Level; S.LastLevel = level; ReportTarget(); } 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") + ")"); level = st.Level; S.LastLevel = level; game.MenuChanged(); PropertiesLevelUp(); ReportTarget(); } var g = GameStatsRead; if (g != null) g(st); } void DoOperations() { nextOps = DateTime.UtcNow.AddMinutes(5); if (!game.OpenTab(Tab.Operations)) { Visited(Tab.Operations, null, true); return; } int collectedBefore = Count.OpsCollected, startedBefore = Count.OpsStarted; List slots; using (var f = game.Capture()) slots = game.ReadOpSlots(f); var now = DateTime.UtcNow; bool hold = false; var holdUntil = now; 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) { hold = true; holdUntil = cardAt; } else if (slots.Any(x => x.Collect != null) && ExpPending(now) && ExpCardReady(now) && GlobalBossHoldsExp(now)) { hold = true; 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)) { string why; if (ExpBarsReady(now, last, out why) || ExpLastCall(now)) { if (ClaimHeldExp(Tab.Operations, "the finished operations, then the bosses")) { bossesNow = true; using (var f = game.Capture()) slots = game.ReadOpSlots(f); } else if (game.PlaytimeExpSeconds > 0 && game.PlaytimeExpSeconds <= 300 && ExpPending(now)) { hold = true; holdUntil = UtcParse(S.ExpCardAt); } } else { hold = true; 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); } opsWaiting = hold; for (int i = 0; i < 4 && !hold; i++) { var s = slots.FirstOrDefault(x => x.Collect != null); if (s == null) break; game.CollectOp(s); Count.OpsCollected++; 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"); if (free > 0) { StartOperations(free); game.OpsTop(); } using (var f = game.Capture()) slots = game.ReadOpSlots(f); var ops = OpsRead; 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(); int soonest = -1; opsReadyAt = DateTime.MinValue; if (left.Count > 0) { soonest = left.Min(); if (S.Events && EventOn(GameEvent.OpsSpeed)) soonest /= 2; opsReadyAt = DateTime.UtcNow.AddSeconds(soonest); nextOps = DateTime.UtcNow.AddSeconds(Math.Min(soonest + 8, 30 * 60)); } opsKnown = true; if (hold) { nextOps = holdUntil; Visited(Tab.Operations, false, true, holdUntil.AddSeconds(90)); return; } Visited(Tab.Operations, Count.OpsCollected > collectedBefore || Count.OpsStarted > startedBefore, !allRunning, soonest >= 0 ? DateTime.UtcNow.AddSeconds(soonest + 90) : DateTime.MinValue); } bool opsNoneSaid; void StartOperations(int free) { game.OpsTop(); var all = new List(); string lastSig = null; int step = 1; for (int page = 0; page < 30; page++) { List rows; using (var f = game.Capture()) rows = game.ReadOpRows(f, page); if (page == 0) step = Game.OpsStep(rows); foreach (var r in 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; lastSig = sig; game.OpsDown(step); } 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 (pick == null) { 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"); opsNoneSaid = true; if (only.Length == 0) SnapshotOnce("no operation to start"); nextOps = DateTime.UtcNow.AddMinutes(15); return; } opsNoneSaid = false; game.OpsTop(); for (int i = 0; i < pick.Page; i++) game.OpsDown(step); for (int n = 0; n < free; n++) { OpRow row; 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; game.StartOp(row); Count.OpsStarted++; Log("Started operation \"" + row.Name + "\""); } } DateTime BossesDoneUntil(DateTime now) { DateTime until; return DateTime.TryParse(S.BossesDoneUntil, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out until) && until > now ? until : now; } int stagesLocked = -1; bool stagesWalked; void BossesLevelUp(int newLevel) { var now = DateTime.UtcNow; if (newLevel > 0 && newLevel < S.BossesOpenLevel && BossesDoneUntil(now) > now) return; nextBosses = now; S.BossesDoneUntil = ""; S.BossesOpenLevel = 0; } void BossesDone(DateTime until) { nextBosses = until; S.BossesDoneUntil = until.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture); S.BossesOpenLevel = stagesLocked > level ? stagesLocked : 0; SaveSettings(); if (S.BossesOpenLevel > 0) SayOnce("bosses open " + S.BossesOpenLevel, "Bosses: nothing to fight now - the next stage opens at level " + S.BossesOpenLevel); } void DoBosses() { 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 softSeeks = 0; DateTime claimAt; 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); return; } if (!game.OpenTab(Tab.Bosses)) return; var st = ReadStage(); 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); return; } 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(); string stageBoss = st.Boss, stop = null; var done = new List(); 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); break; } if (st == null || st.Stage < 0) { if (waits == 8 && !reopened) { reopened = true; 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; } game.Wait(500); st = ReadStage(); continue; } if (st.Stage != stageAt) { 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.Attack == null) { if (st.Cooldown || ((st.DefeatedToday || st.Cleared) && waits < 10)) { waits++; game.Wait(500); st = ReadStage(); continue; } if (!sought) { sought = true; seeks++; if (mwOnly ? soften && SeekSoft(ref st) : SeekReady(ref st, Math.Max(lowest, lastBeaten > 0 ? lastBeaten + 1 : 1))) { waits = 0; continue; } noneReady = true; 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; } noneReady = true; if (st == null || st.Stage < 0) continue; } if ((st.Cleared || st.DefeatedToday) && nexts < 2 && !stagesWalked) { nexts++; for (int roll = 0; roll < 6 && st != null && st.Next == null && !st.ListAt.IsEmpty; roll++) { string sig = st.ListSig; game.StagesListScroll(st, -3); game.Wait(500); st = ReadStage(); if (ListStill(sig, st)) break; } if (st != null && st.Next != null) { game.StageNext(st); waits = 0; st = ReadStage(); continue; } if (st == null) continue; } else if (!st.Cleared && !st.DefeatedToday && st.Next != null && nexts < 2) { nexts++; game.StageNext(st); waits = 0; st = ReadStage(); continue; } 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"; break; } waits = 0; calm = 0; if (sinceBars >= 5 || stamina <= 1) { bool cardHealth = st.YourHealth >= 0; var h = ReadBarsSettled(15000, x => x.Stamina >= 0 && (x.Health >= 0 || cardHealth)); if (h.Stamina < 0) { var card = ReadStage(); 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; } stamina = h.Stamina; sinceBars = 0; 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; } if (mwOnly) { 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 (!sought) { sought = true; seeks++; if (mwOnly ? soften && SeekSoft(ref st) : SeekReady(ref st, Math.Max(lowest, lastBeaten > 0 ? lastBeaten + 1 : 1))) { waits = 0; continue; } noneReady = true; if (st == null || st.Stage < 0 || st.Attack == null) continue; } bool softHit = false; if (mwOnly) { string why = null; bool target = soften && SoftTarget(st.Stage); if (target && SoftenOk(ref st, out why)) softHit = true; else { if (st == null || st.Stage < 0) continue; if (target) { if (stageHits == 0 && why != null && saidOnce.Add("soften " + softDay + " " + st.Stage)) Log("Bosses: stage " + st.Stage + " not brought low before the 2X EXP - " + why); SoftPassed(st.Stage); } if (soften && softSeeks++ < 60 && SeekSoft(ref st)) { waits = 0; continue; } stop = "no most wanted"; break; } } int bossBefore = st.Health, youBefore = st.YourHealth; HealthSpent(); game.StageAttack(st); progressAt = Environment.TickCount; hits++; stageHits++; stamina--; sinceBars++; Count.BossHits++; game.Wait(1100); st = ReadStage(); 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) : ""); if (softHit && beaten) { 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++; else idle = 0; if (idle >= 1) { bool hospital; 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)) { Count.BossesBeaten++; lastBeaten = stageAt; 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); stageHits = 0; } } 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"; if (hitCosts.Count >= 3) { int cost = hitCosts.OrderBy(x => x).ElementAt(hitCosts.Count / 2); if (cost != S.BossHitHealth) { S.BossHitHealth = cost; SaveSettings(); } } if (mwOnly) { 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; SaveSettings(); 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); return; } } switch (stop) { case "cleared": 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)); break; case "no attack": 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)); break; case "stamina": case "health": if (hits == 0) Log("Bosses: " + where + " waits - " + (stop == "stamina" ? "no stamina left" : "health is low")); nextBosses = DateTime.UtcNow.AddMinutes(exp ? 2 : 10); break; case "bars": nextBosses = DateTime.UtcNow.AddMinutes(2); break; case null: nextBosses = DateTime.UtcNow.AddMinutes(1); break; default: Log("Bosses: stopped at " + where + " - " + stop); nextBosses = DateTime.UtcNow.AddMinutes(15); break; } bool up = stop != "cleared" && stop != "no attack"; Info("bossname", st != null && st.Stage > 0 ? "Stage " + st.Stage + (st.Boss.Length > 0 ? " - " + st.Boss : "") : "?"); if (stop == "cleared") { 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); } string softDay = ""; 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 <= 0) return; if (stage < MostWantedFrom) softTop = Math.Min(softTop, stage - 1); else softMw = Math.Max(softMw, stage + 1); } Dictionary> StageHitsRead() { var d = new Dictionary>(); foreach (var part in (S.StageHits ?? "").Split(';')) { var p = part.Split(':'); int stage, dmg, n; DateTime at; 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); } return d; } void NoteStageHit(int stage, int dmg, DateTime now, bool trusted = false) { if (stage <= 0 || dmg <= 0) return; var d = StageHitsRead(); Tuple 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)); SaveSettings(); } int StageHitOf(int stage, DateTime now) { Tuple 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) { why = null; 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; } game.Wait(300); var again = ReadStage(); 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"; return false; } st = again; return true; } bool SeekSoft(ref BossStage st) { if (st == null || st.Stage < 0) return false; if (softTop >= 1) { 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; softTop = 0; } if (SeekReadyFrom(ref st, softMw)) return true; if (st == null || st.Stage < 0 || st.Next == null || st.NextStage < softMw) return false; game.StageNext(st); var now = ReadStage(); if (now != null && now.Stage > 0) st = now; return true; } 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++) { int was = st.ListLast; string wasSig = st.ListSig; game.StagesListScroll(st, -3); game.Wait(400); var now = ReadStage(); if (now == null || now.Stage < 0 || now.ListAt.IsEmpty) return false; st = now; 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(); if (r.Value != null) { game.StageReady(r.Value); var now = ReadStage(); 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); return true; } if (st.ListFirst == 1) return false; int first = st.ListFirst; string firstSig = st.ListSig; game.StagesListScroll(st, 3); game.Wait(400); var then = ReadStage(); if (then == null || then.Stage < 0 || then.ListAt.IsEmpty) return false; st = then; if ((first > 0 && st.ListFirst == first) || ListStill(firstSig, st)) return false; } return false; } static bool ListStill(string before, BossStage now) { return now != null && !string.IsNullOrEmpty(before) && before == now.ListSig; } BossStage ReadStage() { BossStage st = null; 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; } 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++) { int was = st.ListFirst; string wasSig = st.ListSig; game.StagesListScroll(st, 10); game.Wait(400); var now = ReadStage(); if (now == null || now.Stage < 0 || now.ListAt.IsEmpty) break; st = now; 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; game.StageReady(st); var now = ReadStage(); 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"); return false; } return true; } 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; int last = st.ListLast; string lastSig = st.ListSig; game.StagesListScroll(st, -3); game.Wait(400); var then = ReadStage(); if (then == null || then.Stage < 0) return false; st = then; 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; } } return false; } bool SeekReadyFrom(ref BossStage st, int from) { for (int roll = 0; roll < 30 && !(st.ListFirst > 0 && st.ListFirst <= from); roll++) { int was = st.ListFirst; string wasSig = st.ListSig; game.StagesListScroll(st, 10); game.Wait(400); var now = ReadStage(); if (now == null || now.Stage < 0 || now.ListAt.IsEmpty) break; st = now; 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); if (r.Value != null) { game.StageReady(r.Value); var now = ReadStage(); 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); return true; } if (st.Next != null || st.ListAt.IsEmpty) return false; int last = st.ListLast; string lastSig = st.ListSig; game.StagesListScroll(st, -3); game.Wait(400); var then = ReadStage(); if (then == null || then.Stage < 0) return false; st = then; if ((last > 0 && st.ListLast == last) || ListStill(lastSig, st)) return false; } 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 ok) { int end = Environment.TickCount + maxMs; int closed = 0; while (true) { Header h; using (var f = game.Capture()) { h = game.ReadHeader(f); 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; game.Wait(400); } } const int LowHealthPct = 10; Rejoiner rejoiner = new Rejoiner(); int[] barsSeen; DateTime barsSameSince = DateTime.MinValue; int dialogLookAt = Environment.TickCount - 60000; bool dialogUp; bool disconnectHint; bool robloxUsed; DateTime robloxUsedAt; string dialogSeen; uint stepInput; IntPtr focusBack; DateTime gameInput; 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; disconnectHint = 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; return true; } bool eventWindow; bool notifyWindow; bool inviteWindow; string defendName, defendCity; int defendNo; bool defendStationed; DateTime defendHit = DateTime.MinValue, defendNext = DateTime.MinValue; static readonly TimeSpan DefendAgain = TimeSpan.FromMinutes(3), DefendOver = TimeSpan.FromMinutes(2); DateTime nextRulingLook = DateTime.MinValue; void TerritoryCheck() { bool lost; System.Drawing.Point x; string read, by, text; for (int toast = 0; toast < 4; toast++) { using (var f = game.Capture()) read = game.TerritoryToastDistrict(f, out lost, out x, out by, out text); if (read == null) break; string spared = S.DefendTerritory ? DefendSpared(S.NeverDefend, by, text) : null; if (spared != 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); } Shipment(); 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(); } void SaveDefence() { 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)); SaveSettings(); } void LoadDefence() { defendName = null; defendStationed = false; defendByList = false; defendNext = DateTime.MinValue; var p = (S.Defence ?? "").Split('|'); int no; DateTime hit; 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"); } bool defendByList; internal static string DefendSpared(string list, string by, string text) { Func key = s => Parse.Key(s).Replace("2", "II").Replace("3", "III"); string b = key(by), t = key(text); foreach (var e in NeverAttackEntries(list)) { string k = key(e); 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; } } return null; } readonly Dictionary shipmentsSaid = new Dictionary(); void Shipment() { string text; System.Drawing.Point x; 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) { string city, name; int no; 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"); return false; } if (defendName != null && defendName != name) { SayOnce("defending elsewhere " + name, "Territory: " + name + " is attacked too - the crew stays at " + defendName + " while that attack lasts"); return false; } 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; SaveDefence(); return true; } 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 (toast) { 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); return; } DistrictWindow w; 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)) { game.CloseDistrict(w); Log("Territory: " + defendName + " is held by " + w.HeldBy + " now - the defence ends"); EndDefence(null); return; } 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); game.CloseDistrict(); 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; SaveDefence(); 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")); } void CloseEventWindow() { TerritoryCheck(); if (notifyWindow) { notifyWindow = false; using (var f = game.Capture()) game.CloseNotifyPrompt(f); } if (inviteWindow) { inviteWindow = false; using (var f = game.Capture()) game.CloseInvite(f); } if (!eventWindow) return; eventWindow = false; using (var f = game.Capture()) game.CloseEventPopup(f); } DateTime robloxBarSince = DateTime.MinValue, robloxBarSaid = DateTime.MinValue; void NoteRobloxBar(bool showing) { var now = DateTime.UtcNow; if (showing) { if (robloxBarSince != DateTime.MinValue) return; robloxBarSince = now; if (now - robloxBarSaid < TimeSpan.FromMinutes(30)) return; robloxBarSaid = now; 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; barSweeps = 0; barFixTries = 0; nextBarSweep = DateTime.MinValue; bankHiddenSaid = false; } } DateTime nextBarSweep = DateTime.MinValue; int barSweeps; int barFixTries; 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; barSweeps++; nextBarSweep = now.AddMinutes(barSweeps < 3 ? 2 : 15); bool gone = false; 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; } bool f11 = false; 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"); return true; } bool FixRobloxBarNow() { 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; int halfSaid; DateTime halfSaidAt = DateTime.MinValue; readonly HashSet coverSaid = new HashSet(); void NoteEmptyPictures() { NoteScreenFirst(); 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(); void NoteScreenFirst() { if (!RobloxWindow.ScreenFirst || S.ScreenFirst) return; lock (screenFirstLock) { if (S.ScreenFirst) return; S.ScreenFirst = true; SaveSettings(); } 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); } bool LeaveDeadGame() { 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; dialogUp = true; if (!Offline(d)) { State(rejoiner.Status ?? Game.Disconnected); WatchRoblox(4000); } return true; } void WatchRoblox(int ms) { int end = Environment.TickCount + ms; do { 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); } IntPtr readHandle; bool readOk; bool rejoinIdleSaid; void GameReadable() { var now = DateTime.UtcNow; dialogSeen = null; robloxUsed = false; disconnectSaid = false; rejoinIdleSaid = false; readHandle = game.Win.Handle; readOk = true; rejoiner.Readable(now); RejoinNote(); if (focusBack != IntPtr.Zero) { if (Native.LastInputTick() == stepInput) game.Win.GiveFocusBack(focusBack); focusBack = IntPtr.Zero; } if (game.Win.IsForeground && game.Win.UserActiveWithin(5000)) gameInput = now; } bool Offline(Game.Disconnect d) { var now = DateTime.UtcNow; Rejoiner.Step step; if (d != null) { string text = d.ToString(); if (text != dialogSeen) { 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")); dialogSeen = text; } 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; } } else { dialogSeen = null; step = rejoiner.OnUnreadable(now, Rejoins, robloxUsed); } return TakeRejoinStep(step); } bool TakeRejoinStep(Rejoiner.Step step) { RejoinNote(); 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; do WatchRoblox(500); while (!stop && !robloxUsed && game.Win.UserActiveWithin(S.IdleSeconds * 1000) && end - Environment.TickCount > 0); return true; } var now = DateTime.UtcNow; if (step == Rejoiner.Step.Launch) { IntPtr front = Native.GetForegroundWindow(); rejoiner.Took(step, now); robloxUsed = false; try { RobloxWindow.StartGame(); } catch (Exception e) { Log("Couldn't start Idle Mafia Game: " + e.Message); rejoiner.Failed(now); RejoinNote(); Sleep(4000); return true; } Log("Starting Idle Mafia Game again"); stepInput = Native.LastInputTick(); focusBack = S.GiveFocusBack ? front : IntPtr.Zero; game.ForgetView(); State("Starting Idle Mafia Game in Roblox..."); WatchRoblox(5000); return true; } 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); robloxUsed = false; stepInput = Native.LastInputTick(); game.ForgetView(); Log("Pressed Esc, L and Enter in Roblox (its menu's Leave) - the game is started again once Roblox has closed"); } WatchRoblox(3000); return true; } 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); }); if (pressed) { rejoiner.Took(step, now); robloxUsed = false; 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"); } WatchRoblox(3000); return true; } volatile string stopWhy; DateTime awaySince = DateTime.MinValue, nextSummary, nextLevelPing; string awaySaid; volatile bool offlineSaid; volatile bool awayOut; bool disconnectSaid, awayYours; public bool OutOfGame { get { return offlineSaid || awayOut; } } public bool GameLeftToYou { get { return rejoiner.LeftToYou || robloxCheckUp; } } volatile bool robloxCheckUp; Counters summaryFrom; int summaryLevel = -1, levelSaid = -1, levelSeen = -1, levelSaidXp = -1, rerollsSaid; void DiscordStart() { var now = DateTime.UtcNow; stopWhy = null; 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."; } byte[] Shot() { 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 (robloxUsed) return; if (awayLogged == null && now - awaySince >= TimeSpan.FromMinutes(2)) { awayLogged = key; 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; awaySaid = key; if (awayYours) return; offlineSaid = true; Discord.Problem("away " + key, title, text, key == "closed" ? null : (Func)Shot, false, 60); } string awayLogged; bool rejoinBackSaid; void BackIn() { if (awayLogged != null && !rejoinBackSaid) Log("The game reads again, after " + Math.Max(1, (int)Math.Round((DateTime.UtcNow - awaySince).TotalMinutes)) + " minutes"); rejoinBackSaid = false; 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; awayOut = false; awaySaid = awayLogged = null; awaySince = DateTime.MinValue; } void RejoinNote() { string n = rejoiner.Note; rejoiner.Note = null; if (n == null) return; Log(n); if (n.StartsWith("Back in the game")) { rejoinBackSaid = true; if (offlineSaid) Discord.Problem("back", "✅ " + n, "The bot plays on.", Shot, false, 0, Discord.Green); offlineSaid = false; } 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); levelSaid = lv; levelSaidXp = xpNext; nextLevelPing = DateTime.UtcNow.AddMinutes(10); } void PingHenchman(Game.CrewRow got, int replaced, bool perfect, IEnumerable 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 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()).Any(r => !ReferenceEquals(r, got) && r.Rarity >= 0 && r.Rarity < got.Rarity); } void DiscordSummary() { var now = DateTime.UtcNow; if (now < nextSummary) return; nextSummary = now.AddHours(3); var from = summaryFrom; int fromLevel = summaryLevel; summaryFrom = Count.Copy(); summaryLevel = level; if (from == null || !Discord.On(Ping.Summary)) return; var c = Count; var lines = new List(); 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(); Action 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 FamilyRead; FamilyInfo family = new FamilyInfo(); DateTime nextTakedown, nextGive, nextFamily; DateTime freeGoneAt = DateTime.MinValue; bool familyMissingSaid; int takedownHealthCost = 25; int takedownNoAttack; int takedownNotTaken; DateTime takedownHold = DateTime.MinValue; DateTime takedownAwaySince = DateTime.MinValue; int familyFails; int familyShots; string familyLogDay; readonly HashSet saidOnce = new HashSet(); DateTime bossesReadAt = DateTime.MinValue, bossBackAt = DateTime.MaxValue; bool bossesUp; readonly RegenGuess energyRegen = new RegenGuess(), staminaRegen = new RegenGuess(); int takedownWants; int givenSinceFights; DateTime giveBrokenUntil = DateTime.MinValue; int giveMissed; void GiveMissed(string what) { int wait = ++giveMissed == 1 ? 10 : 60; Log(what + " - tries again in " + wait + " minutes"); game.NoteFailure(what); nextGive = giveBrokenUntil = DateTime.UtcNow.AddMinutes(wait); } void ResetFamily(DateTime now) { nextTakedown = nextGive = nextFamily = now; familyMissingSaid = false; familyFails = 0; bossesReadAt = DateTime.MinValue; bossBackAt = DateTime.MaxValue; bossesUp = false; takedownWants = givenSinceFights = giveMissed = 0; giveBrokenUntil = DateTime.MinValue; saidOnce.Clear(); } 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; bossesUp = stillUp > 0; bossBackAt = soonest < int.MaxValue ? DateTime.UtcNow.AddSeconds(soonest) : DateTime.MaxValue; } void NoteTicks(Header h) { if (h == null) return; 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); if (g < 0) return false; value = g; max = regen.Max; return true; } int Kept(int 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); return max / 2; } static int FightSpare(int max, int kept) { return max > 0 ? Math.Max(1, Math.Min(5, max - kept)) : 5; } bool FamilyTakes() { 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); if (!FamilyTakes()) { 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 false; } return kept > 0 && h.Stamina >= kept && h.StaminaMax > 0 && givenSinceFights >= Math.Max(5, h.StaminaMax - kept); } bool familyOpened; DateTime familyDoubtAt = DateTime.MinValue; bool FamilyOpen(string sub, ref DateTime retry) { var nav = game.OpenFamily(sub); if (nav == FamilyNav.Open) { familyFails = 0; familyOpened = true; familyDoubtAt = DateTime.MinValue; if (family.NotInFamily) { family.NotInFamily = false; familyMissingSaid = false; FamilyChanged(); } return true; } 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); return false; } familyOpened = false; 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); familyFails = 0; 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)" : "")); return false; } if (nav == FamilyNav.NotInFamily) { nextTakedown = nextGive = nextFamily = DateTime.UtcNow.AddHours(6); if (!familyMissingSaid) { 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; FamilyChanged(); return false; } retry = DateTime.UtcNow.AddMinutes(30); return false; } void NoteTakedown(TakedownState t) { if (t == null || !t.Page) return; family.Takedown = t; family.TakedownAt = DateTime.Now; FamilyChanged(); } void NotePerks(List rows) { if (rows == null || rows.Count == 0) return; family.Perks = rows; family.PerksAt = DateTime.Now; FamilyChanged(); } 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"; return null; } int takedownEvery = 60; bool TakedownCrewHome() { 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 (away > 0) { 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"); return false; } 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(); return true; } void DoTakedown() { takedownWants = 0; nextTakedown = DateTime.UtcNow.AddMinutes(takedownEvery); if (!FamilyOpen("TAKEDOWN", ref nextTakedown)) return; var t = game.ReadTakedownSteady(); NoteTakedown(t); 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"); } var start = t; 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); if (why != null) { 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); takedownNoAttack = 0; 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); break; } takedownNoAttack = 0; 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); waitShort = true; break; } 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) { barFixTries++; 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); waitShort = true; break; } if (!crewChecked) { crewChecked = true; if (!TakedownCrewHome()) { crewWait = waitShort = true; break; } if (!FamilyOpen("TAKEDOWN", ref nextTakedown)) break; t = game.ReadTakedownSteady(); NoteTakedown(t); continue; } hpBefore = hp; int free = t.Free; 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); break; } done++; Count.TakedownAttacks++; family.Attacks++; if (done == 1) { game.Wait(600); SnapshotOnce("takedown right after an attack"); } string said; 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--; NoteTakedown(after); 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")); waitShort = true; break; } game.Snapshot("takedown attack not counted", 60); done--; Count.TakedownAttacks--; family.Attacks--; NoteTakedown(after); t = after; 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) + ")" : "")); waitShort = true; if (after.NewSeconds > 0 && DateTime.UtcNow.AddSeconds(after.NewSeconds + 120) < nextTakedown) nextTakedown = DateTime.UtcNow.AddSeconds(after.NewSeconds + 120); } break; } takedownNotTaken = 0; 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(); NoteTakedown(t); } if (done > 0) { var parts = new List { 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)); } int GiveReserve(int max) { if (max > 10 && ExpKeepsStamina(DateTime.UtcNow)) return Math.Max(Kept(max), max - 10); return Kept(max); } 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"; return null; } string giveWhySaid; 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; giveWhySaid = why; Info("givewhy", why); } PerkRow PickPerk(List 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; } return r; } 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"); return pick; } static string Progress(PerkRow r) { return r == null || r.Have < 0 ? "?" : string.Format(CultureInfo.InvariantCulture, "{0:N0} of {1:N0}", r.Have, r.Need); } void DoGiveStamina() { try { GiveStaminaHere(); } finally { if (nextGive > DateTime.UtcNow.AddMinutes(20)) giveBrokenUntil = nextGive; } } void GiveStaminaHere() { nextGive = DateTime.UtcNow.AddMinutes(5); var h = ReadBarsSettled(3000); bool open = false; if (h.Stamina < 0) { if (!FamilyOpen("PERKS", ref nextGive)) return; open = true; h = ReadBarsSettled(6000); if (h.Stamina < 0) { 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); return; } } if (WhyNotGive(h) != null) { Neutral(); return; } if (!open && !FamilyOpen("PERKS", ref nextGive)) return; var rows = game.WalkStaminaPerks(); if (rows.Count == 0) { game.Wait(1500); if (!FamilyOpen("PERKS", ref nextGive)) return; rows = game.WalkStaminaPerks(); } NotePerks(rows); if (rows.Count == 0) { game.Snapshot("stamina perks unreadable", 60); GiveMissed("Couldn't read the stamina perks - no stamina given"); return; } var pick = PickPerk(rows); if (pick == null) { nextGive = DateTime.UtcNow.AddMinutes(60); return; } int reserve = GiveReserve(h.StaminaMax), stamina = h.Stamina, given = 0; PerkRow row = null; while (stamina - 5 >= reserve && given < 500) { row = (row != null ? game.PerkRowInSight(pick.Name) : null) ?? game.FindPerkRow(pick.Name); if (row == null) { 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"); break; } if (row.Maxed) break; long haveBefore = row.Have; var pressedAt = DateTime.UtcNow; game.GiveStamina(row); PerkRow seen = null; bool counted = false; for (int look = 0; look < 3 && !counted; look++) { if (look > 0) game.Wait(500); var r = game.PerkRowInSight(pick.Name, true); if (r == null) continue; seen = r; 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; } if (!counted) { string toast = game.ReadToast(800, t => Parse.Has(t, "GAVE") && Parse.Has(t, "STAMINA")); counted = toast != null; } int left = stamina - 5; if (!counted) { var after = ReadBarsSettled(6000, x => x.Stamina >= 0 && x.Stamina <= stamina - 4); if (after.Stamina < 0) { Log("Gave stamina to " + pick.Name + " but couldn't see the 5 go (the stamina bar was covered) - giving again in 5 minutes"); break; } 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); break; } 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)); break; } left = after.Stamina; } if (seen != null) row = seen; if (given == 0) SnapshotOnce("stamina perk after GIVE 5"); giveMissed = 0; given += 5; stamina = left; Count.StaminaGiven += 5; family.StaminaGiven += 5; givenSinceFights += 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; } } void DoFamilyWatch() { 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); } else { 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.Overview = o; family.OverviewAt = DateTime.Now; FamilyChanged(); } 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()); } } string FamilyLine() { var ic = CultureInfo.InvariantCulture; var parts = new List(); var o = family.Overview; if (o != null) { var p = new List { (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 t = family.Takedown; if (t != null) { var p = new List { "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); } void ScanFamily() { 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; FamilyChanged(); if (FamilyOpen("TAKEDOWN", ref ignore)) { var t = game.ReadTakedownSteady(); NoteTakedown(t); 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(); NotePerks(rows); foreach (var r in rows) 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)); } Log(FamilyLine()); } public event Action ProfileRead; public event Action ProfileChanged; void CheckGame() { 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; try { 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; } var sh = SafehouseRead; if (sh != null) sh(s); }); if (Open(p, Tab.Bank, "the Bank")) CheckPart(p, "the Bank's fee", () => { string line; using (var f = game.Capture()) p.BankFee = game.ReadBankFee(f, out line); p.BankLine = line; if (p.BankFee >= 0) p.BankFeeFrom = "the Bank page"; }); if (Open(p, Tab.Operations, "Operations")) CheckPart(p, "Operations", () => { List slots; 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); var ops = OpsRead; 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])); p.JobCash = j.Cash; if (game.LearnJobs(Catalog, 0, false) > 0) { GameData.Renumber(Catalog); SaveCatalog(); } }); if (Open(p, Tab.Crew, "the Crew")) CheckPart(p, "the Crew", () => { Game.CrewTotals c; 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; } else 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 = ""; return; } if (nav != FamilyNav.Open) { p.Note("your family"); return; } var o = game.ReadFamilyOverviewAll(); p.InFamily = 1; if (o.Name.Length > 0) p.Family = o.Name + (o.Tag.Length > 0 ? " [" + o.Tag + "]" : ""); p.FamilyLevel = o.Level; if (o.Members >= 0) { p.Members = o.Members; p.MembersMax = o.MembersMax; } family.Overview = o; family.OverviewAt = DateTime.Now; FamilyChanged(); }); 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", () => { game.HeistsTop(); 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); try { p.Save(file); } catch (Exception e) { Log("Couldn't save what the check found: " + e.Message); } feeNotedFor = null; foreach (var row in Setup.Found(p)) Log("Check - " + row[0] + ": " + row[1]); #if LINUX Log("--- Check finished (saved in " + System.IO.Path.GetFileName(file) + "). The Tasks page shows what it suggests. Nothing changes until you say so ---"); #else Log("--- Check finished (saved in " + System.IO.Path.GetFileName(file) + "). The window shows what it suggests. Nothing changes until you say so ---"); #endif var ev = ProfileRead; if (ev != null) ev(p); } bool Open(Profile p, Tab t, string what) { if (p.Lacks(t)) return false; bool ok = false; CheckPart(p, what, () => { ok = game.OpenTab(t); if (!ok && !game.NotInMenu(t)) p.Note(what + " (the tab didn't open)"); }); return ok; } void CheckPart(Profile p, string what, Action read) { try { read(); } catch (StopException) { throw; } catch (UserBusyException) { throw; } catch (WindowChangedException) { throw; } catch (NeedUserException) { throw; } catch (Exception e) { 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); } } void Scan() { Log("--- Scan: reading every screen, no actions ---"); if (last != null) 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)); ReportTarget(); var job = PickJob(last); if (job != null) Log("Job it would do now: " + job.Name + " (" + job.Cost + " energy)"); if (game.OpenTab(Tab.Heists)) { game.HeistsTop(); 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()) { bool war; 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); var ops = OpsRead; 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 : "")); } ScanFamily(); if (game.OpenTab(Tab.Crew)) using (var f = game.Capture()) { System.Drawing.Rectangle box; string why; 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(); if (info != null) { foreach (var line in info.Lines()) Log(line); var h = SafehouseRead; if (h != null && info.Found > 0) h(info); } Log("--- Scan finished ---"); } } }