using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text.RegularExpressions; using System.Threading; namespace IdleMafiaBot { enum Tab { Safehouse, Jobs, Properties, Inventory, Collection, Shop, Fight, Bank, Operations, Contracts, Crew, Bosses, Family, Territory, Rankings, Trade, BlackMarket, Heists } enum ButtonState { None, Ready, Dim, Locked } sealed class Header { public string Name, Family; public Rectangle NameBox; public int Level = -1, Xp = -1, XpNext = -1, SkillPoints = -1; public int Energy = -1, EnergyMax = -1, EnergyTick = -1; public int Stamina = -1, StaminaMax = -1, StaminaTick = -1; public int Health = -1, HealthMax = -1; public double Cash = -1, Banked = -1; public bool HealthAtLeast(int pct) { return Health >= 0 && HealthMax > 0 && Health * 100 >= pct * HealthMax; } } sealed class JobCard { public JobInfo Info; public string Name, ButtonText = "", Mastery; public Rectangle NameBox, MasteryBar; public bool BarGuessed; public Point ButtonAt; public Point StateAt; public int Cost = -1, Xp = -1; public ButtonState Button; public int NameY { get { return NameBox.Y; } } } sealed class HeistRow { public int Top; public Point HelpAt; public string Name = ""; public string HelpText = ""; public int XpPerHelp = -1, Energy = -1; public bool CanHelp, YourFamily; } sealed class FightRow { public int Index; public Point AttackAt, NameAt; public string AttackText = "", Name = "", Family = ""; public int Level = -1; public int Steal = -1; public int StaminaCost = 5; public int Prestige = -1; public bool Ready, War, NoFamily; } sealed class OpSlot { public int Index; public string Name = ""; public int SecondsLeft = -1; public bool Running, Locked, Done, Pass; public OcrLine Collect; } sealed class OpRow { public string Name; public int Y; public Point StartAt; public string StartText = ""; public int Seconds = -1; public int Xp = -1; public bool CanStart; public int Page; } sealed class CityHeader { public int Index; public int Y; public bool Collapsed; public Point ButtonAt; } sealed class StopException : Exception { public StopException() : base("stopped") { } } sealed class NeedUserException : Exception { public NeedUserException(string m) : base(m) { } } sealed partial class Game { public const int RED = 0xBA4A3E, BLUE = 0x5B8FB9, HEADER_BG = 0x1A1A1C; public readonly RobloxWindow Win = new RobloxWindow(); readonly Action log; readonly Func stopRequested; readonly Dictionary jobScroll = new Dictionary(); public Game(Action log, Func stopRequested) { this.log = log; this.stopRequested = stopRequested; } public Func UserBack = () => false; public void Check() { if (stopRequested()) throw new StopException(); if (UserBack()) throw new UserBusyException(); } public void Wait(int ms) { int end = Environment.TickCount + ms; while (end - Environment.TickCount > 0) { Check(); Thread.Sleep(Math.Min(100, Math.Max(1, end - Environment.TickCount))); } } public Frame Capture() { Check(); return Win.Capture(); } public string ProblemsDir; public Action Saved; public Action Failed; public void NoteFailure(string reason) { var h = Failed; if (h != null) h(reason, true); } readonly Dictionary lastShot = new Dictionary(); public int Shots { get; private set; } public const string Covered = "-covered"; public volatile string Tag; public void Snapshot(string reason, int minMinutes = 0, bool keep = false, bool failure = true, Bitmap picture = null) { var failed = Failed; if (failure && !keep && failed != null) failed(reason, false); if (string.IsNullOrEmpty(ProblemsDir)) return; try { string slug = Regex.Replace(reason ?? "problem", @"[^A-Za-z0-9]+", "-").Trim('-'); if (slug.Length > 60) slug = slug.Substring(0, 60); var tag = Tag; if (!string.IsNullOrEmpty(tag)) slug += "-" + Regex.Replace(tag, @"[^A-Za-z0-9]+", ""); DateTime shot; if (minMinutes > 0 && lastShot.TryGetValue(slug, out shot) && DateTime.UtcNow < shot.AddMinutes(minMinutes)) return; string active = System.IO.Path.Combine(ProblemsDir, "active"); if (keep && SeenBefore(slug)) return; string dir = keep ? System.IO.Path.Combine(active, "first seen") : active; System.IO.Directory.CreateDirectory(dir); string file = System.IO.Path.Combine(dir, DateTime.Now.ToString("yyyyMMdd-HHmmss") + "-" + slug + ".png"); bool sendable; using (var f = picture != null ? new Frame(Frame.Marked(picture, new Bitmap(picture))) : Win.Capture()) { sendable = f.Sendable; if (!sendable) file = file.Substring(0, file.Length - 4) + Covered + ".png"; if (GameLoading(f)) { log("(The game was loading just then - after an update, a teleport or a reconnect. No picture kept.)"); return; } f.Bitmap.Save(file, System.Drawing.Imaging.ImageFormat.Png); } lastShot[slug] = DateTime.UtcNow; if (!keep) Shots++; log("Saved a screenshot of this: problems\\active\\" + (keep ? "first seen\\" : "") + System.IO.Path.GetFileName(file)); var saved = Saved; if (!keep && failure && saved != null) saved(reason ?? "problem", sendable ? file : null); var reasons = new HashSet(); int kept = 0, most = keep ? 200 : 80; foreach (var o in new System.IO.DirectoryInfo(dir).GetFiles("*.png").OrderByDescending(x => x.Name)) { bool newest = reasons.Add(o.Name.Length > 16 ? o.Name.Substring(16) : o.Name); if (++kept > most && !newest) o.Delete(); } } catch (Exception) { } } public static bool GameLoading(Frame f) { bool updated; return GameLoading(f, out updated); } public static bool GameLoading(Frame f, out bool updated) { updated = false; bool loading = false; foreach (var l in Ocr.Read(f, new Rectangle(0, 0, f.Width, f.Height), f.Width < 1300 ? 2 : 1, Prep.None)) { string k = Parse.Key(l.Text); if (k == "GAMEUPDATED" || k.Contains("JOININGTHEIATESTVERSION") || k.Contains("PIAYINGVERSION")) loading = updated = true; else if (k.Contains("IOADINGYOURSAVE") || k.Contains("PREPARINGYOURINTERFACE")) loading = true; } return loading || RobloxJoining(f); } internal static bool RobloxJoining(Frame f) { var area = Rectangle.FromLTRB(f.Width / 4, f.Height * 40 / 100, f.Width * 3 / 4, f.Height * 70 / 100); var lines = Ocr.Read(f, area, f.Width < 1300 ? 2 : 1, Prep.None); var name = lines.FirstOrDefault(l => Parse.Key(l.Text).StartsWith("IDIEMAFIAGAM")); if (name == null) return false; int h = Math.Max(6, name.Box.Height); return lines.Any(l => l != name && l.Box.Y > name.Box.Bottom && l.Box.Y - name.Box.Bottom < 6 * h && Math.Abs(l.CenterX - name.CenterX) < name.Box.Width && Parse.Key(l.Text).EndsWith("CASH")); } static readonly Regex VersionRx = new Regex(@"VERS[I1l]ON\s*([0-9]+(?:\.[0-9]+){1,3})", RegexOptions.IgnoreCase); public static string LoadingVersion(Frame f) { var area = Rectangle.FromLTRB(f.Width * 70 / 100, f.Height * 90 / 100, f.Width, f.Height); foreach (int s in new[] { f.Width < 1300 ? 3 : 2, 4 }) { var m = VersionRx.Match(Ocr.ReadText(f, area, s, Prep.None)); if (m.Success) return m.Groups[1].Value; } return null; } bool SeenBefore(string slug) { foreach (var sub in new[] { System.IO.Path.Combine("active", "first seen"), System.IO.Path.Combine("fixed", "first seen"), System.IO.Path.Combine("reported", "first seen"), "first seen" }) { string d = System.IO.Path.Combine(ProblemsDir, sub); if (System.IO.Directory.Exists(d) && (System.IO.Directory.GetFiles(d, "????????-??????-" + slug + ".png").Length > 0 || System.IO.Directory.GetFiles(d, "????????-??????-" + slug + Covered + ".png").Length > 0)) return true; } return false; } void Click(int x, int y, int waitMs) { Check(); NoteLateFrame(); Win.Click(x, y); Wait(waitMs + (FrameLag() > 0 ? 800 : 0)); } void Scroll(int x, int y, int notches) { Check(); byte[] before = NoteLateFrame() ?? PageSample(); Win.Scroll(x, y, notches); byte[] now = PageSample(); if (FrameLag() > 0 && Moved(before, now) && ++quickRolls >= 10) { frameLagMs = 0; lateSeen = 0; quickRolls = 0; log("Roblox's picture comes right away again - the bot no longer waits for it after a scroll"); } for (int waited = 0; waited < Math.Max(frameLagMs, 0) && !Moved(before, now); waited += 150) { Wait(150); now = PageSample(); } lastScrollStill = Moved(before, now) ? null : now; stillAt = Win.LastInput; } int frameLagMs; byte[] lastScrollStill; DateTime stillAt; int lateSeen; DateTime lateSeenAt = DateTime.MinValue; DateTime tabOpenedAt = DateTime.MinValue; int quickRolls; static readonly TimeSpan LateWindow = TimeSpan.FromMinutes(15); internal static int LateCount(int count, DateTime lastAt, DateTime now) { return count > 0 && now - lastAt <= LateWindow ? count + 1 : 1; } internal static bool LateOver(DateTime lastAt, DateTime now) { return now - lastAt > LateWindow; } internal static bool RightAfterTab(DateTime rollAt, DateTime tabAt) { return tabAt != DateTime.MinValue && rollAt >= tabAt && rollAt - tabAt < TimeSpan.FromSeconds(3); } int FrameLag() { EndLateWait(DateTime.UtcNow); return frameLagMs; } internal void EndLateWait(DateTime now) { if (frameLagMs <= 0 || !LateOver(lateSeenAt, now)) return; frameLagMs = 0; lateSeen = 0; quickRolls = 0; log("Roblox's picture has come right away for 15 minutes - the bot no longer waits for it"); } byte[] NoteLateFrame() { if (lastScrollStill == null) return null; var now = PageSample(); if (Win.LastInput != stillAt) { lastScrollStill = null; return now; } if (Moved(lastScrollStill, now)) LateSeen(); lastScrollStill = null; return now; } void LateSeen() { LateSeen(stillAt, DateTime.UtcNow); } internal void LateSeen(DateTime rollAt, DateTime now) { if (RightAfterTab(rollAt, tabOpenedAt)) return; EndLateWait(now); quickRolls = 0; lateSeen = LateCount(lateSeen, lateSeenAt, now); lateSeenAt = now; if (lateSeen >= 2 && frameLagMs < 2500) { frameLagMs = 2500; log("Roblox's picture came late twice after a scroll (a slow or busy PC or game does this, or a remote desktop that was closed) - " + "the bot waits for it until it has come right away for 15 minutes"); Snapshot("late picture", 0, true); } else if (frameLagMs >= 2500 && frameLagMs < 6000 && lateSeen >= 6) { frameLagMs = 6000; log("Roblox's picture comes even later - the bot waits up to 6 s for it after a scroll"); } } internal bool WaitLateRoll(int ms) { var still = lastScrollStill; if (still == null) return false; for (int waited = 0; waited < ms; waited += 250) { Wait(250); if (Win.LastInput != stillAt) return false; if (Moved(still, PageSample())) { lastScrollStill = null; LateSeen(); Wait(300); return true; } } return false; } byte[] PageSample() { if (Win.Minimized) return null; using (var f = Capture()) { var a = view != null && view.Problem == null && view.Width == f.Width && view.Height == f.Height ? view.Page : new Rectangle(f.Width / 5, f.Height / 6, f.Width * 3 / 4, f.Height * 3 / 4); var s = new byte[32 * 24]; for (int j = 0; j < 24; j++) for (int i = 0; i < 32; i++) { var c = f.Pixel(a.X + a.Width * (2 * i + 1) / 64, a.Y + a.Height * (2 * j + 1) / 48); s[j * 32 + i] = (byte)((c.R + c.G + c.B) / 3); } return s; } } static bool Moved(byte[] a, byte[] b) { if (a == null || b == null || a.Length != b.Length) return false; int n = 0; for (int i = 0; i < a.Length; i++) if (Math.Abs(a[i] - b[i]) > 24) n++; return n * 4 > a.Length; } internal int ListNotches(int max) { int h = view != null && view.Problem == null ? view.Page.Height : 891; double k = Math.Max(1, notchPx / 98); return Math.Max(1, Math.Min(max, (int)((h / k - 150) / 98))); } double notchPx = 98; Size notchFor = Size.Empty; int notchTries; Size lastNotchTry = Size.Empty; void MeasureNotch(List catalog) { if (view == null || view.Problem != null) return; var size = new Size(view.Width, view.Height); if (size == notchFor) return; if (notchTries >= 3 && size == lastNotchTry) return; if (size != lastNotchTry) { lastNotchTry = size; notchTries = 0; } notchTries++; List before, after; using (var f = Capture()) before = ReadJobCards(f, catalog, false); if (before.Count == 0) return; ScrollPage(0.54, -1); using (var f = Capture()) after = ReadJobCards(f, catalog, false); ScrollPage(0.54, 1); var shifts = new List(); foreach (var a in after) { var b = before.FirstOrDefault(x => a.Info != null ? x.Info == a.Info : x.Name == a.Name); if (b != null && b.NameY > a.NameY) shifts.Add(b.NameY - a.NameY); } if (shifts.Count == 0) return; shifts.Sort(); int px = shifts[shifts.Count / 2]; if (px < 60) return; notchFor = size; if (Math.Abs(px - notchPx) < 12) return; notchPx = Math.Max(30, px); jobScroll.Clear(); if (notchPx > 120) log("Lists: a mouse-wheel notch moves them " + px + " px on this screen (98 at Windows' 100% scale) - the bot rolls them by less from now on"); } internal void ScrollPage(double fy, int notches) { var p = view != null && view.Problem == null ? view.PagePoint(0.5, fy) : new Point(1085, 118 + (int)(891 * fy)); Scroll(p.X, p.Y, notches); } public static bool IsDim(Frame f, int x, int y) { var c = f.Pixel(x, y); int max = Math.Max(c.R, Math.Max(c.G, c.B)), min = Math.Min(c.R, Math.Min(c.G, c.B)); return max <= 95 && min >= 20 && max - min <= 22; } public static bool GoldIsh(Frame f, int x, int y) { var c = f.Pixel(x, y); return c.R >= 120 && c.G >= 95 && c.B <= 90 && c.R - c.B >= 70 || EventOrange(c); } public static bool EventOrange(Color c) { return c.R >= 150 && c.G >= 0.44 * c.R && c.G <= 0.62 * c.R && c.B <= 0.33 * c.R || CurseGreen(c); } public static bool CurseGreen(Color c) { return c.G >= 170 && c.R >= 0.55 * c.G && c.R <= 0.72 * c.G && c.B <= 0.45 * c.G; } public static bool EventFace(Color c) { return Math.Abs(c.R - 37) <= 6 && Math.Abs(c.G - 31) <= 6 && Math.Abs(c.B - 50) <= 6 || Math.Abs(c.R - 30) <= 6 && Math.Abs(c.G - 44) <= 7 && Math.Abs(c.B - 28) <= 8; } public static bool EventEdge(Color c) { return Math.Abs(c.R - 66) <= 8 && Math.Abs(c.G - 56) <= 8 && Math.Abs(c.B - 88) <= 8; } View view; int viewAt, remeasures; readonly int[] barMax = { -1, -1, -1 }; readonly Dictionary maxOf = new Dictionary(); readonly string[] barLook = { "", "", "" }; public string BarLook(int i) { return barLook[i]; } public int BarMax(int i) { return barMax[i]; } readonly int[] barShort = { 0, 0, 0 }; IntPtr viewWindow; View lastGood; IntPtr lastGoodWindow; public View ViewOf(Frame f) { bool sameWindow = view != null && view.Width == f.Width && view.Height == f.Height && viewWindow == Win.Handle; if (sameWindow && view.Problem != null && Environment.TickCount - viewAt < 10000) return view; if (!sameWindow || view.Problem != null) { view = View.Read(f); viewAt = Environment.TickCount; remeasures = 0; viewWindow = Win.Handle; if (view.Problem == null) { Win.ParkAt = view.ParkPoint; lastGood = view; lastGoodWindow = Win.Handle; } else { var kept = KeptUnderNotification(f); if (kept != null) view = kept; } } else if ((view.EnergyPlaced || view.Cash == null) && remeasures < 20 && Environment.TickCount - viewAt > 2000) { remeasures++; viewAt = Environment.TickCount; var again = View.Read(f); if (again.Problem == null) { view = again; Win.ParkAt = view.ParkPoint; if (again.Cash != null && !again.EnergyPlaced) remeasures = 0; } } return view; } public void ForgetView() { view = null; } View KeptUnderNotification(Frame f) { var g = lastGood; if (g == null || g.Width != f.Width || g.Height != f.Height || lastGoodWindow != Win.Handle) return null; return UnderNotification(g, f) ? g : null; } public View MenuNow(Frame f) { var v = View.Read(f); return v.Problem == null ? v : KeptUnderNotification(f); } public static bool UnderNotification(View good, Frame f) { bool covered = !f.Near(good.ProbeX, good.Energy.CenterY, HEADER_BG, 14) || !f.Near(good.ProbeX, good.Stamina.CenterY, HEADER_BG, 14) || !GoldToastBox(f, good, good.Energy.CenterY).IsEmpty || !GoldToastBox(f, good, good.Stamina.CenterY).IsEmpty; return covered && good.ReadMenu(f); } public Header ReadHeader(Frame f) { var v = ViewOf(f); return v.Problem == null ? ReadHeader(f, v) : new Header(); } public volatile bool RobloxBar; internal static bool RobloxBarOver(Frame f) { foreach (double fy in new[] { 0.014, 0.028 }) { int y = Math.Max(1, (int)(f.Height * fy)), hits = 0; for (int i = 0; i < 25; i++) if (f.Near((int)(f.Width * (0.02 + i * 0.04)), y, 0x262930, 10)) hits++; if (hits < 23) return false; } int below = Math.Max(1, (int)(f.Height * 0.12)), same = 0; for (int i = 0; i < 25; i++) if (f.Near((int)(f.Width * (0.02 + i * 0.04)), below, 0x262930, 10)) same++; return same < 20; } public bool ClearRobloxBar() { for (int i = 0; i < 2; i++) { Check(); Win.SweepFromTop(); Wait(1500); using (var f = Capture()) if (!RobloxBarOver(f)) return true; } return false; } public bool ToggleFullscreenClearBar() { for (int i = 0; i < 2; i++) { Check(); Win.PressKey(0x7A); Wait(2500); using (var f = Capture()) if (!RobloxBarOver(f)) return true; } return false; } public Header ReadHeader(Frame f, View v) { RobloxBar = RobloxBarOver(f); if (RobloxBar) return new Header(); var h = new Header(); int a, b; double money, row = 34.5 * v.HeaderScale; string raw; int probeX = ToastProbeX(v); bool toast1 = !f.Near(probeX, v.Energy.CenterY, HEADER_BG, 14) || !GoldToastBox(f, v, v.Energy.CenterY).IsEmpty; bool toast2 = !f.Near(probeX, v.Stamina.CenterY, HEADER_BG, 14) || !GoldToastBox(f, v, v.Stamina.CenterY).IsEmpty; bool toast3 = !f.Near(probeX, v.Health.CenterY, HEADER_BG, 14) || !GoldToastBox(f, v, v.Health.CenterY).IsEmpty; int barScale = View.ScaleFor(20 * v.HeaderScale); var bar = BarRow(v, v.Energy); if (!toast1 && ReadTopBar(f, bar, barScale, 10, 0, out a, out b, out raw)) { bool full = Parse.Has(raw, "FULL"); h.Energy = full ? b : a; h.EnergyMax = barMax[0] = b; h.EnergyTick = full ? 0 : Parse.RegenTimer(raw); } bar = BarRow(v, v.Stamina); if (!toast2 && ReadTopBar(f, bar, barScale, 5, 1, out a, out b, out raw)) { bool full = Parse.Has(raw, "FULL"); h.Stamina = full ? b : a; h.StaminaMax = barMax[1] = b; h.StaminaTick = full ? 0 : Parse.RegenTimer(raw); } bar = BarRow(v, v.Health); if (!toast3 && ReadTopBar(f, bar, barScale, 10, 2, out a, out b, out raw)) { h.Health = Parse.Has(raw, "FULL") ? b : a; h.HealthMax = barMax[2] = b; } if (v.Cash != null) { h.Cash = ReadCash(f, v); string text; if (TryRead(f, BankedArea(v), View.ScaleFor(18 * v.HeaderScale), t => Parse.Money(t, out money), out text)) { Parse.Money(text, out money); h.Banked = lastBanked = BankedPoint(money, lastBanked); } } ReadPlayerPanel(f, v, h); return h; } double lastBanked = -1; internal static double BankedPoint(double read, double last) { if (last > 0 && read >= last * 5) foreach (var div in new[] { 100.0, 10.0 }) if (Math.Abs(read / div - last) <= last * 0.03) return read / div; return read; } public bool ReadHealthBar(Frame f, out int cur, out int max) { cur = max = -1; var v = ViewOf(f); if (v.Problem != null || !f.Near(ToastProbeX(v), v.Health.CenterY, HEADER_BG, 14) || !GoldToastBox(f, v, v.Health.CenterY).IsEmpty) return false; int a, b; string raw; var bar = BarRow(v, v.Health); if (!ReadTopBar(f, bar, View.ScaleFor(20 * v.HeaderScale), 10, 2, out a, out b, out raw)) return false; cur = Parse.Has(raw, "FULL") ? b : a; max = barMax[2] = b; return cur >= 0 && max > 0; } static Rectangle CashArea(View v) { double row = 34.5 * v.HeaderScale; int x = v.Cash.Box.X - 6, right = Math.Min(v.Energy.Box.X - (int)(row / 2), x + (int)(7 * row)); int y = v.Cash.Box.Bottom, split = y + (int)(1.6 * row); return new Rectangle(x, y, right - x, split - y); } static Rectangle BankedArea(View v) { var c = CashArea(v); return new Rectangle(c.X, c.Bottom, c.Width, v.Header.Bottom - c.Bottom); } public double ReadCash(Frame f, View v, bool other = false) { if (v == null || v.Problem != null || v.Cash == null) return -1; var area = CashArea(v); int scale = View.ScaleFor(26 * v.HeaderScale), lo = Math.Max(1, scale - 1); var tries = new List> { Tuple.Create(scale, Prep.None), Tuple.Create(scale + 1, Prep.None), Tuple.Create(scale, Prep.DarkText), Tuple.Create(lo, Prep.None) }; if (other) tries = new List> { tries[1], tries[3], tries[2], tries[0] }; tries.AddRange(new[] { Tuple.Create(scale + 1, Prep.DarkText), Tuple.Create(lo, Prep.Contrast), Tuple.Create(scale + 2, Prep.None), Tuple.Create(lo, Prep.DarkText) }); var reads = new List>(); for (int i = 0; i < tries.Count; i++) { if (i == 4 && (reads.Any(r => r.Item2) || !reads.Any(r => Regex.IsMatch(r.Item3.Trim(), "8\\W*$")))) break; string text = Ocr.ReadText(f, area, tries[i].Item1, tries[i].Item2); double money; if (!Parse.Money(text, out money)) continue; bool suffix = MoneySuffix.IsMatch(text); reads.Add(Tuple.Create(money, suffix, text)); if (suffix && !CommaB.IsMatch(text) && reads.Count(r => r.Item2 && Math.Abs(r.Item1 - money) <= money * 0.002) >= 2) return money; } if (reads.Count == 0) return ReadLoneMoney(f, area); return PickCash(reads); } static readonly Regex CommaB = new Regex(@"\$\s?\d{1,3},\s?\d{2}\s?B(?![A-Za-z])", RegexOptions.IgnoreCase); internal static double PickCash(List> reads) { if (reads == null || reads.Count == 0) return -1; var plain = reads.Where(r => !r.Item2).Select(r => Regex.Match(r.Item3, @"\$\s?(\d{1,3}),(\d{2})8(?![0-9,.])")).Where(m => m.Success).ToList(); var lettered = reads.Where(r => r.Item2).ToList(); if (plain.Count > 0 && lettered.Count > 0 && lettered.All(r => plain.Any(m => Regex.IsMatch(r.Item3, @"\$\s?" + m.Groups[1].Value + @",\s?" + m.Groups[2].Value + @"\s?B(?![A-Za-z])", RegexOptions.IgnoreCase)))) lettered.Clear(); var pool = lettered.Count > 0 ? lettered : reads.Where(r => !r.Item2).ToList(); return pool.GroupBy(r => r.Item1).OrderByDescending(g => g.Count()).First().Key; } static double ReadLoneMoney(Frame f, Rectangle area) { var r = Rectangle.Intersect(area, new Rectangle(0, 0, f.Width, f.Height)); int x0 = int.MaxValue, y0 = int.MaxValue, x1 = -1, y1 = -1; for (int y = r.Top; y < r.Bottom; y++) for (int x = r.Left; x < r.Right; x++) { var c = f.Pixel(x, y); if (c.G < 110 || c.G - c.B < 35 || c.G - c.R < 15) continue; x0 = Math.Min(x0, x); x1 = Math.Max(x1, x); y0 = Math.Min(y0, y); y1 = Math.Max(y1, y); } if (x1 < 0 || y1 - y0 < 6) return -1; var crop = Rectangle.Intersect(Rectangle.FromLTRB(x0 - 3, y0 - 3, x1 + 4, y1 + 4), new Rectangle(0, 0, f.Width, f.Height)); var back = f.Pixel(crop.X, crop.Y); int m = Math.Max(6, (y1 - y0) / 2); foreach (int copies in new[] { 2, 3 }) { var bmp = new Bitmap(2 * m + copies * crop.Width + (copies - 1) * 2 * m, crop.Height + 2 * m, System.Drawing.Imaging.PixelFormat.Format32bppArgb); using (var g = Graphics.FromImage(bmp)) { g.Clear(back); for (int k = 0; k < copies; k++) g.DrawImage(f.Bitmap, new Rectangle(m + k * (crop.Width + 2 * m), m, crop.Width, crop.Height), crop, GraphicsUnit.Pixel); } using (var copy = new Frame(bmp)) foreach (var t in new[] { Tuple.Create(1, Prep.None), Tuple.Create(2, Prep.None), Tuple.Create(1, Prep.DarkText), Tuple.Create(2, Prep.Contrast) }) { string text = Ocr.ReadText(copy, new Rectangle(0, 0, copy.Width, copy.Height), t.Item1, t.Item2); var found = Regex.Matches(text, @"\$\s?[0-9OoIlS]{1,3}(?:[.,][0-9OoIlS]{1,3})*\s?[KMBT]?(?![A-Za-z])", RegexOptions.IgnoreCase) .Cast().Select(x => x.Value.Replace(" ", "").ToUpperInvariant()).ToList(); double v; if (found.Count == copies && found.Distinct().Count() == 1 && Parse.Money(found[0], out v)) return v; } } return -1; } static readonly Regex MoneySuffix = new Regex(@"\$\s?[0-9OoIlS]{1,3}(?:[.,][0-9OoIlS]{1,2})?\s?[KMBT](?![A-Za-z])", RegexOptions.IgnoreCase); public double ReadCash(Frame f, bool other = false) { return ReadCash(f, ViewOf(f), other); } static int ToastProbeX(View v) { return v.ProbeX; } static string ToastText(Frame f, View v) { double row = 34.5 * v.HeaderScale; int x = ToastProbeX(v) - 8; string t = Ocr.ReadText(f, new Rectangle(x, (int)(v.Energy.CenterY - row / 2), v.Width - x, (int)row), View.ScaleFor(20 * v.HeaderScale), Prep.None); if (!string.IsNullOrWhiteSpace(t)) return t; var two = Rectangle.Intersect(new Rectangle(x, (int)(v.Energy.CenterY - row / 2), v.Width - x, (int)(2 * row)), new Rectangle(0, 0, f.Width, f.Height)); return Ocr.ReadText(f, two, View.ScaleFor(20 * v.HeaderScale), Prep.None); } static Rectangle GoldToastBox(Frame f, View v, int y) { double row = 34.5 * v.HeaderScale; int probe = ToastProbeX(v), limit = (int)(4 * row), run = (int)(2 * row); if (y < 1 || y >= f.Height - 1) return Rectangle.Empty; for (int left = Math.Min(f.Width - 2, probe + (int)(row / 3)); left > Math.Max(1, probe - (int)(2 * row)); left--) { if (!IsToastGold(f.Pixel(left, y)) || IsToastGold(f.Pixel(left - 1, y)) || IsToastGold(f.Pixel(Math.Min(f.Width - 1, left + 4), y))) continue; int top = y, bottom = y; while (top > 0 && y - top < limit && IsToastGold(f.Pixel(left, top - 1))) top--; while (bottom < f.Height - 1 && bottom - y < limit && IsToastGold(f.Pixel(left, bottom + 1))) bottom++; if (bottom - top < 0.8 * row || bottom - top >= limit) continue; int gold = 0, right = left; for (int x = left; x < Math.Min(f.Width, left + run); x++) if (IsToastGold(f.Pixel(x, top))) gold++; if (gold < run * 9 / 10) continue; for (int x = left, miss = 0; x < f.Width && miss < 3; x++) if (IsToastGold(f.Pixel(x, top))) { right = x; miss = 0; } else miss++; return Rectangle.FromLTRB(left, top, right + 1, bottom + 1); } return Rectangle.Empty; } static bool IsToastGold(Color c) { return c.R >= 150 && c.G >= 120 && c.B <= 90 && c.R - c.B >= 90 && c.R - c.G <= 60 || c.R >= 200 && EventOrange(c); } static string GoldToastText(Frame f, View v, Rectangle box) { var inside = Rectangle.FromLTRB(box.Left + 3, box.Top + 3, box.Right - 3, box.Bottom - 3); return Ocr.ReadText(f, inside, View.ScaleFor(20 * v.HeaderScale), Prep.None); } internal Rectangle ToastBox(Frame f) { var v = ViewOf(f); return v.Problem != null ? Rectangle.Empty : GoldToastBox(f, v, v.Energy.CenterY); } static Rectangle BarRow(View v, OcrLine label) { double row = 34.5 * v.HeaderScale; int x = label.Box.Right + 2; return new Rectangle(x, (int)(label.CenterY - row * 0.48), v.Width - x - 2, (int)(row * 0.96)); } void ReadPlayerPanel(Frame f, View v, Header h) { double row = 34.5 * v.HeaderScale; int right = v.Cash != null ? v.Cash.Box.X - 4 : v.Width / 2; if (right < 200) return; int s = View.ScaleFor(f, 12, 12 * v.HeaderScale); var lines = Ocr.Read(f, new Rectangle(168, 0, right - 168, v.Header.Bottom), s, Prep.None); if (!lines.Any(l => l.CenterY < row && Regex.Matches(l.Text, "[A-Za-z]").Count >= 2)) lines = Ocr.Read(f, new Rectangle(168, 0, right - 168, v.Header.Bottom), s + 1, Prep.None); var points = lines.FirstOrDefault(l => Parse.Has(l.Text, "POINTS")); if (points != null) h.SkillPoints = Math.Max(0, Parse.FirstInt(points.Text)); var words = lines.Where(l => l != points && Regex.Matches(l.Text, "[A-Za-z]").Count >= 2 && !Parse.Has(l.Text, "XP")).OrderBy(l => l.Box.Y).ToList(); var name = words.FirstOrDefault(l => l.CenterY < row); if (name != null) { string whole = name.Text.Trim(); h.Name = NameOnly(whole); h.NameBox = h.Name.Length == whole.Length ? name.Box : new Rectangle(name.Box.X, name.Box.Y, Math.Max(1, name.Box.Width * h.Name.Length / whole.Length), name.Box.Height); } var family = words.FirstOrDefault(l => name == null || l.Box.Y > name.Box.Bottom); if (family != null && family.CenterY < 2 * row) h.Family = family.Text.Trim(); int left = name != null ? name.Box.X : family != null ? family.Box.X : -1; var xpLine = points == null ? lines.FirstOrDefault(l => Parse.Has(l.Text, "XP") && Regex.IsMatch(l.Text, @"\d\s*/")) : null; int xpRow = points != null ? points.CenterY : xpLine != null ? xpLine.CenterY : family != null ? family.Box.Bottom + (int)(row * 0.6) : name != null ? name.Box.Bottom + (int)(row * 1.4) : -1; Rectangle xpArea = left > 0 && xpRow > 0 ? new Rectangle(left - 4, (int)(xpRow - row * 0.45), Math.Max(0, right - left + 4), (int)(row * 0.9)) : Rectangle.Empty; var xpTries = new[] { new { S = s + 1, P = Prep.WhiteText }, new { S = s + 2, P = Prep.None }, new { S = s, P = Prep.WhiteText }, new { S = s, P = Prep.None }, new { S = s, P = Prep.WhiteBright }, new { S = Math.Max(1, s - 1), P = Prep.WhiteText }, new { S = Math.Max(1, s - 1), P = Prep.None }, new { S = s + 1, P = Prep.WhiteBright }, new { S = s + 2, P = Prep.WhiteText }, new { S = s + 1, P = Prep.Contrast }, }; if (xpArea.Width > 0) foreach (var t in xpTries) { int a, b; if (Parse.Fraction(Ocr.ReadText(f, xpArea, t.S, t.P), out a, out b) && b > 0 && a <= b * 2) { h.Xp = a; h.XpNext = b; break; } } if (left > 0) { double side = 2.66 * row; int top = (int)Math.Max(0, (name ?? family).Box.Y - 0.12 * side - (name == null ? row : 0)); var badge = new Rectangle((int)(left - 1.2 * side), (int)(top + 0.45 * side), (int)(1.2 * side) - 2, (int)(0.62 * side)); int bs = View.ScaleFor(20 * v.HeaderScale); var seen = new List(); foreach (var t in new[] { new { S = bs, P = Prep.None }, new { S = bs + 1, P = Prep.None }, new { S = bs, P = Prep.DarkText }, new { S = bs + 2, P = Prep.None }, new { S = bs + 1, P = Prep.DarkText }, new { S = bs + 3, P = Prep.None }, new { S = Math.Max(2, bs - 1), P = Prep.None }, new { S = bs + 4, P = Prep.None }, }) { var m = Regex.Matches(Ocr.ReadText(f, badge, t.S, t.P), @"\d+"); if (m.Count == 0) continue; string d = m[m.Count - 1].Value; int lv; if (d.Length > 3) d = d.Substring(d.Length - 3); if (!int.TryParse(d, out lv) || lv <= 0) continue; if (seen.Contains(lv)) { h.Level = lv; break; } seen.Add(lv); } if (h.Level < 0) { var ink = BadgeInk(f, badge, side); int lv = ink.IsEmpty ? -1 : ReadLoneNumber(f, ink); if (lv > 0 && lv < 1000) h.Level = lv; } } if (LevelFitsXp(h.Level, h.XpNext) == false) h.Level = -1; if (h.Level < 0 && h.XpNext > 0 && h.Xp >= 0 && h.Xp < h.XpNext) { int lv = Array.IndexOf(XpToNext, h.XpNext) + 1, a, b; if (lv > 0 && Parse.Fraction(Ocr.ReadText(f, xpArea, s + 3, Prep.None), out a, out b) && b == h.XpNext) h.Level = lv; } } internal static string NameOnly(string text) { var parts = Regex.Split(text.Trim(), @"\s+"); if (parts.Length < 2) return text.Trim(); string rest = string.Join(" ", parts.Skip(1)); string name = parts[0] + string.Concat(parts.Skip(1).TakeWhile(NamePiece)); if (name.Length > parts[0].Length || Regex.IsMatch(rest, @"BUFF|BOOST|WEEKEND|\b\dX\b|\d\s*:\s*\d|%|\+\s*\d", RegexOptions.IgnoreCase)) return name; int badge; return SplitPrestige(text, out badge); } static bool NamePiece(string piece) { return Regex.IsMatch(piece, @"^[A-Za-z0-9_]{2,}$") && piece.Any(char.IsLower) && !Regex.IsMatch(piece, @"BUFF|BOOST|WEEKEND|^\dX$", RegexOptions.IgnoreCase); } public string NameAgain(Frame f) { var v = ViewOf(f); if (v.Problem != null || RobloxBarOver(f)) return null; double row = 34.5 * v.HeaderScale; int right = v.Cash != null ? v.Cash.Box.X - 4 : v.Width / 2; if (right < 200) return null; int s = View.ScaleFor(f, 12, 12 * v.HeaderScale); var panel = new Rectangle(168, 0, right - 168, v.Header.Bottom); OcrLine name = null; foreach (int z in new[] { s, s + 1 }) { name = Ocr.Read(f, panel, z, Prep.None).Where(l => !Parse.Has(l.Text, "POINTS") && !Parse.Has(l.Text, "XP") && Regex.Matches(l.Text, "[A-Za-z]").Count >= 2) .OrderBy(l => l.Box.Y).FirstOrDefault(l => l.CenterY < row); if (name != null) break; } if (name == null) return null; int h = Math.Max(8, name.Box.Height); var box = Rectangle.Intersect(Rectangle.FromLTRB(name.Box.X - h, name.Box.Y - h / 2, name.Box.Right + h, name.Box.Bottom + h / 2), new Rectangle(0, 0, f.Width, f.Height)); string again = NameOnly(Ocr.ReadText(f, box, s + 1, Prep.None).Trim()); return again.Length > 0 ? again : null; } internal static readonly int[] XpToNext = { 10, 18, 30, 45, 50, 70, 120, 155, 195, 240, 284, 333, 384, 439, 497 }; internal static bool? LevelFitsXp(int lv, int xpNext) { if (lv < 1 || lv > XpToNext.Length || xpNext <= 0) return null; return xpNext <= XpToNext[lv - 1] * 3; } static Rectangle BadgeInk(Frame f, Rectangle badge, double side) { var r = Rectangle.Intersect(badge, new Rectangle(0, 0, f.Width, f.Height)); int reach = Math.Max(3, (int)(0.22 * side)); Func gold = (x, y) => { if (x < 0 || y < 0 || x >= f.Width || y >= f.Height) return false; var c = f.Pixel(x, y); return Math.Abs(c.R - 212) <= 22 && Math.Abs(c.G - 175) <= 22 && c.B <= 95 || EventOrange(c); }; Func goldAlong = (x, y, dx, dy) => { for (int k = 1; k <= reach; k++) if (gold(x + k * dx, y + k * dy)) return true; return false; }; int x0 = int.MaxValue, y0 = int.MaxValue, x1 = -1, y1 = -1; for (int y = r.Top; y < r.Bottom; y++) for (int x = r.Left; x < r.Right; x++) { var c = f.Pixel(x, y); if ((c.R + c.G + c.B) / 3 >= 90) continue; if (!goldAlong(x, y, -1, 0) || !goldAlong(x, y, 1, 0) || !goldAlong(x, y, 0, -1) || !goldAlong(x, y, 0, 1)) continue; x0 = Math.Min(x0, x); x1 = Math.Max(x1, x); y0 = Math.Min(y0, y); y1 = Math.Max(y1, y); } if (x1 < 0 || x1 - x0 > 0.42 * side || y1 - y0 > 0.35 * side || y1 - y0 < 0.06 * side) return Rectangle.Empty; return Rectangle.FromLTRB(x0, y0, x1 + 1, y1 + 1); } static bool TryRead(Frame f, Rectangle area, int scale, Func ok, out string text) { var tries = new[] { new { S = scale, P = Prep.None }, new { S = scale + 1, P = Prep.None }, new { S = scale, P = Prep.DarkText }, new { S = Math.Max(1, scale - 1), P = Prep.None } }; foreach (var t in tries) { text = Ocr.ReadText(f, area, t.S, t.P); if (ok(text)) return true; } text = null; return false; } static bool ReadBar(Frame f, Rectangle area, int scale, int minMax, double fill, int knownMax, out int cur, out int max, out string raw) { bool lost; return ReadBar(f, area, scale, minMax, fill, knownMax, false, out cur, out max, out raw, out lost); } bool ReadTopBar(Frame f, Rectangle bar, int scale, int minMax, int i, out int cur, out int max, out string raw) { bool lost; double fill = BarFill(f, bar); barLook[i] = string.Format(System.Globalization.CultureInfo.InvariantCulture, "the bar measured {0}, its max known {1}", fill < 0 ? "nothing" : fill.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture) + " full", barMax[i] > 0 ? barMax[i].ToString() : "not yet"); if (ReadBar(f, bar, scale, minMax, fill, barMax[i], barShort[i] < 3, out cur, out max, out raw, out lost)) { barShort[i] = 0; return true; } if (lost) barShort[i]++; return false; } internal static bool DigitLost(int known, int max) { if (known <= 0 || max <= 0 || max >= known) return false; string k = known.ToString(), m = max.ToString(); if (m.Length != k.Length - 1) return false; for (int i = 0; i < k.Length; i++) if (k.Remove(i, 1) == m) return true; return false; } static bool ReadBar(Frame f, Rectangle area, int scale, int minMax, double fill, int knownMax, bool refuseLost, out int cur, out int max, out string raw, out bool lost) { lost = false; int lo = Math.Max(2, scale - 1); var left = new Rectangle(area.X, area.Y, area.Width * 45 / 100, area.Height); var tries = new[] { new { R = left, S = scale + 2, P = Prep.Contrast }, new { R = left, S = scale + 2, P = Prep.None }, new { R = left, S = scale + 1, P = Prep.Contrast }, new { R = area, S = scale, P = Prep.WhiteText }, new { R = area, S = scale, P = Prep.WhiteBright }, new { R = area, S = scale, P = Prep.WhiteSoft }, new { R = area, S = scale + 1, P = Prep.WhiteText }, new { R = area, S = scale + 1, P = Prep.WhiteBright }, new { R = area, S = lo, P = Prep.WhiteSoft }, new { R = area, S = lo, P = Prep.WhiteBright }, new { R = area, S = scale, P = Prep.None }, new { R = left, S = scale, P = Prep.Contrast }, new { R = left, S = scale, P = Prep.WhiteText }, new { R = left, S = scale, P = Prep.WhiteSoft }, new { R = left, S = scale + 1, P = Prep.WhiteBright }, }; var seen = new List>(); int maxRead = -1; string maxRaw = "", allText = ""; Func withEnd = r => Parse.Has(r, "FULL") || Parse.RegenTimer(r) >= 0 ? r : r + " " + Ocr.ReadText(f, area, scale, Prep.WhiteSoft); foreach (var t in tries) { string text = Ocr.ReadText(f, t.R, t.S, t.P); allText += " " + text; int c, m; if (!Parse.Fraction(text, out c, out m, true) || m < minMax || c > m) { var only = Regex.Match(text, @"[/|]\s*([0-9OoIlSB]{1,7})(?![0-9])"); if (maxRead < 0 && only.Success && int.TryParse(Parse.Digits(only.Groups[1].Value), out m) && m >= minMax && !(refuseLost && DigitLost(knownMax, m))) { maxRead = m; maxRaw = text; } continue; } if (refuseLost && DigitLost(knownMax, m)) { lost = true; continue; } if (maxRead < 0) { maxRead = m; maxRaw = text; } if (fill >= 0 && !FitsFill(c, m, fill)) { string d = c.ToString(); if (d.Length > 1 && d[0] == '1' && FitsFill(int.Parse(d.Substring(1)), m, fill)) c = int.Parse(d.Substring(1)); else continue; } var same = seen.FirstOrDefault(s => s.Item1 == c && s.Item2 == m); if (same != null) { cur = c; max = m; raw = withEnd(same.Item3); return true; } seen.Add(Tuple.Create(c, m, text)); } if (seen.Count > 0) { cur = seen[0].Item1; max = seen[0].Item2; raw = withEnd(seen[0].Item3); return true; } if (maxRead < 0 && fill >= 0 && knownMax > 0 && (fill >= 0.99 || Regex.IsMatch(allText, @"(?= 0 && maxRead >= minMax) { cur = (int)Math.Floor(fill * maxRead + 0.3); max = maxRead; raw = withEnd(maxRaw); return true; } cur = max = -1; raw = ""; return false; } static bool FitsFill(int cur, int max, double fill) { return fill <= 0 ? cur == 0 : Math.Abs(cur - fill * max) <= Math.Max(1.5, 0.03 * max); } static double BarFill(Frame f, Rectangle row) { int cy = row.Y + row.Height / 2, x = Math.Max(0, row.Left), end = Math.Min(f.Width, row.Right); if (cy < 0 || cy >= f.Height) return -1; while (x < end && !IsBarFill(f.Pixel(x, cy)) && !IsBarTrack(f.Pixel(x, cy))) x++; if (x + 40 >= end) return -1; int top = cy, bottom = cy, col = x + 2; while (top > row.Top && (IsBarFill(f.Pixel(col, top - 1)) || IsBarTrack(f.Pixel(col, top - 1)))) top--; while (bottom < row.Bottom && (IsBarFill(f.Pixel(col, bottom + 1)) || IsBarTrack(f.Pixel(col, bottom + 1)))) bottom++; int h = bottom - top, maxGap = Math.Max(6, row.Height / 4); if (h < 8) return -1; double a = FillOnRow(f, x - 3, end, top + (int)Math.Round(h * 0.15), maxGap), b = FillOnRow(f, x - 3, end, bottom - (int)Math.Round(h * 0.15), maxGap); return a < 0 || b < 0 || Math.Abs(a - b) > 0.02 ? -1 : (a + b) / 2; } static double FillOnRow(Frame f, int from, int end, int y, int maxGap) { int x = Math.Max(1, from); end = Math.Min(end, f.Width - 2); while (x < end && !IsBarFill(f.Pixel(x, y)) && !IsBarTrack(f.Pixel(x, y))) x++; int left = x, fillEnd = x, gap = 0; if (!FrameLeftOf(f, left, y)) return -1; bool track = false; for (; x < end && gap <= maxGap && !BarEnds(f, x, y); x++) { var c = f.Pixel(x, y); if (IsBarFill(c)) { if (track) return -1; fillEnd = x + 1; gap = 0; } else if (IsBarTrack(c)) { track = true; gap = 0; } else gap++; } if (x >= end || !BarEnds(f, x, y) || x - left < 40) return -1; if (!track && fillEnd > left && x - fillEnd <= 2) return 1; return (double)(fillEnd - left) / (x - left); } static bool FrameLeftOf(Frame f, int left, int y) { for (int k = 1; k <= 3 && left - k >= 0; k++) { var c = f.Pixel(left - k, y); if (IsBarFrame(c) || IsFrameEdge(c)) return true; if (IsHeaderBack(c) || IsBarFill(c) || IsBarTrack(c)) return false; } return false; } static bool BarEnds(Frame f, int x, int y) { if (!IsBarFrame(f.Pixel(x, y)) && !IsFrameEdge(f.Pixel(x, y))) return false; for (int k = 1; k <= 3; k++) { var c = f.Pixel(x + k, y); if (IsHeaderBack(c)) return IsHeaderBack(f.Pixel(x + k + 1, y)); if (!IsBarFrame(c) && !IsFrameEdge(c)) return false; } return false; } static bool IsFrameEdge(Color c) { int max = Math.Max(c.R, Math.Max(c.G, c.B)), min = Math.Min(c.R, Math.Min(c.G, c.B)); return max - min <= 9 && max >= 31 && max <= 50 && min >= 24 || EventFace(c); } static bool IsBarFrame(Color c) { return Math.Abs(c.R - 38) <= 6 && Math.Abs(c.G - 38) <= 6 && Math.Abs(c.B - 40) <= 6 || EventFace(c); } static bool IsHeaderBack(Color c) { return Math.Abs(c.R - 26) <= 5 && Math.Abs(c.G - 26) <= 5 && Math.Abs(c.B - 28) <= 5 || Math.Abs(c.R - 24) <= 3 && Math.Abs(c.G - 20) <= 3 && Math.Abs(c.B - 32) <= 3; } static bool IsBarFill(Color c) { int max = Math.Max(c.R, Math.Max(c.G, c.B)), min = Math.Min(c.R, Math.Min(c.G, c.B)); return max >= 120 && max - min >= 60; } static bool IsBarTrack(Color c) { return Math.Max(c.R, Math.Max(c.G, c.B)) <= 20; } public string ToastIn(Frame f) { var v = ViewOf(f); if (v.Problem != null) return null; var gold = GoldToastBox(f, v, v.Energy.CenterY); if (!gold.IsEmpty) return GoldToastText(f, v, gold); if (f.Near(ToastProbeX(v), v.Energy.CenterY, HEADER_BG, 14)) return null; return ToastText(f, v); } internal string StackedToast(Frame f, Func isMine) { var v = ViewOf(f); if (v.Problem != null || isMine == null) return null; double row = 34.5 * v.HeaderScale; int x = ToastProbeX(v) - 8, top = Math.Max(0, (int)(v.Energy.CenterY - row / 2)); var area = Rectangle.Intersect(new Rectangle(x, top, v.Width - x, (int)(5 * row)), new Rectangle(0, 0, f.Width, f.Height)); if (area.Width < 20 || area.Height < 8) return null; int s = View.ScaleFor(20 * v.HeaderScale); foreach (int scale in new[] { s, s + 1 }) { var mine = Ocr.Read(f, area, scale, Prep.None).Where(l => isMine(l.Text)).OrderBy(l => l.Box.Y).FirstOrDefault(); if (mine != null) return mine.Text; } return null; } public string ReadToast(int waitForItMs) { return ReadToast(waitForItMs, null); } public string ReadToast(int waitForItMs, Func isMine) { return ReadToast(waitForItMs, isMine, false); } public string ReadToast(int waitForItMs, Func isMine, bool stacked) { int end = Environment.TickCount + waitForItMs; do { using (var f = Capture()) { string t = ToastIn(f); if (!string.IsNullOrWhiteSpace(t) && (isMine == null || isMine(t))) return t; if (stacked && isMine != null && t != null) { string under = StackedToast(f, isMine); if (under != null) return under; } } Wait(150); } while (end - Environment.TickCount > 0); return null; } public bool OnTab(Frame f, Tab t) { var v = ViewOf(f); return v.Problem == null && v.IsSelected(f, t); } public Tab? SelectedTab(Frame f) { var v = ViewOf(f); return v.Problem == null ? v.Selected(f) : null; } public bool OpenTab(Tab t) { return OpenTab(t, false); } public bool OpenTab(Tab t, bool toBuy) { bool shop = toBuy && (t == Tab.Shop || t == Tab.Properties || t == Tab.BlackMarket); if (!shop) Guard(View.TabLabels[t]); if (NotInMenu(t)) return false; int rolls = 0, rolled = 0; Dictionary before = null; bool chatTried = false, lookedAgain = false, foldTried = false; int pressedNoChange = 0; OcrLine fold = null; int scan = 0; Dictionary scanFrom = null; bool topSeen = false, bottomSeen = false; for (int attempt = 0; attempt < 4; attempt++) { View v, cached = view; View.Place place; bool roll = false; using (var f = Capture()) { v = ViewOf(f); if (v.Problem != null) { bool cleared = ClearPopupIfAny(f); ForgetView(); if (!cleared) Wait(1000); continue; } if (v == cached && !v.ReadMenu(f)) { ForgetView(); Wait(1000); continue; } if (v.IsSelected(f, t)) return TabOpen(); if (!foldTried && v.EventsOpen && v.EventsBar != null && !v.Tabs.ContainsKey(t)) { foldTried = true; fold = v.EventsBar; } place = v.Find(f, t); if (place.Label != null && MenuDimmed(f, v)) { var w = ReadDistrictWindow(f); if (w != null) { CloseDistrict(w); ForgetView(); continue; } } else if (place.Label != null && IsLockedEntry(f, v, place.Label)) { MarkNotInMenu(t, null); return false; } if (place.Spot == View.Spot.Up || place.Spot == View.Spot.Down) { int dir = place.Spot == View.Spot.Down ? 1 : -1; var now = v.MenuSignature(); if (rolled == dir && View.SameMenu(before, now)) place = v.AtEnd(f, t, dir); else { roll = true; before = now; rolled = dir; } } if (place.Spot == View.Spot.Absent) { var now = v.MenuSignature(); if (scan != 0 && View.SameMenu(scanFrom, now)) { if (scan < 0) topSeen = true; else bottomSeen = true; } int dir = !topSeen ? -1 : !bottomSeen ? 1 : 0; if (dir != 0) { roll = true; scan = dir; scanFrom = now; rolled = dir; before = null; } } } if (fold != null) { Press(fold.Text, fold.CenterX, fold.CenterY, 700); fold = null; ForgetView(); rolled = 0; before = null; attempt--; continue; } if (roll) { if (!chatTried) { chatTried = true; using (var f = Capture()) if (HideChat(f)) { ForgetView(); rolled = 0; before = null; attempt--; continue; } } if (rolls++ >= 12) break; Scroll(v.Menu.X + v.Menu.Width / 2, v.Menu.Y + v.Menu.Height / 2, rolled > 0 ? -5 : 5); ForgetView(); attempt--; continue; } if (place.Spot == View.Spot.Absent || place.Spot == View.Spot.Hidden) { if (!chatTried) { chatTried = true; using (var f = Capture()) if (HideChat(f)) { ForgetView(); continue; } } if (!lookedAgain) { lookedAgain = true; Wait(700); ForgetView(); continue; } if (place.Spot == View.Spot.Absent) { MarkNotInMenu(t, topSeen && bottomSeen ? "not found from the top of the menu to its bottom" : place.Why); return false; } log("Can't read " + View.TabLabels[t] + " in the game's menu (" + place.Why + ") - not pressing anything there"); Snapshot("menu entry unreadable " + t, 60); return false; } var label = place.Label; if (label.CenterY < ChatZoneBottom || attempt > 0) using (var f = Capture()) if (HideChat(f)) { ForgetView(); continue; } bool slow = pressedNoChange >= 2 || DateTime.UtcNow < slowUntil; if (shop) PressShopEntry(label, t, slow); else if (slow) PressSlow(label.Text, label.CenterX, label.CenterY, 650); else Press(label.Text, label.CenterX, label.CenterY, 650); using (var f = Capture()) { if (OnTab(f, t)) { if (pressedNoChange >= 2) SlowPressWorked(t); return TabOpen(); } if (ClearPopupIfAny(f)) continue; var dw = ReadDistrictWindow(f); if (dw != null) { CloseDistrict(dw); ForgetView(); continue; } } if (++pressedNoChange == 2) NoteFrontWindow(t); Win.Park(); Wait(1500); ForgetView(); } string language = null; using (var f = Capture()) { if (OnTab(f, t)) return TabOpen(); language = ViewOf(f).Language; } if (language != null) { log("Could not open " + t + ": " + View.InLanguage(language)); Snapshot("game in " + language, 60); return false; } if (!openRetrying) { openRetrying = true; try { Wait(3000); ForgetView(); return OpenTab(t, toBuy); } finally { openRetrying = false; } } log("Could not open " + t + " - is something covering the game?"); if (TabFails++ == 0) TabFailsSince = DateTime.UtcNow; Snapshot("could not open " + t, 60); return false; } bool openRetrying; DateTime frontSaid = DateTime.MinValue; void NoteFrontWindow(Tab t) { if (DateTime.UtcNow - frontSaid < TimeSpan.FromMinutes(30)) return; frontSaid = DateTime.UtcNow; string front; try { front = Win.FrontWindow(); } catch (Exception e) { front = "unknown (" + e.GetType().Name + ")"; } log("Pressed " + View.TabLabels[t] + " twice and the game didn't change - the window in front: " + front); } DateTime slowUntil = DateTime.MinValue, slowSaid = DateTime.MinValue; void SlowPressWorked(Tab t) { slowUntil = DateTime.UtcNow.AddMinutes(30); if (DateTime.UtcNow - slowSaid < TimeSpan.FromMinutes(30)) return; slowSaid = DateTime.UtcNow; log("Opened " + View.TabLabels[t] + " with a slower press (the quick one did nothing twice) - menu presses go slower for 30 minutes"); Snapshot("tab opened by a slow press", 0, true, false); } void PressSlow(string text, int x, int y, int waitMs) { Guard(text); if (OnlyNavigate && !NavigationPress(text, x, y)) RefuseInCheck(text); Check(); NoteLateFrame(); Win.SlowClick(x, y); Wait(waitMs + (FrameLag() > 0 ? 800 : 0)); } bool TabOpen() { tabOpenedAt = DateTime.UtcNow; TabFails = 0; return true; } public int TabFails { get; private set; } public DateTime TabFailsSince { get; private set; } readonly Dictionary notInMenu = new Dictionary(); readonly HashSet saidNotInMenu = new HashSet(); public bool NotInMenu(Tab t) { DateTime until; return notInMenu.TryGetValue(t, out until) && DateTime.UtcNow < until; } void MarkNotInMenu(Tab t, string why) { notInMenu[t] = DateTime.UtcNow.AddHours(1); if (!saidNotInMenu.Add(t)) return; if (why == null) { log(View.TabLabels[t] + " is locked in the game's menu: it opens at a higher level. The bot looks again after each level-up."); return; } log(View.TabLabels[t] + " isn't in the game's menu (" + why + "). Maybe not open at your level yet. The bot looks again every hour and after a level-up."); Snapshot("not in the menu " + t, 0, true); } internal static bool IsLockedEntry(Frame f, View v, OcrLine label) { int ink = 0; var box = Rectangle.Intersect(label.Box, new Rectangle(0, 0, f.Width, f.Height)); for (int y = box.Top; y < box.Bottom; y++) for (int x = box.Left; x < box.Right; x++) { var c = f.Pixel(x, y); ink = Math.Max(ink, (c.R + c.G + c.B) / 3); } if (ink >= 150) return false; int x0 = v.Menu.Right - (int)(v.Menu.Width * 0.22), x1 = Math.Min(f.Width, v.Menu.Right - 2); int grey = 0, gold = 0, n = 0; for (int y = Math.Max(0, label.CenterY - label.Box.Height); y <= Math.Min(f.Height - 1, label.CenterY + label.Box.Height); y++) for (int x = Math.Max(0, x0); x < x1; x++) { var c = f.Pixel(x, y); n++; int mx = Math.Max(c.R, Math.Max(c.G, c.B)), mn = Math.Min(c.R, Math.Min(c.G, c.B)); if (c.R > 150 && c.R - c.B > 80) gold++; else if (mx - mn < 25 && mx >= 90 && mx < 200) grey++; } return n > 0 && grey * 100 >= 3 * n && gold * 10 < n; } internal static bool MenuDimmed(Frame f, View v) { if (v.Tabs.Count < 3) return false; foreach (var l in v.Tabs.Values) { var box = Rectangle.Intersect(l.Box, new Rectangle(0, 0, f.Width, f.Height)); for (int y = box.Top; y < box.Bottom; y++) for (int x = box.Left; x < box.Right; x++) { var c = f.Pixel(x, y); if ((c.R + c.G + c.B) / 3 >= 150) return false; } } return true; } public void MenuChanged() { notInMenu.Clear(); } public void MenuChanged(Tab t) { notInMenu.Remove(t); } public string KnownMax { get { return string.Join(",", barMax); } } public void SeedMax(string kept) { var parts = (kept ?? "").Split(','); int m; for (int i = 0; i < 3 && i < parts.Length; i++) if (barMax[i] <= 0 && int.TryParse(parts[i].Trim(), out m) && m > 0) barMax[i] = m; } public void ResetAccount(string from, string to) { notInMenu.Clear(); saidNotInMenu.Clear(); jobScroll.Clear(); if (!string.IsNullOrEmpty(from)) maxOf[from] = (int[])barMax.Clone(); int[] known; if (!string.IsNullOrEmpty(to) && maxOf.TryGetValue(to, out known)) known.CopyTo(barMax, 0); else barMax[0] = barMax[1] = barMax[2] = -1; barShort[0] = barShort[1] = barShort[2] = 0; PointsCost = 1; cratesLeft.Clear(); } const int ChatZoneBottom = 420; bool chatHidden; DateTime chatPressed = DateTime.MinValue; public bool ChatShows(Frame f) { if (f.Width > 476 && f.Height > 410 && f.FillRatio(16, 476, 410, 0x335FFF, 30) >= 0.6) return true; var lines = Ocr.Read(f, new Rectangle(0, 50, Math.Min(520, f.Width / 2), Math.Min(ChatZoneBottom, f.Height) - 50), 2, Prep.None); return lines.Any(l => { string k = Parse.Key(l.Text); return (l.CenterY < 150 && (k.EndsWith("GIOBAI") || k == "FRIENDS")) || k.Contains("TOCHATCIICK") || k.Contains("SAYHITO") || k.Contains("JOINEDTHE") || k.Contains("UNIOCKCHAT") || k.Contains("AGECHECK"); }); } public bool HideChat(Frame f) { if ((DateTime.UtcNow - chatPressed).TotalSeconds < 5 || !ChatShows(f)) return false; chatPressed = DateTime.UtcNow; if (!chatHidden) log("Hiding Roblox's chat window: it covers the top of the menu. The speech bubble at the top left brings it back."); chatHidden = true; Click(138, 34, 700); Win.Park(); Wait(400); return true; } static readonly HashSet SafeCloseKeys = Keys("DONE", "OK", "OKAY", "CONTINUE", "CLOSE", "COLLECT", "CLAIM", "NICE", "AWESOME", "GOT IT", "COOL"); static HashSet Keys(params string[] words) { var set = new HashSet(); foreach (var w in words) set.Add(Parse.Key(w)); return set; } public bool ClearPopup(Frame f) { if (CloseEventPopup(f)) return true; var lines = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None); CheckForPurchasePrompt(lines); foreach (var l in lines) { if (!SafeCloseKeys.Contains(Parse.Key(l.Text))) continue; log("Closing a popup with \"" + l.Text + "\""); Snapshot("popup closed with " + l.Text, 0, false, false); Press(l, 800); return true; } return false; } internal static Point EventPopupX(Frame f) { var area = new Rectangle(f.Width * 25 / 100, f.Height * 15 / 100, f.Width * 50 / 100, f.Height * 70 / 100); foreach (int s in new[] { 1, 2 }) { var title = Ocr.Read(f, area, s, Prep.None).FirstOrDefault(l => View.Distance(Parse.Key(l.Text), "JOINEVENT") <= 2); if (title == null) continue; int h = Math.Max(6, title.Box.Height), right = title.Box.X - h / 2, left = Math.Max(0, title.Box.X - 3 * Math.Max(title.Box.Width, 4 * h)); int x0 = -1, x1 = -1, y0 = int.MaxValue, y1 = -1; for (int x = right; x >= left; x--) { bool any = false; for (int y = Math.Max(0, title.CenterY - h); y <= Math.Min(f.Height - 1, title.CenterY + h); y++) { var c = f.Pixel(x, y); if (Math.Min(c.R, Math.Min(c.G, c.B)) < 190) continue; any = true; y0 = Math.Min(y0, y); y1 = Math.Max(y1, y); } if (any) { if (x1 < 0) x1 = x; x0 = x; } else if (x1 >= 0 && x1 - x > h / 3) break; } if (x1 < 0 || x1 - x0 > 2 * h) return Point.Empty; return new Point((x0 + x1) / 2, (y0 + y1) / 2); } return Point.Empty; } public bool CloseEventPopup(Frame f) { var x = EventPopupX(f); if (x.IsEmpty) return false; log("Closing Roblox's \"Join Event\" window with its X - it was over the game"); Snapshot("roblox join event window", 0, true, false); Click(x.X, x.Y, 800); Win.Park(); return true; } internal static OcrLine NotifyPromptCancel(Frame f) { var area = PopupArea(f); string titleKey = Parse.Key("Turn on Notifications"), cancelKey = Parse.Key("Cancel"), yesKey = Parse.Key("Yes"); foreach (int s in new[] { 1, 2 }) { var lines = Ocr.Read(f, area, s, Prep.None); var title = lines.FirstOrDefault(l => View.Distance(Parse.Key(l.Text), titleKey) <= 3); if (title == null) continue; int h = Math.Max(6, title.Box.Height); var cancel = lines.FirstOrDefault(l => Parse.Key(l.Text) == cancelKey && l.CenterY > title.Box.Bottom && l.CenterY < title.CenterY + 12 * h && l.CenterX < title.CenterX && l.CenterX > title.CenterX - title.Box.Width); if (cancel == null) continue; if (lines.Any(l => Parse.Key(l.Text) == yesKey && Math.Abs(l.CenterY - cancel.CenterY) < h && l.CenterX > title.CenterX && l.CenterX < title.CenterX + title.Box.Width)) return cancel; } return null; } public bool CloseNotifyPrompt(Frame f) { var cancel = NotifyPromptCancel(f); if (cancel == null) return false; log("Closing Roblox's \"Turn on Notifications\" window with Cancel - it dimmed the game, so the bars didn't read"); Snapshot("roblox turn on notifications window", 0, true, false); Click(cancel.CenterX, cancel.CenterY, 800); Win.Park(); return true; } static readonly string[] InviteTitles = { Parse.Key("Invite from"), Parse.Key("Invitacion de"), Parse.Key("Invitación de"), Parse.Key("Invitation de"), Parse.Key("Convite de"), Parse.Key("Einladung von") }; internal static Point InviteIgnore(Frame f) { var area = new Rectangle(f.Width / 2, 0, f.Width - f.Width / 2, Math.Max(160, f.Height / 4)); foreach (int s in new[] { 1, 2, 3 }) { var title = Ocr.Read(f, area, s, Prep.None).FirstOrDefault(l => InviteTitles.Any(t => { string k = Parse.Key(l.Text); return k.Length >= t.Length && View.Distance(k.Substring(0, t.Length), t) <= 2; })); if (title == null) continue; int h = Math.Max(6, title.Box.Height); int x0 = int.MaxValue, x1 = -1, y0 = int.MaxValue, y1 = -1; for (int y = title.Box.Bottom; y < Math.Min(f.Height, title.CenterY + 12 * h); y++) for (int x = area.Left; x < area.Right; x++) { var c = f.Pixel(x, y); if (c.B < 200 || c.R > 90 || c.G < 70 || c.G > 130) continue; x0 = Math.Min(x0, x); x1 = Math.Max(x1, x); y0 = Math.Min(y0, y); y1 = Math.Max(y1, y); } int w = x1 - x0 + 1; if (x1 < 0 || w < 8 * h || y1 - y0 + 1 < 2 * h) return Point.Empty; return new Point(x0 - w / 2, (y0 + y1) / 2); } return Point.Empty; } public bool CloseInvite(Frame f) { var ignore = InviteIgnore(f); if (ignore.IsEmpty) return false; log("Closing Roblox's invite to another game with Ignore - it was over the game"); Snapshot("roblox invite window", 0, true, false); Click(ignore.X, ignore.Y, 800); Win.Park(); return true; } internal static Rectangle PopupArea(Frame f) { int top = Math.Min(f.Height * 11 / 100, f.Width * 88 / 1000); return new Rectangle(f.Width * 20 / 100, top, f.Width * 61 / 100, f.Height * 11 / 100 + f.Height * 87 / 100 - top); } internal static Rectangle BigPopupArea(Frame f) { var a = PopupArea(f); int half = Math.Min(f.Width * 46 / 100, f.Height * 585 / 1009); if (half <= a.Width / 2) return a; return new Rectangle(f.Width / 2 - half, a.Y, 2 * half, a.Height); } internal static int PopupScale(Frame f) { return View.ScaleFor(16.0 * Math.Min(f.Height / 1009.0, f.Width / 1790.0)); } internal static List JoinUpgrade(List lines) { if (!Bridge.On) return lines; var all = new List(lines); foreach (var a in lines.Where(l => Parse.Key(l.Text) == "UPGRADE")) { int h = Math.Max(8, a.Box.Height); var b = all.Where(l => l != a && l.Box.X >= a.Box.Right - h / 2 && l.Box.X - a.Box.Right < 3 * h && Math.Abs(l.CenterY - a.CenterY) <= h && Regex.IsMatch(l.Text, @"^\s*\$\s?[0-9OoIlS]")).OrderBy(l => l.Box.X).FirstOrDefault(); if (b == null || !all.Contains(a)) continue; all.Remove(a); all.Remove(b); all.Add(new OcrLine { Text = a.Text.Trim() + " " + b.Text.Trim(), Box = Rectangle.Union(a.Box, b.Box) }); } return all; } internal static List JoinWrapped(List lines, Func first, Func second) { var all = new List(lines); foreach (var a in lines.Where(l => first(l.Text))) { int h = Math.Max(8, a.Box.Height); var b = all.Where(l => l != a && l.Box.Y >= a.Box.Bottom - h / 3 && l.Box.Y - a.Box.Bottom < h && l.Box.X < a.Box.Right && l.Box.Right > a.Box.X && second(l.Text)) .OrderBy(l => l.Box.Y).FirstOrDefault(); if (b == null || !all.Contains(a)) continue; all.Remove(a); all.Remove(b); all.Add(new OcrLine { Text = a.Text.Trim() + " " + b.Text.Trim(), Box = Rectangle.Union(a.Box, b.Box) }); } return all; } internal void CheckForPurchasePrompt(List lines, bool shopInSight = false) { if (DisconnectIn(null, lines) != null) throw new WindowChangedException(Disconnected); bool robux = false, buy = false, dialog = false, shopPage = shopInSight, buyButton = false, cancel = false; foreach (var l in lines) { string k = Parse.Key(l.Text); if (k.Contains("ROBUX") || l.Text.Contains("R$")) robux = true; if (k.StartsWith("BUY") || k.Contains("PURCHASE")) buy = true; if (k == "BUY" || k == "BUYNOW") buyButton = true; if (k == "CANCEI") cancel = true; if (k.Contains("WOUIDYOUIIKE") || k.Contains("WANTTOBUY") || k.Contains("YOURBAIANCE") || k.Contains("BAIANCEAFTER") || k == "BUYITEM" || k.Contains("CONFIRMPURCHASE") || k.Contains("BUYROBUX") || k.Contains("INSUFFICIENTROBUX") || k.Contains("ENOUGHROBUX")) dialog = true; if (k.Contains("ROBUXSH") || k.Contains("MENTCASH") || k.Contains("GAMEPASSES") || k.Contains("NEWSTOCKIN")) shopPage = true; } if (dialog || (!shopPage && (buyButton && cancel || robux && buy))) { Snapshot("robux purchase window"); throw new NeedUserException("A Robux purchase window seems to be open. The bot stopped and won't touch it - close it yourself."); } } public void CheckForPurchaseWindow() { using (var f = Capture()) CheckForPurchasePrompt(Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None)); } public void JobsTop() { string last = null; for (int batch = 0; batch < 4; batch++) { ScrollPage(0.54, 90); using (var f = Capture()) { var lines = Ocr.Read(f, JobsArea(f), 2, Prep.None); if (lines.Any(l => GameData.CityIndex(l.Text) == 0)) return; string sig = string.Join(",", lines.Select(l => l.Text + "@" + l.Box.Y)); if (sig == last) return; last = sig; } } } Rectangle JobsArea(Frame f) { var v = ViewOf(f); return v.Problem == null ? v.Page : new Rectangle(300, 205, 1560, 785); } public List ReadJobCards(Frame f, List catalog, bool readMastery) { return CardsFromLines(f, Ocr.Read(f, JobsArea(f), 2, Prep.None), catalog, readMastery); } static bool IsStats(string t) { return t.Contains("$") || Parse.Has(t, "XP") || Parse.Has(t, "ITEM"); } List CardsFromLines(Frame f, List lines, List catalog, bool readMastery) { var area = JobsArea(f); var cards = new List(); foreach (var l in lines) { string t = l.Text; if (IsStats(t) || Parse.Has(t, "TIER") || Parse.Has(t, "MASTERY") || Parse.Key(t) == "DOJOB" || GameData.CityIndex(t) >= 0 || Regex.Matches(t, "[A-Za-z]").Count < 6) continue; double u = Math.Max(10, l.Box.Height); var stats = lines.FirstOrDefault(s => s != l && IsStats(s.Text) && s.Box.Y > l.Box.Y + 0.8 * u && s.Box.Y < l.Box.Y + 2.2 * u && Math.Abs(s.Box.X - l.Box.X) < 5 * u); if (stats == null) continue; if (l.Box.Y < area.Top + 0.07 * area.Height) continue; if (l.Box.Y + 2.4 * u > area.Bottom) continue; var card = new JobCard { Name = t.Trim(), NameBox = l.Box }; ParseJobStats(StatsRow(lines, stats, l), card); if (card.Cost < 0) card.Cost = EnergyChip(f, stats, l); card.Info = CardJob(t, card.Cost, card.Xp, catalog); var button = lines.Where(b => b.Box.X > l.Box.Right && b.CenterY > l.Box.Y && b.CenterY < l.Box.Y + 2.6 * u && (Parse.Key(b.Text) == "DOJOB" || Parse.Has(b.Text, "LEVEL") || Parse.Has(b.Text, "LOCKED"))) .OrderBy(b => b.Box.X).LastOrDefault(); if (button != null) { card.ButtonText = button.Text; card.ButtonAt = new Point(button.CenterX, button.CenterY); card.StateAt = new Point(button.Box.X - (Bridge.On ? 2 * button.Box.Height : Math.Max(4, button.Box.Height / 2)), button.CenterY); } else { card.ButtonAt = new Point(area.Right - (int)(area.Width * 0.16), stats.CenterY - (int)(0.2 * u)); card.StateAt = new Point(card.ButtonAt.X - (int)(3 * u), card.ButtonAt.Y); } card.Button = Parse.Has(card.ButtonText, "LEVEL") || Parse.Has(card.ButtonText, "LOCKED") ? ButtonState.Locked : GoldIsh(f, card.StateAt.X, card.StateAt.Y) ? ButtonState.Ready : IsDim(f, card.StateAt.X, card.StateAt.Y) ? ButtonState.Dim : ButtonState.Locked; var label = lines.FirstOrDefault(m => Parse.Has(m.Text, "MASTERY") && m.Box.Y > stats.Box.Y && m.Box.Y < l.Box.Y + 3.8 * u); card.MasteryBar = MasteryBarOf(f, card, label, stats, u); if (readMastery) card.Mastery = ReadMasteryText(f, card); cards.Add(card); } return cards; } static Rectangle MasteryBarOf(Frame f, JobCard c, OcrLine label, OcrLine stats, double u) { var found = BarByPixels(f, c.NameBox.X, stats.Box.Bottom, u, c.ButtonAt.X > 0 ? c.ButtonAt.X : f.Width); if (!found.IsEmpty) return found; int left = c.NameBox.X, top = c.NameBox.Y + (int)(2.45 * u), height = (int)Math.Max(8, 0.9 * u); if (label != null) return new Rectangle(left, label.CenterY - height / 2, Math.Max(10, 2 * (label.CenterX - left)), height); int right = c.ButtonAt.X > 0 ? left + (int)(0.55 * (c.ButtonAt.X + 3 * u - left)) : left + (int)(20 * u); c.BarGuessed = true; return new Rectangle(left, top, right - left, height); } static Rectangle BarByPixels(Frame f, int left, int fromY, double u, int maxRight) { int x = left + 6, runTop = -1; int minH = (int)Math.Max(6, 0.4 * u), maxH = (int)(2 * u), end = Math.Min(f.Height - 1, fromY + (int)(3 * u)); for (int y = Math.Max(0, fromY); y <= end; y++) { bool bar = y < end && IsBarPixel(f.Pixel(x, y)); if (bar) { if (runTop < 0) runTop = y; continue; } if (runTop < 0) continue; int h = y - runTop; if (h >= minH && h <= maxH) { int row = runTop + 1, l = x, r = x, miss = 0; while (l > 1 && IsBarPixel(f.Pixel(l - 1, row))) l--; for (int xx = x; xx < f.Width - 1 && miss < 4; xx++) if (IsBarPixel(f.Pixel(xx, row))) { r = xx; miss = 0; } else miss++; if (r - l >= 10 * u && Math.Abs(l - left) <= u && r < maxRight) return new Rectangle(l, runTop, r - l + 1, h); } runTop = -1; } return Rectangle.Empty; } static bool IsBarPixel(Color c) { int max = Math.Max(c.R, Math.Max(c.G, c.B)); return (c.R >= 150 && c.R - c.B >= 100) || max <= 18 || CurseGreen(c); } public static string ReadMasteryText(Frame f, JobCard c) { var bar = c.MasteryBar; bar.Inflate(0, 3); string first = null; foreach (var p in new[] { Prep.None, Prep.GreyText }) foreach (int scale in p == Prep.None ? new[] { 4 } : new[] { 4, 3 }) { string t = Ocr.ReadText(f, bar, scale, p); if (first == null) first = t; if (IsMasteryLabel(t)) return t; } return first; } internal static bool IsMasteryLabel(string t) { if (string.IsNullOrEmpty(t)) return false; int a, b; if (Parse.Fraction(t, out a, out b) && (b == 25 || b == 50 || b == 100) && a <= b) return true; return GoldWord(t) && !Regex.IsMatch(Regex.Replace(t, @"\+?\s*[0-9]+\s*%", ""), "[0-9]"); } internal static bool GoldWord(string t) { return Regex.IsMatch(Parse.Key(t), "[GC6]OID"); } static string StatsRow(List lines, OcrLine stats, OcrLine name) { if (!Bridge.On) return stats.Text; double u = Math.Max(10, name.Box.Height); return string.Join(" ", lines.Where(s => Math.Abs(s.CenterY - stats.CenterY) < 0.7 * u && s.Box.X > name.Box.X - 3 * u && s.Box.Right < stats.Box.Right + 20 * u) .OrderBy(s => s.Box.X).Select(s => s.Text)); } static int EnergyChip(Frame f, OcrLine stats, OcrLine name) { var row = stats; int h = Math.Max(10, row.Box.Height); int dollar = row.Text.IndexOf('$'); int right = dollar >= 0 ? row.Box.X + dollar * row.Box.Width / Math.Max(1, row.Text.Length) - 2 : row.Box.X - 2; var spot = Rectangle.FromLTRB(name.Box.X, row.Box.Y - 2, right, row.Box.Bottom + 2); if (spot.Width < h) return -1; foreach (var p in new[] { Prep.None, Prep.Contrast }) { var m = Regex.Match(Ocr.ReadText(f, spot, 4, p), @"([0-9]{1,3})\s*$"); int v; if (m.Success && int.TryParse(m.Groups[1].Value, out v) && v > 0 && v < 1000) return v; } return -1; } static void ParseJobStats(string s, JobCard c) { int dollar = s.IndexOf('$'); int v; if (dollar > 0) { string d = Parse.Digits(s.Substring(0, dollar)); if (d.Length >= 2 && d[0] == '0') d = d.Substring(1); if (int.TryParse(d, out v) && v > 0 && v < 1000) c.Cost = v; } var m = Regex.Match(s, @"([0-9OoIl,]+)\s*[xX][pP]"); if (m.Success && int.TryParse(Parse.Digits(m.Groups[1].Value), out v)) c.Xp = v; } public static JobInfo MatchJob(string text, List catalog) { return MatchJob(text, -1, -1, catalog); } internal static JobInfo CardJob(string name, int cost, int xp, List catalog) { var j = MatchJob(name, cost, -1, catalog); if (j != null || cost <= 0 || xp <= 0) return j; var same = catalog.Where(x => x.Cost == cost && x.Xp == xp).ToList(); return same.Count == 1 ? same[0] : null; } public static JobInfo MatchJob(string text, int cost, int cityAtLeast, List catalog) { string k = Parse.Key(text); if (k.Length < 6) return null; var near = catalog.Where(j => cityAtLeast < 0 || GameData.CityIndex(j.City) >= cityAtLeast) .Select(j => new { J = j, D = Levenshtein(k, Parse.Key(j.Name)) }) .Where(x => x.D <= Math.Max(2, x.J.Name.Length / 7)).OrderBy(x => x.D); foreach (var x in near) if (x.D == 0 || !CostOff(cost, x.J.Cost)) return x.J; return null; } internal static bool CostOff(int read, int known) { return read > 0 && known > 0 && Math.Abs(read - known) * 10 > known * 3; } static int Levenshtein(string a, string b) { return View.Distance(a, b); } public static void ApplyMastery(JobInfo j, string text, Frame f = null, JobCard card = null) { if (j == null) return; if (string.IsNullOrEmpty(text)) { CountFromBar(j, f, card); return; } string u = text.ToUpperInvariant(); int a, b; bool count = Parse.Fraction(text, out a, out b); if (count && b == 10 && a > 10 && a < 100) b = 100; if (count && (b == 25 || b == 50 || b == 100) && a <= b) { int rank = b == 25 ? 0 : b == 50 ? 1 : 2; if (f != null && card != null && !card.BarGuessed) { int fromBar = MasteryFromBar(f, card.MasteryBar, b); if (fromBar > 0 && Math.Abs(fromBar - a) > 2) a = fromBar; } if (j.MasteryRank >= 3 && !(b == 100 && a >= 100) && (f == null || card == null || card.BarGuessed || MasteryFromBar(f, card.MasteryBar, 100) >= 97)) return; if (b == 100 && a >= 100) { j.MasteryRank = 3; j.MasteryCur = j.MasteryGoal = -1; return; } if (rank == j.MasteryRank && b == j.MasteryGoal && a < j.MasteryCur) a = j.MasteryCur; j.MasteryRank = rank; j.MasteryCur = a; j.MasteryGoal = b; return; } if (GoldWord(text) && !u.Contains("SILVER") && !u.Contains("BRONZE") && (f == null || card == null || (card.BarGuessed ? Regex.IsMatch(Parse.Key(text), "[GC6]OIDMASTERY") : MasteryFromBar(f, card.MasteryBar, 100) >= 97))) { j.MasteryRank = 3; j.MasteryCur = j.MasteryGoal = -1; return; } CountFromBar(j, f, card); } static void CountFromBar(JobInfo j, Frame f, JobCard card) { if (f == null || card == null || card.BarGuessed || j.MasteryRank < 0 || j.MasteryRank >= 3 || j.MasteryGoal <= 0) return; int fromBar = MasteryFromBar(f, card.MasteryBar, j.MasteryGoal); if (fromBar > j.MasteryCur && fromBar < j.MasteryGoal) j.MasteryCur = fromBar; } internal static string MasteryWording(string label) { return string.Join(" ", Regex.Matches(label ?? "", "[A-Za-z]{3,}").Cast().Select(m => m.Value.ToUpperInvariant())); } static int MasteryFromBar(Frame f, Rectangle bar, int goal) { if (bar.Width < 20) return -1; double best = 0; foreach (int y in new[] { bar.Top + 1, bar.Bottom - 2 }) { int hit = 0, n = 0; for (int x = bar.Left + 3; x <= bar.Right - 3; x += 3) { var c = f.Pixel(x, y); n++; if (c.R >= 100 && c.R - c.B >= 60) hit++; } if (n > 0) best = Math.Max(best, (double)hit / n); } return (int)Math.Round(best * goal); } public List ReadCityHeaders(Frame f, List lines = null) { var area = JobsArea(f); var result = new List(); foreach (var l in lines ?? Ocr.Read(f, area, 2, Prep.None)) { int ci = GameData.CityIndex(l.Text); if (ci < 0) continue; string k = Parse.Key(l.Text); int cut = k.IndexOf("IEVEI"); if (cut > 0) k = k.Substring(0, cut); if (k.Length > Parse.Key(GameData.Cities[ci]).Length + 3) continue; int y = l.CenterY, bestStart = -1, bestLen = 0, runStart = -1; for (int x = area.Right - 2; x > l.Box.Right + 40; x--) { if (GoldIsh(f, x, y)) { if (runStart < 0) runStart = x; } else if (runStart >= 0) { int len = runStart - x; if (len > bestLen) { bestLen = len; bestStart = runStart; } runStart = -1; } } if (bestLen < 6) continue; int cx = bestStart - bestLen / 2; int tall = 0; for (int dx = -1; dx <= 1; dx++) { int up = 0, down = 0; while (up < 3 * bestLen && GoldIsh(f, cx + dx, y - up - 1)) up++; while (down < 3 * bestLen && GoldIsh(f, cx + dx, y + down + 1)) down++; tall = Math.Max(tall, up + down + 1); } bool plus = tall >= Math.Max(6, bestLen * 0.6); result.Add(new CityHeader { Index = ci, Y = l.Box.Y, Collapsed = plus, ButtonAt = new Point(cx, y) }); } return result; } public bool ExpandCity(int cityIndex) { return ExpandCities(new[] { cityIndex }).Contains(cityIndex); } public HashSet ExpandCities(IEnumerable cities) { return SetCities(cities, new int[0]); } public HashSet SetCities(IEnumerable open, IEnumerable fold) { var close = new HashSet(fold); var want = new HashSet(open.Where(c => !close.Contains(c))); var seen = new HashSet(); if (want.Count + close.Count == 0) return seen; int lastWanted = want.Concat(close).Max(); JobsTop(); MeasureNotch(new List()); string lastSeen = null; int notches = ListNotches(6), clicks = 0; for (int page = 0, pages = 150 / notches + 1; page < pages; page++) { List heads; Rectangle area; string sig; using (var f = Capture()) { area = JobsArea(f); var lines = Ocr.Read(f, area, 2, Prep.None); heads = ReadCityHeaders(f, lines); sig = string.Join(",", lines.Select(x => x.Text + "@" + x.Box.Y)); } bool clicked = false; foreach (var h in heads.OrderBy(x => x.Y)) { bool shut = close.Contains(h.Index); if (!(want.Contains(h.Index) || shut) || seen.Contains(h.Index) || h.ButtonAt.Y > area.Bottom - 20) continue; seen.Add(h.Index); if (h.Collapsed == shut) continue; log((shut ? "Folding " : "Opening ") + GameData.Cities[h.Index] + " in the Jobs list" + (shut ? " (every job there is Gold)" : "")); Click(h.ButtonAt.X, h.ButtonAt.Y, 900); jobScroll.Clear(); clicked = true; break; } if (seen.Contains(lastWanted)) break; if (clicked && ++clicks <= want.Count + close.Count) { lastSeen = null; page--; continue; } if (page > 0 && sig == lastSeen) break; lastSeen = sig; ScrollPage(0.54, -notches); } return seen; } public readonly List JobsSeen = new List(); public JobCard FindJob(JobInfo job, List catalog) { using (var f = Capture()) { var area = JobsArea(f); foreach (var c in ReadJobCards(f, catalog, false)) if (c.Info == job && c.NameY >= area.Top && c.NameY <= area.Top + 0.6 * area.Height) return c; } int remembered; if (jobScroll.TryGetValue(job.Name, out remembered)) { JobsTop(); if (remembered > 0) ScrollPage(0.54, -remembered); using (var f = Capture()) foreach (var c in ReadJobCards(f, catalog, false)) if (c.Info == job) return c; } JobsTop(); MeasureNotch(catalog); int pos = 0; string lastSig = null; JobCard low = null; JobsSeen.Clear(); for (int page = 0; page < 60; page++) { List cards; Rectangle area; using (var f = Capture()) { cards = ReadJobCards(f, catalog, false); area = JobsArea(f); } foreach (var c in cards) if (!JobsSeen.Contains(c.Name)) JobsSeen.Add(c.Name); low = null; foreach (var c in cards) { if (c.Info != job) continue; if (c.NameY > area.Top + 0.6 * area.Height) { low = c; break; } jobScroll[job.Name] = pos; return c; } string sig = string.Join(",", cards.Select(x => x.Name)); if (page > 0 && sig == lastSig) break; lastSig = sig; int step = low != null ? 2 : ListNotches(4); ScrollPage(0.54, -step); pos += step; } if (low != null) jobScroll[job.Name] = pos; return low; } public int DoJob(JobCard card, int maxTimes, List catalog = null) { int done = 0; bool nudged = false; var nameArea = card.NameBox; nameArea.Inflate(card.NameBox.Height, card.NameBox.Height / 3); for (int i = 0; i < maxTimes; i++) { bool ready = false, moved = false; for (int look = 0; look < 8 && !ready && !moved; look++) { using (var f = Capture()) { if (GoldIsh(f, card.StateAt.X, card.StateAt.Y)) { var name = Ocr.ReadText(f, nameArea, 2, Prep.None); if (card.Info != null && MatchJob(name, new List { card.Info }) == null) moved = true; else ready = true; } else if (look >= 2 && IsDim(f, card.StateAt.X, card.StateAt.Y)) break; } if (!ready && !moved) Wait(250); } if (moved && !nudged && catalog != null && card.Info != null) { nudged = true; ScrollPage(0.54, 1); Wait(300); JobCard again; using (var f = Capture()) again = ReadJobCards(f, catalog, false).FirstOrDefault(c => c.Info == card.Info); if (again != null) { card = again; nameArea = card.NameBox; nameArea.Inflate(card.NameBox.Height, card.NameBox.Height / 3); jobScroll.Remove(card.Info.Name); i--; continue; } } if (moved) { log("The job list moved - looking again next time"); NoteFailure("the job list moved under the job"); break; } if (!ready) break; Press(card.ButtonText, card.ButtonAt.X, card.ButtonAt.Y, 450); done++; } return done; } public int LearnJobs(List catalog, int level, bool openNewCities, ICollection fold = null) { if (level > 0) SetCities(Enumerable.Range(0, GameData.Cities.Length).Where(ci => GameData.CityLevels[ci] <= level && (openNewCities || catalog.Any(j => GameData.CityIndex(j.City) == ci))), fold ?? new int[0]); JobsTop(); MeasureNotch(catalog); var walk = WalkJobsList(catalog, ListNotches(6), level: level, look: () => { var frame = Capture(); try { var area = JobsArea(frame); var lines = Ocr.Read(frame, area, 2, Prep.None); return new JobsListLook { Frame = frame, Area = area, Lines = lines, Cards = CardsFromLines(frame, lines, catalog, true) }; } catch { frame.Dispose(); throw; } }, roll: notches => ScrollPage(0.54, notches)); jobScroll.Clear(); CountUnseen(catalog, walk); return walk.Seen.Count; } internal sealed class JobsListLook { public Rectangle Area; public List Lines; public List Cards; public Frame Frame; } internal sealed class JobsWalk { public string City = GameData.Cities[0]; public int Tier = 1, TierLevel = 1; public bool ReachedEnd; public readonly HashSet Seen = new HashSet(); } internal JobsWalk WalkJobsList(List catalog, int step, Func look, Action roll, int level = 0) { var walk = new JobsWalk(); JobsListLook last = null; Dictionary lastAt = null; int rolled = 0; double notch = 0; bool saidStep = false; for (int page = 0; page < 80; page++) { var p = look(); try { if (last != null && SameLook(p, last)) { walk.ReachedEnd = true; break; } var at = Anchors(p); if (last != null) { double strip = Strip(last, p); int? moved = Moved(lastAt, at); if (moved.HasValue && moved.Value > 0 && rolled > 0) notch = Math.Max(notch, (double)moved.Value / rolled); bool gap = moved.HasValue ? moved.Value > strip : notch <= 0 || rolled * notch > strip; if (gap && rolled > 1) { int back = rolled / 2; roll(back); rolled -= back; continue; } if (notch > 0 && step * notch > strip) { step = Math.Max(1, (int)(strip / notch)); if (!saidStep) { saidStep = true; log("Jobs list: a roll moves it further than a look shows - read again higher up where it went past cards, and rolling it by less for the rest of this read"); } } } TakeJobsPage(walk, p.Lines, p.Cards, catalog, p.Frame); if (level > 0 && LockedFromHere(p.Cards, catalog, level)) break; last = p; lastAt = at; } finally { if (p.Frame != null) p.Frame.Dispose(); } roll(-step); rolled = step; } return walk; } internal static bool LockedFromHere(List cards, List catalog, int level) { foreach (var c in cards) { var m = Regex.Match(c.ButtonText ?? "", @"LEVEL\s*([0-9OIl]+)", RegexOptions.IgnoreCase); int need; if (c.Info == null || !m.Success || !int.TryParse(Parse.Digits(m.Groups[1].Value), out need) || need <= level) continue; if (c.Info.Order < 100000 && catalog.Any(j => j.City == c.Info.City && j.Order > c.Info.Order && j.Order < 100000)) return true; } return false; } static double Strip(JobsListLook a, JobsListLook b) { double u = Math.Max(10, a.Cards.Concat(b.Cards).Select(c => c.NameBox.Height).DefaultIfEmpty(22).Max()); return 0.93 * b.Area.Height - 2.4 * u - 4; } static int? Moved(Dictionary from, Dictionary to) { var d = from.Where(x => to.ContainsKey(x.Key)).Select(x => x.Value - to[x.Key]).OrderBy(v => v).ToList(); return d.Count == 0 ? (int?)null : d[d.Count / 2]; } static Dictionary Anchors(JobsListLook k) { var at = new Dictionary(); var twice = new HashSet(); foreach (var x in k.Cards.Select(c => new KeyValuePair("job " + (c.Info != null ? c.Info.Name : c.Name), c.NameY)) .Concat(Headings(k).Select(l => new KeyValuePair("heading " + l.Text, l.Box.Y)))) { if (at.ContainsKey(x.Key)) twice.Add(x.Key); else at[x.Key] = x.Value; } foreach (var x in twice) at.Remove(x); return at; } static bool SameLook(JobsListLook a, JobsListLook b) { if (a == null || b == null) return false; Func>> marks = k => k.Cards.Select(c => new KeyValuePair(c.Name, c.NameY)) .Concat(Headings(k).Select(l => new KeyValuePair(l.Text, l.Box.Y))).ToList(); List> x = marks(a), y = marks(b); if (x.Count != y.Count) return false; for (int i = 0; i < x.Count; i++) if (x[i].Key != y[i].Key || Math.Abs(x[i].Value - y[i].Value) > 3) return false; return true; } static List Headings(JobsListLook k) { return k.Lines.Where(l => GameData.CityIndex(l.Text) >= 0 || Parse.Has(l.Text, "TIER")).OrderBy(l => l.Box.Y).ToList(); } internal void TakeJobsPage(JobsWalk w, List lines, List cards, List catalog, Frame frame) { var events = new List>(); foreach (var l in lines) if (GameData.CityIndex(l.Text) >= 0 || Parse.Has(l.Text, "TIER")) events.Add(new KeyValuePair(l.Box.Y, l)); foreach (var c in cards) events.Add(new KeyValuePair(c.NameY, c)); var first = lines.Where(l => GameData.CityIndex(l.Text) >= 0).OrderBy(l => l.Box.Y).FirstOrDefault(); int firstCity = first != null ? GameData.CityIndex(first.Text) : -1; int keepAbove = first != null && firstCity <= GameData.CityIndex(w.City) ? first.Box.Y : int.MinValue; foreach (var e in events.OrderBy(x => x.Key)) { var l = e.Value as OcrLine; if (l != null) { int ci = GameData.CityIndex(l.Text); if (ci >= 0) { w.City = GameData.Cities[ci]; continue; } var tm = Regex.Match(l.Text, @"TIER\s*([0-9IlO]+).*?LEVEL\s*([0-9IlOS]+)", RegexOptions.IgnoreCase); int t, tl; if (tm.Success && int.TryParse(Parse.Digits(tm.Groups[1].Value), out t) && int.TryParse(Parse.Digits(tm.Groups[2].Value), out tl)) { w.Tier = t; w.TierLevel = tl >= 200 ? tl / 10 : tl; } continue; } var c = (JobCard)e.Value; bool above = c.NameY < keepAbove; if (above && c.Info != null && GameData.CityIndex(c.Info.City) >= firstCity) c.Info = null; if (c.Info != null && !above) { int here = GameData.CityIndex(w.City), its = GameData.CityIndex(c.Info.City); if (its < here) c.Info = MatchJob(c.Name, c.Cost, here, catalog); else if (its > here) w.City = GameData.Cities[its]; } if (c.Info == null) { if (above || c.Cost <= 0 || c.Xp <= 0 || !Regex.IsMatch(c.Name, @"^[A-Za-z][A-Za-z' \-]+$") || c.Name.Split(' ').Count(x => x.Length >= 2) < 3 || catalog.Any(j => GameData.NameNear(c.Name, j) && (!CostOff(c.Cost, j.Cost) || !CostOff(c.Xp, j.Xp)))) continue; c.Info = new JobInfo { City = w.City, Tier = w.Tier, TierLevel = w.TierLevel, Name = c.Name, Cost = c.Cost, Xp = c.Xp, Order = 100000 + catalog.Count }; catalog.Add(c.Info); log(string.Format("New job found: \"{0}\" ({1}, {2} energy, {3} XP)", c.Name, w.City, c.Cost, c.Xp)); } else { if (c.Cost > 0 && (c.Info.Cost <= 0 || Math.Abs(c.Cost - c.Info.Cost) * 10 <= c.Info.Cost * 3)) c.Info.Cost = c.Cost; if (c.Xp > 0 && (c.Info.Xp <= 0 || Math.Abs(c.Xp - c.Info.Xp) * 10 <= c.Info.Xp * 3)) c.Info.Xp = c.Xp; } var lm = Regex.Match(c.ButtonText, @"LEVEL\s*([0-9OIl]+)", RegexOptions.IgnoreCase); int need; if (lm.Success && int.TryParse(Parse.Digits(lm.Groups[1].Value), out need) && GameData.InCityLevels(c.Info.City, need)) c.Info.TierLevel = Math.Max(c.Info.TierLevel, need); ApplyMastery(c.Info, c.Mastery, frame, c); w.Seen.Add(c.Info); } } internal static void CountUnseen(List catalog, JobsWalk walk) { var seen = walk.Seen; if (seen.Count == 0) return; int last = seen.Max(j => GameData.CityIndex(j.City)); foreach (var j in catalog) { int c = GameData.CityIndex(j.City); if (seen.Contains(j)) j.Unseen = 0; else if (seen.Any(x => GameData.CityIndex(x.City) == c) && (c < last || walk.ReachedEnd)) j.Unseen++; } } public Rectangle PageArea(Frame f) { var v = ViewOf(f); return v.Problem == null ? v.Page : new Rectangle(260, 118, f.Width - 260, f.Height - 118); } int PageScale(Frame f) { var v = ViewOf(f); return v.Problem == null ? View.ScaleFor(14 * v.HeaderScale) : 2; } public int ReadFavors(Frame f, int zoom = 0) { var p = PageArea(f); var top = new Rectangle(p.Left + p.Width / 2, p.Top, p.Width / 2, Math.Max(40, p.Height / 5)); var line = Ocr.Read(f, top, PageScale(f) + zoom, Prep.None).FirstOrDefault(l => Parse.Has(l.Text, "FAVOR")); return line == null ? -1 : FavorsIn(line.Text); } internal static int FavorsIn(string text) { text = text ?? ""; int at = text.IndexOf("FAVOR", StringComparison.OrdinalIgnoreCase); if (at < 0 || Regex.IsMatch(text.Substring(0, at), @"\d")) return -1; var m = Regex.Match(text.Substring(at), @"LEFT\W*([0-9]{1,3})(?![0-9])", RegexOptions.IgnoreCase); int n; if (m.Success && int.TryParse(m.Groups[1].Value, out n)) return n <= 99 ? n : -1; return Regex.IsMatch(text, @"LEFT\W*[OoD]\s*$", RegexOptions.IgnoreCase) ? 0 : -1; } public List ReadHeists(Frame f) { var lines = Ocr.Read(f, PageArea(f), PageScale(f), Prep.None); ReadLostHelpButtons(f, lines); var rows = new List(); foreach (var b in lines.Where(l => Parse.Key(l.Text).Contains("HEIPOUT") || Parse.Key(l.Text).Contains("HEIPED")).OrderBy(l => l.Box.Y)) { int h = Math.Max(8, b.Box.Height); var r = new HeistRow { HelpText = b.Text, HelpAt = new Point(b.CenterX, b.CenterY), Top = b.Box.Y }; var name = lines.Where(l => l.Box.Right < b.Box.X && l.CenterY >= b.Box.Y - 1.5 * h && l.CenterY <= b.CenterY + 0.4 * h && Regex.Matches(l.Text, "[A-Za-z]").Count >= 6 && !Parse.Has(l.Text, "ENDS") && !Parse.Has(l.Text, "LEVEL")) .OrderByDescending(l => l.Box.X).FirstOrDefault(); if (name != null) r.Name = name.Text; var xp = lines.Where(l => Parse.Has(l.Text, "XP") && l.CenterY > b.Box.Bottom && l.CenterY < b.Box.Bottom + 8 * h && l.CenterX > b.Box.X - b.Box.Width && l.CenterX < b.Box.Right + b.Box.Width / 3).OrderBy(l => l.CenterY).FirstOrDefault(); bool grand = Parse.Has(r.Name, "GRAND VAULT"); r.XpPerHelp = grand ? 140 : -1; var xm = xp != null ? Regex.Match(xp.Text, @"(\d[\d,]*)\s*XP", RegexOptions.IgnoreCase) : Match.Empty; int cardXp; if (r.XpPerHelp < 0 && xm.Success && int.TryParse(xm.Groups[1].Value.Replace(",", ""), out cardXp)) r.XpPerHelp = cardXp; if (r.XpPerHelp < 0) r.XpPerHelp = KnownHeistXp(r.Name); var en = lines.Where(l => (Parse.Has(l.Text, "ENERGY") || Regex.IsMatch(l.Text.Trim(), @"^\W{0,2}-\s*[0-9]{1,2}$")) && l.CenterY > b.Box.Bottom && l.CenterY < b.Box.Bottom + 3.5 * h && l.Box.Right > b.Box.X - 6 * h && l.Box.X >= b.Box.X - 9 * h && l.Box.X < b.Box.Right).OrderBy(l => l.Box.X).FirstOrDefault(); var em = en != null ? Regex.Match(en.Text, @"-?\s*([0-9]{1,2})\s*(ENERGY)?\s*$", RegexOptions.IgnoreCase) : Match.Empty; if (em.Success) r.Energy = int.Parse(em.Groups[1].Value); string k = Parse.Key(r.HelpText); r.CanHelp = k.Contains("HEIPOUT") && !k.Contains("HEIPED") && GoldButton(f, b.Box.X, b.CenterY, h); if (en == null && b.Box.Bottom + 2 * h > PageArea(f).Bottom) r.CanHelp = false; var n = Regex.Match(k, @"HEIPOUT([0-9IOS]{1,2})OF([0-9IOS]{1,2})"); if (n.Success && HelpCount(n.Groups[1].Value) >= HelpCount(n.Groups[2].Value) && HelpCount(n.Groups[2].Value) > 0) r.CanHelp = false; r.YourFamily = lines.Any(l => l.Box.Right < b.Box.X && l.CenterY > b.Box.Y - h && l.CenterY < b.Box.Bottom + 3.5 * h && (Parse.Has(l.Text, "YOUR FAMILY") || (l.Text.Contains("[") && GreenText(f, l.Box)))); rows.Add(r); } return rows; } static bool GoldButton(Frame f, int textX, int y, int h) { for (int x = textX - 2; x >= Math.Max(0, textX - 12 * h); x--) { var c = f.Pixel(x, y); if (Math.Max(c.R, Math.Max(c.G, c.B)) >= 80) return GoldIsh(f, x, y); } return false; } static bool GreenText(Frame f, Rectangle box) { int green = 0; for (int y = box.Top; y < box.Bottom; y += 2) for (int x = box.Left; x < box.Right; x += 2) { var c = f.Pixel(x, y); if (c.G >= 140 && c.G - c.R >= 35 && c.G - c.B >= 60) green++; } return green >= 6; } static int HelpCount(string folded) { int v; return int.TryParse(folded.Replace('I', '1').Replace('O', '0').Replace('S', '5'), out v) ? v : -1; } void ReadLostHelpButtons(Frame f, List lines) { var chips = lines.Where(l => Parse.Has(l.Text, "ENERGY")).ToList(); foreach (var chip in chips) { if (chips.Any(c => c != chip && Math.Abs(c.CenterY - chip.CenterY) < chip.Box.Height && c.Box.X < chip.Box.X)) continue; int ch = Math.Max(8, chip.Box.Height); bool button = lines.Any(l => l.CenterY < chip.Box.Y && l.CenterY > chip.Box.Y - 3.5 * ch && Math.Abs(l.Box.X - chip.Box.X) < 6 * ch && (Parse.Key(l.Text).Contains("HEIPOUT") || Parse.Key(l.Text).Contains("HEIPED") || Parse.Has(l.Text, "LEVEL"))); if (button) continue; var sab = lines.FirstOrDefault(l => Parse.Has(l.Text, "SABOTAGE") && Math.Abs(l.CenterY - (chip.Box.Y - 1.8 * ch)) < 1.5 * ch && l.Box.X > chip.Box.Right); int left = chip.Box.X - 2 * ch, right = sab != null ? sab.Box.X - ch : chip.Box.X + 14 * ch; var box = Rectangle.Intersect(new Rectangle(left, chip.Box.Y - (int)(3.4 * ch), right - left, (int)(3.1 * ch)), new Rectangle(0, 0, f.Width, f.Height)); if (box.Width < 4 * ch || box.Height < ch) continue; foreach (var prep in new[] { Prep.Contrast, Prep.DarkText }) { var read = Ocr.Read(f, box, 2, prep).FirstOrDefault(l => Parse.Key(l.Text).Contains("HEIPOUT")); if (read == null) continue; lines.Add(read); break; } } } static int KnownHeistXp(string name) { if (Parse.Has(name, "GRAND VAULT")) return 140; if (Parse.Has(name, "MUSEUM") || Parse.Has(name, "AFTERDARK")) return 60; if (Parse.Has(name, "TRUCK")) return 25; if (Parse.Has(name, "CORNER") || Parse.Has(name, "STORE")) return 12; return -1; } public void HelpHeist(HeistRow r) { Press(r.HelpText, r.HelpAt.X, r.HelpAt.Y, 1100); CheckForPurchaseWindow(); } public void HeistsTop() { ScrollPage(0.71, 10); } public bool Hospitalized(Frame f) { var p = PageArea(f); var right = Rectangle.FromLTRB(p.Left + (int)(p.Width * 0.6), p.Top, p.Right, p.Top + p.Height / 3); var lines = Ocr.Read(f, right, PageScale(f), Prep.None).Where(l => !TargetLine(l.Text)).ToList(); string text = string.Join(" ", lines.Select(l => l.Text)); return Parse.Has(text, "HOSPITALIZED") || Parse.Has(text, "HEAL BEFORE FIGHTING"); } internal static bool TargetLine(string text) { string k = Parse.Key(text ?? ""); return k.Contains("TARGET") || k.Contains("MUSTRECOVER") || k.Contains("THEYMUST"); } public List ReadFightRows(Frame f) { bool war; return ReadFightRows(f, out war); } public List ReadFightRows(Frame f, out bool war) { var p = PageArea(f); var list = new Rectangle(p.Left, p.Top, (int)(p.Width * 0.62), p.Height); int scale = PageScale(f); var lines = Ocr.Read(f, list, scale, Prep.None); if (!lines.Any(l => Parse.Key(l.Text) == "ATTACK")) { list = p; lines = Ocr.Read(f, list, scale, Prep.None); } var rows = new List(); war = false; if (lines.Any(l => ProfileTitleRx.IsMatch(l.Text))) return rows; int i = 0; foreach (var b in lines.Where(l => Parse.Key(l.Text) == "ATTACK").OrderBy(l => l.Box.Y)) { int h = Math.Max(8, b.Box.Height); var r = new FightRow { Index = i++, AttackAt = new Point(b.CenterX, b.CenterY), AttackText = b.Text, Ready = f.Near(b.Box.X - Math.Max(4, h / 2), b.CenterY, RED, 30) }; OcrLine nameLine = null; bool costRead = false; var warLabels = new List(); var topLines = new List(); foreach (var l in lines) { if (l == b || l.Box.Right > b.Box.X || l.CenterY < b.Box.Y - 1.3 * h || l.CenterY > b.Box.Bottom + 1.3 * h) continue; if (Parse.Has(l.Text, "WAR TARGET")) warLabels.Add(l); if (l.CenterY < b.CenterY + 0.3 * h && Parse.Key(l.Text).StartsWith("NOFAMI")) r.NoFamily = true; if (l.CenterY < b.CenterY + 0.3 * h && !Regex.IsMatch(l.Text, @"L\s?eve|Steal|Stamina|WAR\s*TARGET", RegexOptions.IgnoreCase)) topLines.Add(l); int lv = LevelIn(l.Text); if (lv > 0) r.Level = lv; var m = Regex.Match(l.Text, @"Steal\s*([0-9]+)\s*%", RegexOptions.IgnoreCase); if (m.Success) r.Steal = int.Parse(m.Groups[1].Value); m = Regex.Match(l.Text, @"([1-9])\s*Stamina", RegexOptions.IgnoreCase); if (m.Success) { r.StaminaCost = int.Parse(m.Groups[1].Value); costRead = true; } } var under = lines.Where(l => l.Box.Right <= b.Box.X && l.CenterY >= b.Box.Y && l.CenterY <= b.Box.Bottom + 1.3 * h && Regex.IsMatch(l.Text, @"L\s?eve|Steal|Stamina|WAR\s*TARGET", RegexOptions.IgnoreCase)).ToList(); var chip = costRead ? null : under.FirstOrDefault(l => Regex.IsMatch(l.Text, "Stamina", RegexOptions.IgnoreCase)); if (chip != null) { int ch = Math.Max(8, chip.Box.Height); var strip = Rectangle.FromLTRB(Math.Max(list.Left, under.Min(l => l.Box.X) - h), chip.Box.Y - ch / 2, b.Box.X - 2, chip.Box.Bottom + ch / 2); var cost = Ocr.Read(f, strip, scale + 1, Prep.None).Select(l => Regex.Match(l.Text, @"([1-9])\s*Stamina", RegexOptions.IgnoreCase)) .FirstOrDefault(x => x.Success); if (cost != null) r.StaminaCost = int.Parse(cost.Groups[1].Value); } var info = (r.Level < 0 || r.Steal < 0) ? under.FirstOrDefault(l => Regex.IsMatch(l.Text, @"L\s?eve|Steal", RegexOptions.IgnoreCase)) : null; if (info != null) { int ih = Math.Max(8, info.Box.Height); var strip = Rectangle.FromLTRB(Math.Max(list.Left, under.Min(l => l.Box.X) - h), info.Box.Y - ih / 2, b.Box.X - 2, info.Box.Bottom + ih / 2); foreach (var t in new[] { Tuple.Create(scale + 1, Prep.None), Tuple.Create(scale, Prep.Contrast), Tuple.Create(scale + 1, Prep.Contrast) }) foreach (var l in (r.Level < 0 || r.Steal < 0) ? Ocr.Read(f, strip, t.Item1, t.Item2) : new List()) { int lv = LevelIn(l.Text); if (r.Level < 0 && lv > 0) r.Level = lv; var sm = Regex.Match(l.Text, @"Steal\s*([0-9]+)\s*%", RegexOptions.IgnoreCase); if (r.Steal < 0 && sm.Success) r.Steal = int.Parse(sm.Groups[1].Value); } } double nameMaxX = under.Count > 0 ? under.Min(l => l.Box.X) + 3 * h : list.Left + 0.45 * (b.Box.X - list.Left); nameLine = topLines.Where(l => under.Count == 0 || l.Box.Right > under.Min(u => u.Box.X)).OrderBy(l => l.Box.X).FirstOrDefault(); if (nameLine != null && nameLine.Box.X > nameMaxX) nameLine = null; if (nameLine != null) { r.Name = nameLine.Text.Trim(); r.NameAt = new Point(nameLine.CenterX, nameLine.CenterY); } if (!r.NoFamily) { double from = nameLine != null ? nameLine.Box.Right : nameMaxX; r.Family = string.Join(" ", lines.Where(l => l != b && l != nameLine && l.Box.X > from && l.Box.Right <= b.Box.X && l.CenterY >= b.Box.Y - 1.3 * h && l.CenterY < b.CenterY + 0.3 * h && !Regex.IsMatch(l.Text, @"L\s?eve|Steal|Stamina|WAR\s*TARGET", RegexOptions.IgnoreCase)) .OrderBy(l => l.Box.X).Select(l => l.Text.Trim())).Trim(); } int badge; if (nameLine != null && SplitPrestige(r.Name, out badge) != r.Name) r.Prestige = badge > 0 ? badge : PrestigeCount(f, nameLine.Box, scale); double below = (nameLine != null ? nameLine.CenterY : b.Box.Y - 0.5 * h) + 0.5 * h; r.War = warLabels.Any(l => l.CenterY > below && l.CenterY >= b.Box.Y); rows.Add(r); } var levels = rows.Where(r => r.Level > 0).Select(r => r.Level).OrderBy(x => x).ToList(); if (levels.Count >= 3) { int median = levels[levels.Count / 2]; foreach (var r in rows) if (r.Level > 0 && r.Level * 2 < median && median - r.Level >= 30) r.Level = -1; } war = rows.Any(r => r.War) || lines.Any(l => { string k = Parse.Key(l.Text); return k == "WARTARGETS" || k.StartsWith("WARTARGETSONI"); }); return rows; } internal static int LevelIn(string text) { string plain = Regex.Replace(text ?? "", @"[^A-Za-z0-9% ]", ""); var m = Regex.Match(plain, @"L\s?eve(?>[l1I]?)\s?([0-9OoIlSB]{1,3})(?:(?<=[0-9OoIlSB]{3})[0O4])?\b", RegexOptions.IgnoreCase); int lv; return m.Success && Regex.IsMatch(m.Groups[1].Value, @"\d") && int.TryParse(Parse.Digits(m.Groups[1].Value), out lv) ? lv : -1; } public bool Attack(FightRow r) { FightRow now; using (var f = Capture()) now = AttackStillThere(f, r); if (now == null) return false; Press(now.AttackText, now.AttackAt.X, now.AttackAt.Y, 400); return true; } internal FightRow AttackStillThere(Frame f, FightRow r) { int tol = Math.Max(4, f.Height / 100); return ReadFightRows(f).FirstOrDefault(x => x.Ready && Parse.Key(x.AttackText) == "ATTACK" && Math.Abs(x.AttackAt.X - r.AttackAt.X) <= tol && Math.Abs(x.AttackAt.Y - r.AttackAt.Y) <= tol && (x.Name.Length == 0 || r.Name.Length == 0 || SameRowName(x.Name, r.Name))); } static bool SameRowName(string a, string b) { string ka = Parse.Key(a), kb = Parse.Key(b); return ka == kb || (Math.Min(ka.Length, kb.Length) >= 6 && View.Distance(ka, kb) <= 2); } public bool FightListLoading(Frame f) { var p = PageArea(f); var area = Rectangle.FromLTRB(p.Left, p.Top, p.Right, p.Top + p.Height / 3); foreach (int s in new[] { PageScale(f), 1 }.Distinct()) if (Ocr.Read(f, area, s, Prep.None).Any(l => Parse.Key(l.Text).StartsWith("IOADING"))) return true; return false; } public static bool TargetHospitalized(string toast) { if (toast == null || YouHospitalized(toast)) return false; string k = Parse.Key(toast); return k.Contains("HOSPITAI") || k.Contains("MUSTRECOVER"); } public static bool YouHospitalized(string toast) { string k = Parse.Key(toast); return k.Contains("YOUAREHOSPITAI") || k.Contains("YOUAREINTHEHOSPITAI") || k.Contains("HEAIUPFIRST"); } internal static bool TopRightYouHospitalized(Frame f) { double row = 34.5 * f.Height / 989.0; var area = Rectangle.Intersect(new Rectangle(f.Width / 2, 0, f.Width - f.Width / 2, (int)(8 * row)), new Rectangle(0, 0, f.Width, f.Height)); if (area.Width < 20 || area.Height < 8) return false; return Ocr.Read(f, area, View.ScaleFor(20 * f.Height / 989.0), Prep.None).Any(l => YouHospitalized(l.Text)); } const string RefreshArrow = "the Fight page's round arrow"; static bool Goldish(Color c) { return c.R >= 150 && c.G >= 115 && c.B <= 125 && c.R >= c.G && c.R - c.B >= 80 || EventOrange(c); } static bool GoldTinted(Color c) { return c.R >= 90 && (c.G >= 0.6 * c.R && c.G <= c.R + 5 && c.R - c.B >= 40 || c.G >= 0.44 * c.R && c.G <= 0.62 * c.R && c.B <= 0.36 * c.R) || c.G >= 100 && c.R >= 0.5 * c.G && c.R <= 0.75 * c.G && c.B <= 0.5 * c.G; } internal static Rectangle FightRefreshBox(Frame f, IEnumerable words) { foreach (var s in words.Where(o => Parse.Key(o.Text) == "SEARCH").OrderByDescending(o => o.Box.X)) { var box = RefreshAbove(f, s); if (!box.IsEmpty) return box; } return Rectangle.Empty; } static Rectangle RefreshAbove(Frame f, OcrLine search) { int th = Math.Max(6, search.Box.Height), y = search.CenterY; int left = -1; for (int x = search.Box.Right + 1, run = 0; x < f.Width - 1; x++) { if (!Goldish(f.Pixel(x, y))) { run = 0; continue; } if (++run >= th) { left = x - run + 1; break; } } if (left < 0) return Rectangle.Empty; int col = left + 2, top = y, bottom = y, reach = 4 * th; while (y - top < reach && Goldish(f.Pixel(col, top - 1))) top--; while (bottom - y < reach && Goldish(f.Pixel(col, bottom + 1))) bottom++; int bh = bottom - top + 1; if (bh < th || y - top >= reach || bottom - y >= reach) return Rectangle.Empty; int row = top + Math.Max(2, bh / 8), right = col; while (right + 1 < f.Width && Goldish(f.Pixel(right + 1, row))) right++; if (right - left + 1 < 2.5 * bh) return Rectangle.Empty; var area = Rectangle.FromLTRB(Math.Max(0, right - 2 * bh), Math.Max(0, top - (int)(1.6 * bh) - 2), Math.Min(f.Width - 1, right + bh / 2), top - 1 - Math.Max(1, bh / 20)); int l = int.MaxValue, t = int.MaxValue, r = -1, b = -1; for (int yy = area.Top; yy <= area.Bottom; yy++) for (int xx = area.Left; xx <= area.Right; xx++) if (GoldTinted(f.Pixel(xx, yy))) { l = Math.Min(l, xx); t = Math.Min(t, yy); r = Math.Max(r, xx); b = Math.Max(b, yy); } if (r < 0) return Rectangle.Empty; var box = Rectangle.FromLTRB(l, t, r + 1, b + 1); int w = box.Width, h = box.Height; if (w < 0.6 * bh || w > 1.6 * bh || h < 0.6 * bh || h > 1.6 * bh || Math.Abs(w - h) > 0.25 * Math.Max(w, h) + 2) return Rectangle.Empty; if (Math.Abs(box.Right - 1 - right) > 0.3 * bh + 3) return Rectangle.Empty; return ArrowSquare(f, box) ? box : Rectangle.Empty; } static bool ArrowSquare(Frame f, Rectangle box) { int w = box.Width, h = box.Height; int hitT = 0, hitB = 0, hitL = 0, hitR = 0; for (int xx = box.Left; xx < box.Right; xx++) { if (GoldInward(f, xx, box.Top, 0, 1)) hitT++; if (GoldInward(f, xx, box.Bottom - 1, 0, -1)) hitB++; } for (int yy = box.Top; yy < box.Bottom; yy++) { if (GoldInward(f, box.Left, yy, 1, 0)) hitL++; if (GoldInward(f, box.Right - 1, yy, -1, 0)) hitR++; } if (Math.Min(hitT, hitB) * 10 < w * 7 || Math.Min(hitL, hitR) * 10 < h * 7) return false; int inset = Math.Max(2, Math.Min(w, h) / 5), dark = 0, goldIn = 0, n = 0; for (int yy = box.Top + inset; yy < box.Bottom - inset; yy++) for (int xx = box.Left + inset; xx < box.Right - inset; xx++) { var c = f.Pixel(xx, yy); n++; if (Goldish(c)) goldIn++; else if ((c.R + c.G + c.B) / 3 < 80) dark++; } return !(n == 0 || dark * 10 < n * 4 || goldIn * 100 < n * 3 || goldIn * 10 > n * 6); } internal static Rectangle RefreshInTitle(Frame f, OcrLine title, Rectangle page) { int y = title.CenterY, th = Math.Max(6, title.Box.Height); int x = Math.Min(f.Width - 2, page.Right - 2), stop = Math.Max(title.Box.Right, page.Right - page.Width / 6); Func side = c => Goldish(c) || (GoldTinted(c) && c.R >= 140); while (x > stop && !side(f.Pixel(x, y))) x--; if (x <= stop) return Rectangle.Empty; int right = x, top = y, bottom = y, reach = 3 * th; while (y - top < reach && top > 1 && GoldTinted(f.Pixel(right, top - 1))) top--; while (bottom - y < reach && bottom < f.Height - 2 && GoldTinted(f.Pixel(right, bottom + 1))) bottom++; int h = bottom - top + 1; if (h < th || y - top >= reach || bottom - y >= reach) return Rectangle.Empty; int left = right; for (int edge = top; edge <= Math.Min(bottom, top + 2); edge++) { int l = right; while (l - 1 > stop && right - l < 2 * h && GoldTinted(f.Pixel(l - 1, edge))) l--; left = Math.Min(left, l); } Func sideAt = xl => Enumerable.Range(top, h).Count(yy => GoldTinted(f.Pixel(xl, yy))) * 10 >= h * 7; if (!sideAt(left)) for (int xl = Math.Max(stop + 2, right - h * 5 / 4); xl <= right - h * 3 / 4; xl++) if (sideAt(xl)) { left = xl; break; } int w = right - left + 1; if (left - 1 <= stop || Math.Abs(w - h) > 0.25 * Math.Max(w, h) + 2) return Rectangle.Empty; var box = Rectangle.FromLTRB(left, top, right + 1, bottom + 1); return ArrowSquare(f, box) ? box : Rectangle.Empty; } static bool GoldInward(Frame f, int x, int y, int dx, int dy) { for (int k = 0; k < 3; k++) if (GoldTinted(f.Pixel(x + k * dx, y + k * dy))) return true; return false; } public Rectangle FightRefreshIn(Frame f) { var p = PageArea(f); var top = Rectangle.FromLTRB(p.Left, p.Top, p.Right, p.Top + p.Height / 4); var words = Ocr.ReadWords(f, top, PageScale(f), Prep.None); var box = FightRefreshBox(f, words); if (box.IsEmpty) foreach (var t in words.Where(o => Parse.Key(o.Text) == "FIGHT" && o.Box.Y < p.Top + p.Height / 10).OrderBy(o => o.Box.Y)) { box = RefreshInTitle(f, t, p); if (!box.IsEmpty) break; } if (box.IsEmpty) box = FightRefreshByButton(f); return box; } internal Rectangle FightRefreshByButton(Frame f) { var p = PageArea(f); var top = Rectangle.FromLTRB(p.Left, p.Top, p.Right, p.Top + p.Height / 4); foreach (int s in new[] { PageScale(f), 1 }.Distinct()) foreach (var w in Ocr.ReadWords(f, top, s, Prep.DarkText).Where(o => Parse.Key(o.Text) == "SEARCH").OrderByDescending(o => o.Box.X)) { int x = w.Box.X - 1; while (x > 0 && Goldish(f.Pixel(x, w.CenterY))) x--; var box = RefreshAbove(f, new OcrLine { Text = w.Text, Box = new Rectangle(Math.Max(0, x - 3), w.Box.Y, 1, w.Box.Height) }); if (!box.IsEmpty) return box; } return Rectangle.Empty; } public bool RefreshFightList() { Rectangle box; using (var f = Capture()) box = FightRefreshIn(f); if (box.IsEmpty && CloseTerritoryToasts()) using (var f = Capture()) box = FightRefreshIn(f); if (box.IsEmpty) { bool closed; using (var f = Capture()) closed = CloseOpenMenu(f); if (closed) using (var f = Capture()) box = FightRefreshIn(f); } if (box.IsEmpty) return false; Press(RefreshArrow, box.X + box.Width / 2, box.Y + box.Height / 2, 900); return true; } public sealed class PlayerProfile { public string Name = ""; public string Username = ""; public int Respect = -1, Attack = -1, Defense = -1, Crew = -1; public OcrLine Title, Scout; public Point Close; public bool Family; public string Seen = ""; public bool Read { get { return Attack >= 0 && Defense >= 0; } } } internal static string ProfileUsername(string title) { var m = Regex.Match(title ?? "", @"\(\s*@\s*([A-Za-z0-9_ ]{3,24}?)\s*\)?\s*$"); if (!m.Success) return ""; string u = m.Groups[1].Value.Trim(); u = Regex.Matches(u, @"\s+").Count == 1 && !u.Contains("_") ? Regex.Replace(u, @"\s+", "_") : u.Replace(" ", ""); return Regex.IsMatch(u, "^[A-Za-z0-9_]{3,20}$") ? u.ToLowerInvariant() : ""; } internal static string SplitPrestige(string name, out int prestige) { prestige = -1; string s = (name ?? "").Trim(); var m = Regex.Match(s, @"^(.{3,}?)\s+(?:([OoQ0@©®○◯Ⓞ])|([①-⑥])|\(?([1-6])\)?)$"); if (!m.Success) return s; prestige = m.Groups[3].Success ? m.Groups[3].Value[0] - '①' + 1 : m.Groups[4].Success ? m.Groups[4].Value[0] - '0' : 0; return m.Groups[1].Value.Trim(); } internal static int PrestigeCount(Frame f, Rectangle nameLine, int scale) { int h = Math.Max(8, nameLine.Height); var words = Ocr.ReadWords(f, Rectangle.Inflate(nameLine, h / 2, h / 3), scale, Prep.None).OrderBy(w => w.Box.X).ToList(); if (words.Count < 2) return -1; var last = words[words.Count - 1]; int p; SplitPrestige("xxx " + last.Text, out p); if (p < 0 || last.Box.Width > 2 * h || last.Box.Height < h / 2) return -1; return p > 0 ? p : BadgeDigit(f, last.Box); } static List digitShapes; internal static int BadgeDigit(Frame f, Rectangle badge) { var inner = Rectangle.FromLTRB(badge.X + badge.Width * 3 / 10, badge.Y + badge.Height / 5, badge.Right - badge.Width * 3 / 10, badge.Bottom - badge.Height / 5); if (inner.Width < 3 || inner.Height < 5) return -1; var grey = new double[inner.Width, inner.Height]; for (int x = 0; x < inner.Width; x++) for (int y = 0; y < inner.Height; y++) { var c = f.Pixel(inner.X + x, inner.Y + y); grey[x, y] = Math.Max(0, Math.Min(1, ((c.R + c.G + c.B) / 3.0 - 70) / 120)); } var keep = new bool[inner.Width, inner.Height]; int sx = -1, sy = -1; double near = double.MaxValue; for (int x = 0; x < inner.Width; x++) for (int y = 0; y < inner.Height; y++) { double d = Math.Pow(x - inner.Width / 2.0, 2) + Math.Pow(y - inner.Height / 2.0, 2); if (grey[x, y] >= 0.5 && d < near) { near = d; sx = x; sy = y; } } if (sx < 0) return -1; var todo = new Stack(); todo.Push(new Point(sx, sy)); keep[sx, sy] = true; while (todo.Count > 0) { var q = todo.Pop(); for (int dx = -1; dx <= 1; dx++) for (int dy = -1; dy <= 1; dy++) { int x = q.X + dx, y = q.Y + dy; if (x < 0 || y < 0 || x >= inner.Width || y >= inner.Height || keep[x, y] || grey[x, y] < 0.2) continue; keep[x, y] = true; todo.Push(new Point(x, y)); } } for (int x = 0; x < inner.Width; x++) for (int y = 0; y < inner.Height; y++) if (!keep[x, y]) grey[x, y] = 0; var shape = ShapeOf(grey, inner.Width, inner.Height); if (shape == null) return -1; var cols = new double[inner.Width]; for (int x = 0; x < inner.Width; x++) for (int y = 0; y < inner.Height; y++) cols[x] += grey[x, y]; double all = cols.Sum(), two = 0; for (int x = 0; x + 1 < inner.Width; x++) two = Math.Max(two, cols[x] + cols[x + 1]); if (all > 0 && two >= 0.8 * all) return 1; if (digitShapes == null) digitShapes = DigitShapes(); double best = double.MaxValue, second = double.MaxValue; int digit = -1; for (int i = 0; i < digitShapes.Count; i++) { double d = 0; for (int k = 0; k < shape.Length; k++) d += (shape[k] - digitShapes[i][k]) * (shape[k] - digitShapes[i][k]); int n = i % 6 + 1; if (d < best) { if (n != digit) second = best; best = d; digit = n; } else if (d < second && n != digit) second = d; } return best < 4 && second - best > 0.6 ? digit : -1; } static double[] ShapeOf(double[,] grey, int w, int hgt) { int x1 = w, y1 = hgt, x2 = -1, y2 = -1; for (int x = 0; x < w; x++) for (int y = 0; y < hgt; y++) if (grey[x, y] >= 0.5) { x1 = Math.Min(x1, x); y1 = Math.Min(y1, y); x2 = Math.Max(x2, x); y2 = Math.Max(y2, y); } if (x2 < 0 || y2 - y1 < 3) return null; double gw = x2 - x1 + 1, gh = y2 - y1 + 1; var s = new double[4 * 6 + 1]; for (int cx = 0; cx < 4; cx++) for (int cy = 0; cy < 6; cy++) { double sum = 0, n = 0; for (double u = 0.125; u < 1; u += 0.25) for (double v = 0.125; v < 1; v += 0.25) { int x = x1 + (int)((cx + u) * gw / 4), y = y1 + (int)((cy + v) * gh / 6); sum += grey[Math.Min(x2, x), Math.Min(y2, y)]; n++; } s[cy * 4 + cx] = sum / n; } s[24] = 2 * gw / gh; return s; } static List DigitShapes() { var shapes = new List(); foreach (var font in new[] { "Arial", "Segoe UI" }) for (int d = 1; d <= 6; d++) using (var bmp = new Bitmap(60, 80)) using (var g = Graphics.FromImage(bmp)) using (var fnt = new Font(font, 40, FontStyle.Bold, GraphicsUnit.Pixel)) { g.Clear(Color.Black); g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; g.DrawString(d.ToString(), fnt, Brushes.White, 5, 5); var grey = new double[60, 80]; for (int x = 0; x < 60; x++) for (int y = 0; y < 80; y++) grey[x, y] = bmp.GetPixel(x, y).R / 255.0; shapes.Add(ShapeOf(grey, 60, 80)); } return shapes; } static readonly Regex ProfileTitleRx =new Regex(@"^\s*(PLAYER|FAMI[LI1]Y)\s*PROFI[LI1]E\s*[:;.]?\s*(.*)$", RegexOptions.IgnoreCase); public PlayerProfile ReadPlayerProfile(Frame f) { var popup = PopupArea(f); int scale = PopupScale(f); var lines = Ocr.Read(f, popup, scale, Prep.None); var title = lines.FirstOrDefault(l => ProfileTitleRx.IsMatch(l.Text)); if (title == null) return null; var tm = ProfileTitleRx.Match(title.Text); int h = Math.Max(8, title.Box.Height); string rest = ""; var whole = title.Box; foreach (var l in lines.Where(l => l != title && l.Box.X > title.Box.Right && Math.Abs(l.CenterY - title.CenterY) <= 0.5 * h && !Regex.IsMatch(l.Text.Trim(), "^[xX×]$")).OrderBy(l => l.Box.X)) { if (l.Box.X - whole.Right > 2 * h) break; rest += " " + l.Text.Trim(); whole = Rectangle.Union(whole, l.Box); } if (rest.Length > 0) title = new OcrLine { Text = title.Text + rest, Box = whole }; var p = new PlayerProfile { Title = title, Family = !tm.Groups[1].Value.ToUpperInvariant().StartsWith("P") }; p.Name = Regex.Replace(tm.Groups[2].Value + rest, @"\s*\(\s*@.*$", "").Trim(); p.Username = ProfileUsername(tm.Groups[2].Value + rest); var x = lines.Where(l => Regex.IsMatch(l.Text.Trim(), "^[xX×]$") && l.Box.X > title.Box.Right && Math.Abs(l.CenterY - title.CenterY) <= 1.5 * h) .OrderBy(l => l.Box.X).FirstOrDefault(); p.Close = x != null ? new Point(x.CenterX, x.CenterY) : SkillWindowX(f, title, popup); if (p.Close.IsEmpty) { int mirror = f.Width - title.Box.X; var near = new OcrLine { Text = title.Text, Box = Rectangle.FromLTRB(title.Box.X, title.Box.Y, Math.Max(title.Box.Right, mirror - 4 * h), title.Box.Bottom) }; p.Close = SkillWindowX(f, near, popup); } if (p.Close.IsEmpty) p.Close = ProfileClose(f, title, popup); int left = title.Box.X - 3 * h, right = p.Close.IsEmpty ? popup.Right : p.Close.X + 2 * h; var inside = lines.Where(l => l != title && l.Box.X >= left && l.Box.Right <= right && l.Box.Y > title.Box.Bottom).ToList(); var stats = inside.FirstOrDefault(l => Parse.Key(l.Text) == "STATS"); Func labelOf = (label, first) => stats == null ? inside.FirstOrDefault(l => Parse.Key(l.Text).Contains(label)) : inside.FirstOrDefault(l => l.Box.X >= stats.Box.X - h && l.Box.Y > stats.Box.Bottom && Parse.Key(l.Text).Contains(first)); var attack = labelOf("ATTACKPOWER", "ATTACK"); if (stats == null && attack != null) { var full = labelOf; int ah = Math.Max(8, attack.Box.Height); labelOf = (label, first) => full(label, first) ?? inside.FirstOrDefault(l => l != attack && Math.Abs(l.Box.X - attack.Box.X) <= 2 * ah && Math.Abs(l.CenterY - attack.CenterY) <= 8 * ah && Parse.Key(l.Text).StartsWith(first)); } var defense = labelOf("DEFENSEPOWER", "DEFENSE"); Func value = lab => { if (lab == null) return -1; int v; var own = Regex.Match(lab.Text, @":\s*([0-9][0-9,.]*\s*[KMB]?)\s*$", RegexOptions.IgnoreCase); if (own.Success && (v = ProfileNumber(own.Groups[1].Value)) >= 0) return v; int lh = Math.Max(8, lab.Box.Height); var num = inside.Where(l => l != lab && l.Box.X > lab.Box.Right && Math.Abs(l.CenterY - lab.CenterY) <= 0.7 * lh) .OrderBy(l => l.Box.X).FirstOrDefault(); return num != null ? ProfileNumber(num.Text) : -1; }; p.Respect = value(labelOf("RESPECT", "RESPECT")); p.Attack = value(attack); p.Defense = value(defense); p.Crew = value(labelOf("CREWSIZE", "CREW")); p.Scout = inside.FirstOrDefault(l => Parse.Key(l.Text).StartsWith("SCOUT")); if (p.Scout == null && !p.Read && attack != null && defense != null) { int lh = Math.Max(8, attack.Box.Height); var area = Rectangle.FromLTRB(Math.Max(attack.Box.Right, defense.Box.Right) + lh / 2, attack.Box.Y - lh / 2, right, defense.Box.Bottom + lh); foreach (var prep in new[] { Prep.None, Prep.DarkText }) if ((p.Scout = Ocr.Read(f, area, scale, prep).FirstOrDefault(l => Parse.Key(l.Text).StartsWith("SCOUT"))) != null) break; } if (p.Scout == null) { if (p.Attack < 0) p.Attack = ZoomedNumber(f, attack, right, scale); if (p.Defense < 0) p.Defense = ZoomedNumber(f, defense, right, scale); } if (!p.Read) p.Seen = string.Join(" | ", inside.Where(l => l.Box.Y < title.Box.Bottom + 12 * h).Select(l => l.Text.Trim())); return p; } static int ZoomedNumber(Frame f, OcrLine lab, int right, int scale) { if (lab == null) return -1; int lh = Math.Max(8, lab.Box.Height); if (right <= lab.Box.Right + lh) return -1; var strip = Rectangle.FromLTRB(lab.Box.Right + lh / 2, lab.Box.Y - lh / 2, right, lab.Box.Bottom + lh / 2); var seen = new List(); int empty = 0; foreach (var t in new[] { Tuple.Create(Math.Max(3, scale + 1), Prep.None), Tuple.Create(scale, Prep.WhiteBright), Tuple.Create(scale + 1, Prep.WhiteText), Tuple.Create(scale + 2, Prep.None), Tuple.Create(scale + 1, Prep.WhiteBright) }) { var again = Ocr.Read(f, strip, t.Item1, t.Item2); if (again.Count == 0) { if (++empty == 2) break; continue; } empty = 0; int v = ProfileNumber(again.OrderBy(l => l.Box.Right).Last().Text); if (v < 0) continue; if (seen.Contains(v)) return v; seen.Add(v); } return -1; } internal static int ProfileNumber(string text) { var m = Regex.Match((text ?? "").Trim(), @"^([0-9]{1,3}(?:,[0-9]{3})*|[0-9]+)(?:\.([0-9]+))?\s*([KMB])?$", RegexOptions.IgnoreCase); if (!m.Success) return -1; double v = double.Parse(m.Groups[1].Value.Replace(",", "") + (m.Groups[2].Success ? "." + m.Groups[2].Value : ""), System.Globalization.CultureInfo.InvariantCulture); if (m.Groups[2].Success && !m.Groups[3].Success) return -1; switch (m.Groups[3].Success ? char.ToUpperInvariant(m.Groups[3].Value[0]) : ' ') { case 'K': v *= 1e3; break; case 'M': v *= 1e6; break; case 'B': v *= 1e9; break; } return v > int.MaxValue ? int.MaxValue : (int)v; } internal static bool ProfileNames(string profile, string meant) { string a = Parse.Key(profile), b = Parse.Key(meant); if (a.Length < 3 || b.Length < 3) return false; if (Math.Min(a.Length, b.Length) >= 5 && (a.StartsWith(b) || b.StartsWith(a))) return true; return a == b || View.Distance(a, b) <= Math.Max(1, Math.Min(a.Length, b.Length) / 4); } PlayerProfile LookAtProfile(int looks, Func done) { PlayerProfile p = null; for (int look = 0; look < looks; look++) { if (look > 0) Wait(300); using (var f = Capture()) { CheckForPurchasePrompt(Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None)); p = ReadPlayerProfile(f); } if (p != null && done(p)) return p; } return p; } public const string NoStaminaToScout = "no stamina for a scout"; public const string NoProfile = "no profile - No Family, likely one of the game's computer players"; public bool ProfileShowing() { using (var f = Capture()) return ReadPlayerProfile(f) != null; } public bool CloseProfile() { for (int t = 0; t < 5; t++) { PlayerProfile p; int fw, fh; using (var f = Capture()) { p = ReadPlayerProfile(f); fw = f.Width; fh = f.Height; } if (p == null) return true; Point close = p.Close; if (close.IsEmpty && p.Title != null) { int h = Math.Max(8, p.Title.Box.Height); close = new Point(Math.Min(fw - 1, p.Title.Box.Right + 4 * h), Math.Max(0, Math.Min(fh - 1, p.Title.CenterY))); } if (t == 4 || close.IsEmpty) return false; Press("x", close.X, close.Y, 700); } return false; } public PlayerProfile ScoutPlayer(FightRow r, bool mayScout, out bool paid, out string why) { paid = false; why = null; if (r.Name.Length < 3 || r.NameAt.IsEmpty) { why = "its name didn't read"; return null; } if (NeverPress.Why(r.Name) != null) { why = "its name has a word the bot never presses"; return null; } Press(r.Name, r.NameAt.X, r.NameAt.Y, 600); var p = LookAtProfile(12, x => true); if (p == null && r.NoFamily) p = LookAtProfile(20, x => true); if (p == null) { if (r.NoFamily) { why = NoProfile; Snapshot("fight no profile no family", 0, true); } else { why = "no profile opened"; Snapshot("fight profile not opened", 60); } CloseProfile(); return null; } try { if (p.Family) { why = "a family's profile opened"; Snapshot("fight family profile opened", 60); return null; } if (!ProfileNames(p.Name, r.Name)) p = LookAtProfile(20, x => x.Family || ProfileNames(x.Name, r.Name)) ?? p; if (p.Family) { why = "a family's profile opened"; Snapshot("fight family profile opened", 60); return null; } if (!ProfileNames(p.Name, r.Name)) { why = "the profile opened was someone else's: \"" + p.Name + "\" for the row \"" + r.Name + "\""; Snapshot("fight profile not the player", 60); return null; } if (!p.Read && p.Scout == null) p = LookAtProfile(15, x => x.Read || x.Scout != null) ?? p; if (p.Read) return p; if (p.Scout == null) { why = "its numbers didn't load"; Snapshot("fight profile not loaded", 60); return null; } if (!mayScout) { why = NoStaminaToScout; return null; } Press(p.Scout, 700); paid = true; Wait(500); var after = LookAtProfile(25, x => x.Read); if (after == null || !after.Read) { why = "the numbers didn't read after SCOUT"; Snapshot("fight scout unreadable", 60); return null; } Snapshot("fight scouted", 0, true, false); return after; } finally { if (!CloseProfile()) { Snapshot("fight profile not closed", 60); if (why == null) why = "the profile didn't close"; } } } public PlayerProfile ReadOwnProfile() { Header h; using (var f = Capture()) h = ReadHeader(f); if (h == null || string.IsNullOrEmpty(h.Name) || h.NameBox.IsEmpty || NeverPress.Why(h.Name) != null) { Snapshot("own name not read for the profile", 60); return null; } Press(h.Name, h.NameBox.X + h.NameBox.Width / 2, h.NameBox.Y + h.NameBox.Height / 2, 600); var p = LookAtProfile(20, x => x.Read); if (p == null) { Snapshot("own profile not opened", 60); return null; } bool ok = p.Read && ProfileNames(p.Name, h.Name); if (!ok) { log("Own profile: the window says \"" + p.Name + "\"" + (p.Read ? "" : " (its numbers didn't read)") + ", the top bar \"" + h.Name + "\" - not taken as yours" + (p.Seen.Length > 0 ? " - read: " + p.Seen : "")); Snapshot("own profile not ours", 60); } if (!CloseProfile()) Snapshot("fight profile not closed", 60); return ok ? p : null; } public static bool Refused(string toast) { if (TargetHospitalized(toast) || YouHospitalized(toast)) return true; return toast != null && Regex.IsMatch(toast.ToUpperInvariant().Replace('0', 'O'), @"\b(NOT|CANNOT|CAN\W?T|ENOUGH|INSUFFICIENT|NO\s+(MORE\s+)?(FAVORS?|ENERGY|STAMINA|HEALTH)|OUT\s+OF\s+(FAVORS?|ENERGY|STAMINA|HEALTH)|COOL\s*DOWN|HOSPITALI[SZ]ED|MUST\s+RECOVER|DAILY\s*LIMIT|LIMIT\s+REACHED|SHIELDED)\b"); } public static int ShieldSeconds(string toast) { if (!Parse.Has(toast ?? "", "SHIELDED")) return -1; var m = Regex.Match(toast.Replace('O', '0').Replace('o', '0'), @"SHIELDED.*?(\d{1,2})\s?[:.]\s?(\d{2})(?:\s?[:.]\s?(\d{2}))?", RegexOptions.IgnoreCase); if (!m.Success) return 0; int a = int.Parse(m.Groups[1].Value), b = int.Parse(m.Groups[2].Value); return m.Groups[3].Success ? (a * 60 + b) * 60 + int.Parse(m.Groups[3].Value) : a * 60 + b; } public static bool DailyLimit(string text) { return text != null && Regex.IsMatch(text.ToUpperInvariant().Replace('1', 'I'), @"DAILY\s*LIMIT|LIMIT\s+REACHED"); } static bool RewardRead(string spoils) { return Regex.IsMatch(spoils ?? "", @"[+-]\s?[0-9OoIlS]{1,4}(?:[,.][0-9OoIlS]{1,3})*\s+[A-Za-z]{2}", RegexOptions.IgnoreCase); } internal static string TopRightHospitalized(Frame f) { double row = 34.5 * f.Height / 989.0; var area = Rectangle.Intersect(new Rectangle(f.Width / 2, 0, f.Width - f.Width / 2, (int)(3.5 * row)), new Rectangle(0, 0, f.Width, f.Height)); if (area.Width < 20 || area.Height < 8) return null; var lines = Ocr.Read(f, area, View.ScaleFor(20 * f.Height / 989.0), Prep.None).OrderBy(l => l.Box.Y).ToList(); int i = lines.FindIndex(l => TargetHospitalized(l.Text)); if (i < 0) return null; return string.Join(" ", lines.Skip(i).Take(3).Select(l => l.Text.Trim())); } public string LastFightRefusal; public string LastFightToast; public Bitmap AfterAttack; public void ForgetAfterAttack() { if (AfterAttack != null) { AfterAttack.Dispose(); AfterAttack = null; } } public bool FightCalledOff; public bool FightWindowStuck; public double LastFightCash = -1; public bool LastFightPending; internal double WinCash(Frame f, List lines, OcrLine done) { double v = WindowCash(lines, done); var pending = lines.LastOrDefault(l => Parse.Key(l.Text).EndsWith("PENDING") && l.Box.Bottom <= done.Box.Y + 2 && l.Box.Y > done.Box.Y - 8 * done.Box.Height); LastFightPending = pending != null; if (v >= 0 || pending == null) return v; var p = pending.Box; int h = Math.Max(8, p.Height); var area = Rectangle.Intersect(new Rectangle(p.X - 3 * Math.Max(p.Width, 4 * h), p.Y - h, 3 * Math.Max(p.Width, 4 * h) + h, 3 * h), new Rectangle(0, 0, f.Width, f.Height)); if (area.Width <= 0 || area.Height <= 0) return -1; foreach (var prep in new[] { Prep.None, Prep.Contrast }) foreach (int up in new[] { 1, 2 }) { var near = Ocr.Read(f, area, PopupScale(f) + up, prep); foreach (var l in near.OrderByDescending(l => l.Box.Right)) { string t = l.Text.Replace(" ", ""); double m = LastMoney(t.Replace("S", "$").Replace("O", "0")); bool unit = Regex.IsMatch(t, "[KMBT]$") || m == 0; if ((t.Contains("$") || t.StartsWith("S") || t.StartsWith("~")) && m >= 0 && unit) return m; } } return -1; } internal static double WindowCash(List lines, OcrLine done) { var cash = lines.Where(l => l.Box.Bottom <= done.Box.Y + 2 && l.Box.Y > done.Box.Y - 8 * done.Box.Height && l.Text.Contains("$")) .OrderByDescending(l => l.Box.Y); foreach (var l in cash) { double v = LastMoney(l.Text); if (v >= 0) return v; } return -1; } public Bitmap FightGone; const int FightWindowMs = 40000; internal static bool FightWindowIn(List lines) { var crews = lines.Where(l => Parse.Key(l.Text).StartsWith("CREW")).ToList(); foreach (var a in crews) foreach (var b in crews) { int h = Math.Max(8, Math.Max(a.Box.Height, b.Box.Height)); if (b.Box.X <= a.Box.Right + 3 * h || Math.Abs(a.CenterY - b.CenterY) > h) continue; if (lines.Any(t => Parse.Key(t.Text) == "FIGHT" && t.Box.Bottom < Math.Min(a.Box.Y, b.Box.Y) && t.CenterX > a.CenterX && t.CenterX < b.CenterX)) return true; } foreach (var a in crews) { int h = Math.Max(8, a.Box.Height); foreach (var t in lines.Where(t => Parse.Key(t.Text) == "FIGHT" && t.Box.Bottom < a.Box.Y)) { int mid = t.CenterX, d = Math.Abs(a.CenterX - mid); if (d < 3 * h) continue; int other = 2 * mid - a.CenterX; var between = lines.Where(l => l.Box.Y > t.Box.Bottom && l.Box.Bottom < a.Box.Y).ToList(); if (between.Any(p => Math.Abs(p.CenterX - a.CenterX) <= 2 * h && between.Any(q => Math.Abs(q.CenterX - other) <= 2 * h && Math.Abs(q.CenterY - p.CenterY) <= h))) return true; } } return false; } internal bool? GameDimmed(Frame f) { var v = ViewOf(f); if (v.Problem != null) return null; return MenuDimmed(f, v); } public int LastOwnRoll = -1, LastOtherRoll = -1, LastOwnAttack = -1, LastOtherDefense = -1; public bool CollectFights; static readonly Prep[] FightPreps = { Prep.Contrast, Prep.DarkText, Prep.None }; const int FightReadMs = 500; void ReadFightWindow(Frame f, OcrLine done) { int h = Math.Max(6, done.Box.Height); var zooms = new List(); foreach (int z in new[] { 3, 4, 5, 6, PopupScale(f) }) if (z >= 2 && z <= 8 && !zooms.Contains(z)) zooms.Add(z); var strip = Rectangle.Intersect(Rectangle.FromLTRB(0, done.Box.Y - 30 * h, f.Width, done.Box.Y - 6 * h), new Rectangle(0, 0, f.Width, f.Height)); if (strip.Width <= 40 || strip.Height <= 20) return; int start = Environment.TickCount; try { using (var bmp = Crop(f, strip)) using (var sf = new Frame(bmp)) foreach (var prep in FightPreps) foreach (int zoom in zooms) { if (Environment.TickCount - start >= FightReadMs) return; if (FightWindowIn(sf, new Rectangle(0, 0, sf.Width, sf.Height), zoom, prep)) return; } } catch (Exception) { } } bool FightWindowIn(Frame f, Rectangle area, int zoom, Prep prep) { List lines; try { lines = Ocr.Read(f, area, zoom, prep); } catch (Exception) { return false; } var bands = new List(); var lows = new List(); var highs = new List(); foreach (var l in lines) { int lo, hi; if (!BandLine(l.Text, out lo, out hi)) continue; bands.Add(l); lows.Add(lo); highs.Add(hi); } if (bands.Count < 2) return false; int left = 0, right = 0; for (int i = 1; i < bands.Count; i++) { if (bands[i].Box.X < bands[left].Box.X) left = i; if (bands[i].Box.X > bands[right].Box.X) right = i; } if (left == right) return false; if (bands[left].Box.X + bands[left].Box.Width > bands[right].Box.X + 8) return false; if (Math.Abs(bands[left].Box.Y - bands[right].Box.Y) > 2 * bands[left].Box.Height) return false; int own = (lows[left] + highs[left]) / 2, other = (lows[right] + highs[right]) / 2; if (own < 100 || other < 100) return false; LastOwnAttack = own; LastOtherDefense = other; LastOwnRoll = RollIn(lines, bands[left], lows[left], highs[left]); LastOtherRoll = RollIn(lines, bands[right], lows[right], highs[right]); return true; } static bool BandLine(string text, out int lo, out int hi) { lo = hi = -1; var n = FightNumbers(text); if (n.Count < 2) return false; int bottom = n[0], top = n[n.Count - 1]; if (bottom < 100 || top <= bottom) return false; if ((double)top / bottom < 1.47 || (double)top / bottom > 1.53) return false; lo = bottom; hi = top; return true; } static List FightNumbers(string text) { var got = new List(); foreach (Match m in Regex.Matches(text ?? "", @"[0-9][0-9,.]*")) { int v; if (int.TryParse(Parse.Digits(m.Value), out v) && v > 0) got.Add(v); } return got; } static int RollIn(List lines, OcrLine band, int lo, int hi) { int found = -1; foreach (var l in lines) { if (l.Box.Bottom > band.Box.Y + band.Box.Height / 3) continue; if (l.Box.Y < band.Box.Y - 5 * band.Box.Height) continue; if (l.CenterX < band.Box.Left - band.Box.Width / 10 || l.CenterX > band.Box.Right + band.Box.Width / 10) continue; foreach (int v in FightNumbers(l.Text)) if (v >= lo && v <= hi && v > found) found = v; } return found; } void KeepFightPicture(Frame f, string outcome) { if (string.IsNullOrEmpty(ProblemsDir)) return; try { string folder = System.IO.Path.Combine(ProblemsDir, "fights"); System.IO.Directory.CreateDirectory(folder); string file = System.IO.Path.Combine(folder, DateTime.Now.ToString("yyyyMMdd-HHmmss") + "-" + outcome + ".png"); f.Bitmap.Save(file, System.Drawing.Imaging.ImageFormat.Png); log("Kept a picture of that fight: problems\\fights\\" + System.IO.Path.GetFileName(file)); } catch (Exception e) { log("Couldn't keep a picture of that fight: " + e.Message); } } internal static List FightWindowLines(Frame f, List lines, OcrLine done) { var box = FrameAround(f, done.Box); if (!box.IsEmpty) return lines.Where(l => l.Box.X >= box.Left - 4 && l.Box.Right <= box.Right + 4).ToList(); int right = done.CenterX + f.Width * 24 / 100, left = done.CenterX - f.Width * 24 / 100; return lines.Where(l => l.Box.X < right && l.Box.Right > left).ToList(); } public string FinishFight(int timeoutMs) { int start = Environment.TickCount, end = start + timeoutMs; bool skipped = false, sawWindow = false; LastFightRefusal = null; LastFightToast = null; FightCalledOff = FightWindowStuck = false; LastFightCash = -1; LastFightPending = false; LastOwnRoll = LastOtherRoll = LastOwnAttack = LastOtherDefense = -1; if (FightGone != null) { FightGone.Dispose(); FightGone = null; } bool first = true; int spoiled = 0; bool windowUp = false, lastUp = false; int goneLooks = 0; bool doneUp = false; while (doneUp || (windowUp ? start + FightWindowMs : end) - Environment.TickCount > 0) { doneUp = false; using (var f = Capture()) { if (first) { first = false; if (AfterAttack != null) AfterAttack.Dispose(); AfterAttack = Frame.Marked(f.Bitmap, new Bitmap(f.Bitmap)); } var lines = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None); CheckForPurchasePrompt(lines); var done = lines.FirstOrDefault(l => Parse.Key(l.Text) == "DONE"); if (done != null) { lines = FightWindowLines(f, lines, done); string all = string.Join(" ", lines.Select(l => l.Text)).ToUpperInvariant(); var rewards = lines.Where(l => l.Box.Bottom < done.Box.Y && l.Box.Y > done.Box.Y - 9 * done.Box.Height && (l.Text.Contains("+") || l.Text.Contains("-"))).Select(l => l.Text).ToList(); if (!RewardRead(string.Join(" ", rewards)) && spoiled++ < 2) { doneUp = true; Wait(500); continue; } string outcome = all.Contains("VICTORY") ? "VICTORY" : all.Contains("DEFEAT") ? "DEFEAT" : "DONE"; if (outcome == "VICTORY") LastFightCash = WinCash(f, lines, done); ReadFightWindow(f, done); if (CollectFights) KeepFightPicture(f, outcome); Press(done, 700); string spoils = string.Join(" ", rewards).Trim(); return spoils.Length > 0 ? outcome + " (" + spoils + ")" : outcome; } var skip = lines.FirstOrDefault(l => Parse.Has(l.Text, "TAPTOSKIP")); if (!skipped && skip != null) { sawWindow = windowUp = true; skipped = true; Press(skip.Text, skip.CenterX, skip.CenterY - 3 * skip.Box.Height, 300); continue; } bool up = FightWindowIn(lines); bool? dim = up ? true : GameDimmed(f); lastUp = up || dim != false; if (up) { windowUp = true; goneLooks = 0; } else if (windowUp && !sawWindow && dim == false && ++goneLooks >= 2) { FightCalledOff = true; FightGone = Frame.Marked(f.Bitmap, new Bitmap(f.Bitmap)); LastFightToast = ToastIn(f) ?? ReadToast(1500); if (Refused(LastFightToast)) LastFightRefusal = LastFightToast; return null; } else if (dim != false) goneLooks = 0; string toast = sawWindow ? null : ToastIn(f); if (!sawWindow && dim == null && !Refused(toast)) toast = TopRightHospitalized(f) ?? toast; if (toast != null && (dim == false || Refused(toast))) LastFightToast = toast; if (Refused(toast)) { LastFightRefusal = toast; return null; } } Wait(250); } FightWindowStuck = windowUp && lastUp; return null; } public OcrLine FindDepositAll(Frame f) { OcrLine w; return FindDepositAll(f, out w); } public OcrLine FindDepositAll(Frame f, out OcrLine withdraw) { withdraw = null; var area = PageArea(f); int s = PageScale(f); OcrLine card = null; foreach (var prep in new[] { Prep.None, Prep.WhiteText, Prep.DarkText }) { var lines = Ocr.Read(f, area, s, prep); card = card ?? BankCard(lines); if (card == null) continue; foreach (var b in lines.Where(l => Parse.Key(l.Text) == "DEPOSITAII")) { int h = Math.Max(8, b.Box.Height); bool inCard = b.Box.Y > card.Box.Bottom && b.Box.X >= card.Box.X - 4 * h && b.Box.X < card.Box.Right; var beside = lines.FirstOrDefault(w => Parse.Key(w.Text) == "WITHDRAWAII" && Math.Abs(w.CenterY - b.CenterY) <= h && w.Box.X > b.Box.Right && w.Box.X - b.Box.Right < 2 * b.Box.Width); if (inCard && beside != null) { withdraw = beside; return b; } } } return null; } static OcrLine BankCard(List lines) { var title = lines.FirstOrDefault(l => { string k = Parse.Key(l.Text); return k.Contains("NATIONAIBANK") || View.Distance(k, "FIRSTNATIONAIBANK") <= 3; }); if (title != null) return title; var onHand = lines.FirstOrDefault(l => Parse.Key(l.Text) == "ONHAND"); if (onHand == null) return null; int h = Math.Max(8, onHand.Box.Height); return lines.Any(l => Parse.Key(l.Text) == "BANKED" && l.Box.Y > onHand.Box.Bottom && l.Box.Y < onHand.Box.Bottom + 4 * h && Math.Abs(l.Box.X - onHand.Box.X) < 2 * h) ? onHand : null; } public double DepositCashBefore = -1; public string DepositAll(out int fee) { fee = -1; DepositCashBefore = depositCardBefore = -1; if (!OpenTab(Tab.Bank)) return null; OcrLine button; string feeLine; Header before; int look = 0; while (true) { using (var f = Capture()) { button = FindDepositAll(f); fee = ReadBankFee(f, out feeLine); before = ReadHeader(f); if (button != null) depositCardBefore = BankCardAmount(f, true); } if (button != null && (before.Cash < 0 || before.Banked < 0) && look < 4 && CloseTerritoryToasts()) { look++; continue; } if (button != null || ++look >= 4) break; Wait(600); } DepositCashBefore = before.Cash; double firstCard = depositCardBefore; if (button == null) { log("Bank screen not recognised"); Snapshot("bank not recognised", 60); return null; } CheckForPurchaseWindow(); Press(button, 200); string said = BankToast() ?? BankMoved(before, false); if (said == BankUnchanged || said == BankUnread) said = DepositLate(before) ?? said; said = BankCardSays(said); if (said != BankUnchanged) return said; OcrLine again; using (var f = Capture()) { again = FindDepositAll(f); before = ReadHeader(f); if (again != null) depositCardBefore = BankCardAmount(f, true); } if (again == null || (before.Cash >= 0 && before.Cash < 1)) return BankRose(firstCard) ?? said; if (before.Cash > 0) DepositCashBefore = before.Cash; CheckForPurchaseWindow(); Press(again, 200); said = BankToast() ?? BankMoved(before, false); if (said == BankUnchanged || said == BankUnread) said = DepositLate(before) ?? said; said = BankCardSays(said); return said == BankUnchanged || said == BankUnread ? BankRose(firstCard) ?? said : said + " (pressed twice)"; } string BankRose(double before) { if (before < 0) return null; for (int look = 0; look < 3; look++) { if (look > 0) Wait(500); double after; using (var f = Capture()) after = BankCardAmount(f, true); if (after >= before + 1000 && after - before <= Math.Max(before, 1e11)) return "the notification didn't show, the bank card says " + SafehouseInfo.Money(after) + " banked, " + SafehouseInfo.Money(after - before) + " more"; } return null; } string BankCardSays(string said) { if (said != BankUnread) return said; double onHand; using (var f = Capture()) onHand = BankCardOnHand(f); if (onHand < 1000) return said; CloseTerritoryToasts(); return BankUnchanged; } string DepositLate(Header before) { string toast = ReadToast(2500, IsBankToast, true); if (toast != null && toast.Trim().Length > 0) return toast.Trim() + " (shown late)"; double onHand, banked = -1; double was = before != null ? before.Cash : -1; using (var f = Capture()) { toast = DepositedToast(f); if (toast != null) return toast + " (shown late)"; onHand = BankCardOnHand(f); if (depositCardBefore >= 0 && was > 0) banked = BankCardAmount(f, true); } if (DepositShown(depositCardBefore, banked, was)) return "the notification didn't show, the bank card says " + SafehouseInfo.Money(banked) + " banked, " + SafehouseInfo.Money(banked - depositCardBefore) + " more"; if (onHand < 0) return null; if (onHand < 1000 || (was > 0 && onHand < was / 6)) return "the notification didn't show, the bank card says " + SafehouseInfo.Money(onHand) + " on hand"; return null; } string BankToast() { return ReadToast(2000, IsBankToast, true); } public static bool IsBankToast(string t) { return Regex.IsMatch(t ?? "", @"ba\s?n\s?k|d\s?e\s?p\s?[o0]\s?s|withdr|fee\s*\)", RegexOptions.IgnoreCase); } public const string BankUnchanged = "(the top bar shows no change)", BankUnread = "(no notification, and the top bar didn't read)"; public const string BankHidden = "(Roblox's own title bar hides the top bar, so it can't be checked)"; double BankCardOnHand(Frame f) { return BankCardAmount(f, false); } internal double BankCardAmount(Frame f, bool banked) { var lines = Ocr.Read(f, PageArea(f), PageScale(f), Prep.None); var title = lines.FirstOrDefault(l => Parse.Key(l.Text).Contains("NATIONAIBANK")); var label = lines.FirstOrDefault(l => Parse.Key(l.Text) == (banked ? "BANKED" : "ONHAND")); if (title == null || label == null) return -1; int h = Math.Max(8, label.Box.Height); var row = Rectangle.FromLTRB(label.Box.Right + 2 * h, label.Box.Y - 2 * h, Math.Min(f.Width - 1, title.Box.X + title.Box.Width * 3 / 2 + 2 * h), label.Box.Bottom + 2 * h); if (banked) { double v; var own = Rectangle.FromLTRB(label.Box.Right + 2 * h, label.CenterY - h, row.Right, label.CenterY + 2 * h); foreach (var t in new[] { Tuple.Create(2, Prep.Contrast), Tuple.Create(1, Prep.Contrast), Tuple.Create(2, Prep.None), Tuple.Create(3, Prep.Contrast), Tuple.Create(1, Prep.None) }) { var m = Regex.Match(Ocr.ReadText(f, own, t.Item1, t.Item2), @"\$\s?\d{1,3}(?:\s?,\s?\d{3})+(?![\d,.])"); if (m.Success && Parse.Money(m.Value.Replace(" ", ""), out v)) return v; } return ReadLoneMoney(f, Rectangle.FromLTRB(label.Box.Right + 2 * h, label.CenterY - h * 8 / 5, row.Right, label.CenterY + h * 8 / 5)); } foreach (int s in new[] { 2, 1, 3 }) foreach (var prep in new[] { Prep.None, Prep.Contrast, Prep.DarkText }) { double v; if (Parse.Money(string.Join(" ", Ocr.Read(f, row, s, prep).Select(l => l.Text)), out v)) return v; } return -1; } internal static string DepositedToast(Frame f) { var area = Rectangle.FromLTRB(f.Width * 60 / 100, 0, f.Width - 2, f.Height / 4); int s = View.ScaleFor(f, 14, 14.0 * f.Height / 1009); foreach (var prep in new[] { Prep.None, Prep.WhiteText }) { var l = Ocr.Read(f, area, s, prep).FirstOrDefault(x => Parse.Has(x.Text, "DEPOSITED") && Regex.IsMatch(x.Text, @"\$\s?\d") && IsBankToast(x.Text)); if (l != null) return Regex.Replace(l.Text, @"\s+", " ").Trim(); } return null; } double depositCardBefore = -1; string BankMoved(Header before, bool withdraw) { string moved = BankMovedTopBar(before, withdraw); if (withdraw || (moved != BankUnchanged && moved != BankUnread) || depositCardBefore < 0 || DepositCashBefore <= 0) return moved; double after; using (var f = Capture()) after = BankCardAmount(f, true); if (DepositShown(depositCardBefore, after, DepositCashBefore)) return "the notification didn't show, the bank card says " + SafehouseInfo.Money(after) + " banked, " + SafehouseInfo.Money(after - depositCardBefore) + " more"; return moved; } internal static bool DepositShown(double before, double after, double cash) { if (before < 0 || after < 0 || cash <= 0) return false; double up = after - before; return up >= cash * 0.5 && up <= cash * 1.5 + 1000; } string BankMovedTopBar(Header before, bool withdraw) { if (!withdraw && RobloxBar) { double onHand; using (var f = Capture()) onHand = BankCardOnHand(f); return onHand >= 0 && onHand < 1000 ? "the notification didn't show, the bank card says " + SafehouseInfo.Money(onHand) + " on hand" : BankHidden; } if (before == null || before.Cash < 0 || before.Banked < 0) return BankUnread; if (!withdraw && before.Cash < 1) return null; bool read = false; for (int look = 0; look < 4; look++) { if (look > 0) Wait(400); Header h; using (var f = Capture()) h = ReadHeader(f); if (h.Cash < 0 || h.Banked < 0) continue; read = true; bool moved = withdraw ? h.Banked < before.Banked * 0.01 + 1 && h.Cash > before.Cash : h.Banked > before.Banked && (h.Cash < before.Cash * 0.05 + 1 || h.Banked - before.Banked >= before.Cash * 0.85); if (moved) return (withdraw ? "no notification, " : "the notification didn't show, ") + SafehouseInfo.Money(h.Cash) + " on hand and " + SafehouseInfo.Money(h.Banked) + " banked now"; } return read ? BankUnchanged : BankUnread; } DateTime withdrawSaid = DateTime.MinValue; public string WithdrawAll() { if (!OpenTab(Tab.Bank)) return null; OcrLine deposit, withdraw; int fee; string feeLine; Header before; for (int look = 0; ; look++) { using (var f = Capture()) { deposit = FindDepositAll(f, out withdraw); fee = ReadBankFee(f, out feeLine); before = ReadHeader(f); } if ((deposit != null && withdraw != null && fee >= 0) || look >= 3) break; Wait(600); } if (deposit == null || withdraw == null || fee < 0 || Parse.Key(withdraw.Text) != "WITHDRAWAII") { if (DateTime.UtcNow >= withdrawSaid.AddHours(1)) { withdrawSaid = DateTime.UtcNow; log("Bank: WITHDRAW ALL wasn't pressed - " + (deposit == null ? "the bank card's DEPOSIT ALL didn't read" : withdraw == null || Parse.Key(withdraw.Text) != "WITHDRAWAII" ? "WITHDRAW ALL beside it didn't read" : "its fee line didn't read")); } Snapshot("bank withdraw not read", 60); return null; } CheckForPurchaseWindow(); NotDisconnected(); if (OnlyNavigate) RefuseInCheck(withdraw.Text); Click(withdraw.CenterX, withdraw.CenterY, 200); return BankToast() ?? BankMoved(before, true); } public sealed class BankAmountBox { public OcrLine DepositAll, WithdrawAll, Deposit, Withdraw; public Rectangle Box; public string Why; public Point At { get { return new Point(Box.Right - Box.Height / 2, Box.Y + Box.Height / 2); } } } public BankAmountBox ReadAmountBox(Frame f) { var r = new BankAmountBox(); OcrLine wAll; var dAll = FindDepositAll(f, out wAll); if (dAll == null || wAll == null) { r.Why = "the bank card's DEPOSIT ALL and WITHDRAW ALL didn't read"; return r; } r.DepositAll = dAll; r.WithdrawAll = wAll; var area = PageArea(f); int s = PageScale(f); var reads = new List>(); foreach (int zoom in new[] { s, 1, 3 }.Distinct()) { foreach (var prep in new[] { Prep.None, Prep.DarkText, Prep.WhiteText }) { reads.Add(Ocr.Read(f, area, zoom, prep)); if (PairAmountButtons(dAll, wAll, reads, out r.Deposit, out r.Withdraw)) break; } if (r.Withdraw != null) break; } if (r.Withdraw == null) { r.Why = "DEPOSIT and WITHDRAW over DEPOSIT ALL and WITHDRAW ALL didn't read"; return r; } r.Box = AmountBoxOver(r.Deposit, r.Withdraw, dAll); if (r.Box.Width < 20 || r.Box.Height < 8 || !new Rectangle(0, 0, f.Width, f.Height).Contains(r.Box)) return new BankAmountBox { Why = "the Amount box fell outside the picture" }; return r; } internal static bool PairAmountButtons(OcrLine dAll, OcrLine wAll, List> reads, out OcrLine dep, out OcrLine wd) { dep = wd = null; var all = reads.SelectMany(l => l).ToList(); foreach (var d in all.Where(l => Parse.Key(l.Text) == "DEPOSIT")) foreach (var w in all.Where(l => Parse.Key(l.Text) == "WITHDRAW")) if (AmountButtonsWhy(dAll, wAll, d, w) == null) { dep = d; wd = w; return true; } return false; } internal static string AmountButtonsWhy(OcrLine dAll, OcrLine wAll, OcrLine dep, OcrLine wd) { if (dAll == null || wAll == null || dep == null || wd == null) return "not read"; if (Parse.Key(dAll.Text) != "DEPOSITAII" || Parse.Key(wAll.Text) != "WITHDRAWAII") return "not the card's ALL buttons"; if (Parse.Key(dep.Text) != "DEPOSIT" || Parse.Key(wd.Text) != "WITHDRAW") return "not exactly DEPOSIT and WITHDRAW"; int h = Math.Max(8, dAll.Box.Height); int dy = dAll.CenterY - dep.CenterY, wy = wAll.CenterY - wd.CenterY; if (dy < 2 * h || dy > 6 * h || Math.Abs(wy - dy) > h) return "not a row above the ALL buttons"; if (Math.Abs(dep.CenterY - wd.CenterY) > h || wd.Box.X <= dep.Box.Right) return "not side by side"; if (Math.Abs(dep.CenterX - dAll.CenterX) > dAll.Box.Width / 2 || Math.Abs(wd.CenterX - wAll.CenterX) > wAll.Box.Width / 2) return "not over their ALL buttons"; return null; } internal static Rectangle AmountBoxOver(OcrLine dep, OcrLine wd, OcrLine dAll) { int dy = dAll.CenterY - dep.CenterY; return Rectangle.FromLTRB(dep.Box.X - dy / 2, dep.CenterY - dy * 16 / 10, wd.Box.Right + dy / 2, dep.CenterY - dy * 7 / 10); } List AmountBoxReads(Frame f, Rectangle box) { var reads = new List(); foreach (int s in new[] { 2, 3 }) foreach (var prep in new[] { Prep.None, Prep.DarkText, Prep.GreyText, Prep.WhiteText, Prep.Contrast }) { string t = string.Join(" ", Ocr.Read(f, box, s, prep).Select(l => l.Text)).Trim(); if (t.Length > 0) reads.Add(t); } return reads; } internal static bool AmountWord(string t) { return View.Distance(Parse.Key(t).Replace("RN", "M"), "AMOUNT") <= 1; } internal static long AmountNumber(string t) { string s = Regex.Replace(t ?? "", @"[\s,]", ""); if (s.StartsWith("$")) s = s.Substring(1); if (s.Length > 1 && "|Il!".IndexOf(s[s.Length - 1]) >= 0) s = s.Substring(0, s.Length - 1); long n; return Regex.IsMatch(s, "^[0-9]{1,15}$") && long.TryParse(s, out n) ? n : -1; } internal static bool AmountShort(string t) { return Regex.IsMatch(Regex.Replace(t ?? "", @"[\s,]", ""), @"^\$?[0-9]+(\.[0-9]+)?[kKmMbBtT]$"); } internal static bool AmountShows(List reads, long want) { string w = want.ToString(System.Globalization.CultureInfo.InvariantCulture); var nums = reads.Select(AmountNumber).Where(n => n >= 0).Select(n => n.ToString(System.Globalization.CultureInfo.InvariantCulture)).ToList(); Func piece = n => { if (n.Length >= w.Length) return false; int at = 0; foreach (char c in n) { at = w.IndexOf(c, at); if (at < 0) return false; at++; } return true; }; Func nearly = n => n.Length == w.Length && n != w && Enumerable.Range(0, w.Length).Count(i => n[i] != w[i]) <= 2; nums.RemoveAll(n => n == w + "1"); int exact = nums.Count(n => n == w), near = nums.Count(nearly); bool nearOk = exact >= 3 && exact > near; return exact >= 2 && nums.All(n => n == w || piece(n) || (nearOk && nearly(n))); } public string WithdrawAmount(long amount, out string why, out bool seen) { why = null; seen = false; if (amount < 1) { why = "no amount"; return null; } if (!OpenTab(Tab.Bank)) { why = "the Bank didn't open"; return null; } BankAmountBox box = null; List reads = null; int fee = -1; string feeLine; Header before = null; double cardBanked = -1; for (int look = 0; ; look++) { using (var f = Capture()) { box = ReadAmountBox(f); fee = ReadBankFee(f, out feeLine); before = ReadHeader(f); if (box.Why == null) reads = AmountBoxReads(f, box.Box); if (cardBanked < 0) cardBanked = BankCardAmount(f, true); } if ((box.Why == null && fee >= 0) || look >= 2) break; Wait(600); } if (box.Why != null) { why = box.Why; return null; } if (fee < 0) { why = "its fee line didn't read"; return null; } if (CardTooLow(cardBanked, amount)) { why = "the bank card shows only " + SafehouseInfo.Money(cardBanked) + " banked"; return null; } if (!reads.Any(t => AmountWord(t) || AmountNumber(t) >= 0 || AmountShort(t))) { why = "the Amount box didn't read" + (reads.Count > 0 ? " (\"" + reads[0] + "\")" : ""); return null; } if (OnlyNavigate) RefuseInCheck("Amount"); CheckForPurchaseWindow(); NotDisconnected(); var at = box.At; Click(at.X, at.Y, 300); Check(); Win.TypeKeys(new string('\b', 20) + amount.ToString(System.Globalization.CultureInfo.InvariantCulture)); Wait(300); BankAmountBox again; using (var f = Capture()) { again = ReadAmountBox(f); reads = again.Why == null ? AmountBoxReads(f, again.Box) : new List(); } if (again.Why != null) { why = "the card didn't read again after typing (" + again.Why + ")"; return null; } int h = Math.Max(8, box.Withdraw.Box.Height); if (Math.Abs(again.Withdraw.CenterX - box.Withdraw.CenterX) > h || Math.Abs(again.Withdraw.CenterY - box.Withdraw.CenterY) > h) { why = "WITHDRAW moved after typing"; return null; } if (!AmountShows(reads, amount)) { why = "the Amount box read " + (reads.Count > 0 ? "\"" + string.Join("\" / \"", reads.Distinct().Take(3)) + "\"" : "nothing") + ", not " + amount; return null; } var withdraw = again.Withdraw; if (Parse.Key(withdraw.Text) != "WITHDRAW") throw new NeverPressException(withdraw.Text, "not the bank card's WITHDRAW"); CheckForPurchaseWindow(); NotDisconnected(); if (OnlyNavigate) RefuseInCheck(withdraw.Text); Click(withdraw.CenterX, withdraw.CenterY, 200); string toast = BankToast(); string moved = AmountMoved(before, amount, out seen); if (!seen && toast != null && moved == BankUnread) seen = true; return toast != null ? toast.Trim() + (seen && moved != BankUnread ? " - " + moved : "") : moved; } internal static bool CardTooLow(double cardBanked, long amount) { return cardBanked >= 0 && cardBanked < amount; } string AmountMoved(Header before, long amount, out bool seen) { seen = false; if (before == null || before.Cash < 0 || before.Banked < 0) return BankUnread; bool read = false; for (int look = 0; look < 4; look++) { if (look > 0) Wait(400); Header h; using (var f = Capture()) h = ReadHeader(f); if (h.Cash < 0 || h.Banked < 0) continue; read = true; if (h.Cash - before.Cash >= amount * 0.5 || before.Banked - h.Banked >= amount * 0.5) { seen = true; return SafehouseInfo.Money(h.Cash) + " on hand and " + SafehouseInfo.Money(h.Banked) + " banked now"; } } return read ? BankUnchanged : BankUnread; } static bool IsStartButton(string t) { string k = Parse.Key(t); return k == "START" || k == "IOCKED"; } List> OpListRows(List lines) { var rows = new List>(); foreach (var b in lines.Where(l => IsStartButton(l.Text))) { if (!lines.Any(u => u.CenterY > b.Box.Bottom && u.CenterY < b.Box.Bottom + 2.5 * b.Box.Height && Math.Abs(u.CenterX - b.CenterX) < 4 * b.Box.Height && (Parse.Has(u.Text, "UP FRONT") || Parse.Has(u.Text, "REQUIRES")))) continue; var name = lines.Where(l => l.Box.Right < b.Box.X - 4 * b.Box.Height && Math.Abs(l.CenterY - b.CenterY) < 1.6 * b.Box.Height && (!Bridge.On || l.CenterY < b.CenterY + 0.6 * b.Box.Height) && !l.Text.Contains(":") && !l.Text.Contains("$") && Regex.Matches(l.Text, "[A-Za-z]").Count >= 6) .OrderByDescending(l => l.Box.X).FirstOrDefault(); if (name != null) rows.Add(new KeyValuePair(name, b)); } return rows; } public List ReadOpSlots(Frame f) { var p = PageArea(f); var lines = Ocr.Read(f, p, PageScale(f), Prep.None); var list = OpListRows(lines); int listTop = list.Count > 0 ? list.Min(r => r.Key.Box.Y) - 4 : p.Bottom; var listWords = lines.Where(l => Parse.Has(l.Text, "POSSIBLE REWARDS") || Parse.Has(l.Text, "UP FRONT")).ToList(); if (listWords.Count > 0) listTop = Math.Min(listTop, listWords.Min(l => l.Box.Y) - 4); var head = lines.Where(l => Parse.Key(l.Text) == "OPERATIONS" || Parse.Key(l.Text) == "PERATIONS").OrderBy(l => l.Box.Y).FirstOrDefault(); var top = SplitSlotTitles(f, lines.Where(l => l.Box.Bottom < listTop && (head == null || l.CenterY > head.Box.Bottom)).ToList(), p); var titles = top.Where(l => { string k = Parse.Key(l.Text); if (k == "OPENSIOT" || k == "IOCKED") return true; if (k.Contains("PERATION") || k.Contains("OFFIINE") || k == "READY" || k.Contains("FINISH") || k.Contains("CANCEI") || k.Contains("COIIECT") || k.Contains("GAMEPASS") || k.StartsWith("UNIOCK")) return false; return KnownOp(l.Text) != null || (Regex.Matches(l.Text, "[A-Za-z]").Count >= 8 && !l.Text.Contains("$") && !Parse.Has(l.Text, "XP") && !l.Text.Contains(":")); }).OrderBy(l => l.Box.Y).ToList(); var grid = new List>(); double column = p.Width / 4.1; foreach (var t in titles) { var row = grid.FirstOrDefault(r => { int dy = Math.Abs(r[0].CenterY - t.CenterY); return dy < 4 * t.Box.Height || (dy < 8 * t.Box.Height && r.All(o => Math.Abs(o.CenterX - t.CenterX) > column / 2)); }); if (row == null) grid.Add(row = new List()); row.Add(t); } double pitch = grid.Where(r => r.Count > 1).Select(r => { var xs = r.Select(t => t.CenterX).OrderBy(x => x).ToList(); return (double)(xs.Last() - xs.First()) / (xs.Count - 1); }) .DefaultIfEmpty(p.Width / 4.1).First(); var slots = new List(); for (int ri = 0; ri < grid.Count; ri++) { int rowBottom = ri + 1 < grid.Count ? grid[ri + 1].Min(t => t.Box.Y) : listTop; foreach (var t in grid[ri].OrderBy(x => x.CenterX)) { var s = new OpSlot { Index = slots.Count }; string tk = Parse.Key(t.Text); if (tk == "IOCKED") s.Locked = true; else if (tk != "OPENSIOT") s.Name = t.Text; var inside = top.Where(l => l != t && Math.Abs(l.CenterX - t.CenterX) < pitch * 0.45 && l.CenterY > t.Box.Bottom && l.CenterY < rowBottom); foreach (var l in inside) { string k = Parse.Key(l.Text); if (k.Contains("IOCKED") || k.Contains("GAMEPASS")) s.Locked = true; if (k.Contains("GAMEPASS")) s.Pass = true; if (k.Contains("FINISHNOW") || k.Contains("CANCEI") || k.Contains("IEFT")) s.Running = true; if (k == "READY" || k.StartsWith("READ") && k.Length <= 6 || k.Contains("COMPIETE") || k.Contains("FINISHED")) s.Done = true; if (k.StartsWith("COIIECT") || k.StartsWith("CIAIM")) s.Collect = l; } if (s.Running && s.Collect == null) { int total = KnownOpSeconds(s.Name); int y = t.CenterY + (int)(1.55 * t.Box.Height), half = (int)(pitch * 0.43); double fill = SlotBarFill(f, t.CenterX - half, t.CenterX + half, t.Box.Bottom, t.Box.Bottom + 4 * t.Box.Height); if (fill < 0) fill = f.FillRatio(t.CenterX - half, t.CenterX + half, y, BLUE, 30); if (total > 0) s.SecondsLeft = (int)((1 - fill) * total); } if (s.Collect != null) s.Running = false; slots.Add(s); } } return slots; } static double SlotBarFill(Frame f, int x0, int x1, int yFrom, int yTo) { x0 = Math.Max(0, x0); x1 = Math.Min(f.Width - 1, x1); for (int y = Math.Max(0, yFrom); y <= Math.Min(f.Height - 1, yTo); y++) { int left = -1; for (int x = x0; x <= x1; x++) if (f.Near(x, y, BLUE, 30)) { left = x; break; } if (left < 0) continue; int right = left, lastBlue = left, gap = 0; for (int x = left; x <= x1 && gap <= 6; x++) { var c = f.Pixel(x, y); if (f.Near(x, y, BLUE, 30)) { lastBlue = right = x; gap = 0; continue; } int avg = (c.R + c.G + c.B) / 3; if (avg <= 20 || Math.Min(c.R, Math.Min(c.G, c.B)) >= 150) { right = x; gap = 0; } else gap++; } if (right - left < 0.3 * (x1 - x0)) continue; return (double)(lastBlue - left + 1) / (right - left + 1); } return -1; } static List SplitSlotTitles(Frame f, List lines, Rectangle page) { double slot = page.Width / 4.1; var all = new List(); foreach (var l in lines) { string k = Parse.Key(l.Text); bool twice = Regex.Matches(k, "OPENSIOT").Count >= 2; if (!twice && (l.Box.Width < 0.9 * slot || k.Contains("OFFIINE") || k.Contains("PERATION"))) { all.Add(l); continue; } int h = Math.Max(8, l.Box.Height); var words = Ocr.ReadWords(f, Rectangle.Intersect(Rectangle.Inflate(l.Box, h, h / 2), new Rectangle(0, 0, f.Width, f.Height)), View.ScaleFor(h), Prep.None, true) .Where(w => w.CenterY >= l.Box.Y && w.CenterY <= l.Box.Bottom).OrderBy(w => w.Box.X).ToList(); var groups = new List>(); foreach (var w in words) { var last = groups.Count > 0 ? groups[groups.Count - 1] : null; if (last == null || w.Box.X - last[last.Count - 1].Box.Right > h) groups.Add(last = new List()); last.Add(w); } if (groups.Count < 2) { all.Add(l); continue; } foreach (var g in groups) all.Add(new OcrLine { Text = string.Join(" ", g.Select(w => w.Text)), Box = g.Skip(1).Aggregate(g[0].Box, (r, w) => Rectangle.Union(r, w.Box)) }); } return all; } public void CollectOp(OpSlot s) { Press(s.Collect, 1200); } static readonly string[] KnownOps = { "Fence a Hot Shipment|0.1667|20", "Warehouse Job|0.5|70", "Armored Truck Route|1|180", "The Long Con|2|675", "Skim the Counting House|3|1200", "Run the Smuggler's Coast|4|1800", "The Diamond District Job|6|3150", "Topple the Kovac Empire|8|4800", "Hijack the Winter Freight|10|6750", "Rig the Midnight Auction|12|9000", "The Syndicate Summit|16|15000", "Own the Front Page|10|12000", "Empty the City Mint|12|18000", "Dissolve the Charter|16|27000", "Tap the Courier Exchange|10|18000", "Take the Gold Train Whole|12|24000", "Empty the Sovereign Vault|16|36000", "Open the Old Pilgrim Roads|10|24000", "Empty the Elders' Treasury|12|33000", "Overthrow the First Table|16|48000", "Tax the Silk Road|10|33000", "Intercept the Supply Caravan|12|45000", "Rewrite the Oath|16|67500", }; public static readonly string[] OpNames = KnownOps.Select(o => o.Split('|')[0]).ToArray(); public static bool SameOp(string read, string picked) { string a = Parse.Key(read), b = Parse.Key(picked); return a.Length >= 5 && (a == b || Levenshtein(a, b) <= 2); } public static string OpLabel(string name) { int s = KnownOpSeconds(name); return s <= 0 ? name : name + (s < 3600 ? " (" + Math.Round(s / 60.0) + " min)" : " (" + s / 3600 + " h)"); } static string[] KnownOp(string name) { string k = Parse.Key(name); if (k.Length < 5) return null; foreach (var row in KnownOps) { var p = row.Split('|'); if (Parse.Key(p[0]) == k || Levenshtein(Parse.Key(p[0]), k) <= 2) return p; } return null; } public static int KnownOpSeconds(string name) { var p = KnownOp(name); return p == null ? -1 : (int)(double.Parse(p[1], System.Globalization.CultureInfo.InvariantCulture) * 3600); } static int KnownOpXp(string name) { var p = KnownOp(name); return p == null ? -1 : int.Parse(p[2]); } public List ReadOpRows(Frame f, int page) { var lines = Ocr.Read(f, PageArea(f), PageScale(f), Prep.None); var rows = new List(); foreach (var pair in OpListRows(lines)) { OcrLine l = pair.Key, b = pair.Value; int h = Math.Max(8, b.Box.Height); var r = new OpRow { Name = l.Text.Trim(), Y = l.Box.Y, Page = page, StartAt = new Point(b.CenterX, b.CenterY), StartText = b.Text, CanStart = Parse.Key(b.Text) == "START" && GoldIsh(f, b.Box.X - Math.Max(4, h / 2), b.CenterY), }; r.Seconds = KnownOpSeconds(r.Name); r.Xp = KnownOpXp(r.Name); OcrLine chips = null; if (r.Seconds < 0) foreach (var s in lines) { if (s == l || s.Box.Y <= l.Box.Y + l.Box.Height / 2 || s.Box.Y >= l.Box.Bottom + 1.8 * l.Box.Height || s.Box.Right > b.Box.X || s.Box.X < l.Box.X - 3 * l.Box.Height) continue; chips = chips ?? s; if (r.Seconds < 0) r.Seconds = Parse.Duration(s.Text); var m = Regex.Match(s.Text, @"([0-9][0-9,]*)\s*XP", RegexOptions.IgnoreCase); if (m.Success) r.Xp = int.Parse(m.Groups[1].Value.Replace(",", "")); } if (r.Seconds < 0 && chips != null) { int ch = Math.Max(8, chips.Box.Height); var timeChip = Rectangle.Intersect(new Rectangle(chips.Box.X, chips.Box.Y - ch / 3, Math.Min(chips.Box.Width, 7 * ch), ch + 2 * (ch / 3)), new Rectangle(0, 0, f.Width, f.Height)); foreach (var t in new[] { new { S = View.ScaleFor(ch) + 1, P = Prep.None }, new { S = View.ScaleFor(ch) + 1, P = Prep.WhiteSoft }, new { S = View.ScaleFor(ch) + 2, P = Prep.Contrast } }) if ((r.Seconds = Parse.Duration(Ocr.ReadText(f, timeChip, t.S, t.P).Replace('o', '0').Replace('O', '0'))) > 0) break; } rows.Add(r); } return rows; } public void StartOp(OpRow r) { Press(r.StartText, r.StartAt.X, r.StartAt.Y, 1300); } public void OpsTop() { ScrollPage(0.71, 40); } public void OpsDown(int notches) { ScrollPage(0.71, -notches); } public static int OpsStep(List rows) { return rows.Count < 2 ? 1 : Math.Max(1, Math.Min(5, (rows.Max(r => r.Y) - rows.Min(r => r.Y)) / 100)); } public int PlaytimeNextSeconds = -1; static int NextPlaytime(List rewards) { if (rewards.Any(l => Parse.Key(l.Text) == "CIAIM")) return 0; int best = int.MaxValue; foreach (var l in rewards) { var m = Regex.Match(l.Text.Trim(), @"^(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$"); if (!m.Success) continue; int s = (m.Groups[1].Success ? int.Parse(m.Groups[1].Value) * 3600 : 0) + int.Parse(m.Groups[2].Value) * 60 + int.Parse(m.Groups[3].Value); best = Math.Min(best, s); } if (best != int.MaxValue) return best; return rewards.Count(l => Parse.Key(l.Text) == "CIAIMED") >= 5 ? -2 : -1; } public bool PlaytimeExpHeld; static List ExpClaims(List rewards) { int half = CardHalf(rewards); return rewards.Where(c => Parse.Key(c.Text) == "CIAIM") .Where(c => rewards.Any(l => l.Box.Y < c.Box.Y && Math.Abs(l.CenterX - c.CenterX) < half && ExpWord(l))).ToList(); } static readonly Regex CardHeadRx = new Regex(@"^(\d{1,3})\s*M[I1l]N$", RegexOptions.IgnoreCase); static bool ExpWord(OcrLine l) { return Regex.IsMatch(Parse.Key(l.Text), @"^[0-9OIS]{0,2}X?EXP$"); } static int CardHalf(List rewards) { var heads = rewards.Where(l => CardHeadRx.IsMatch(l.Text.Trim())).Select(l => l.CenterX).OrderBy(x => x).ToList(); var claims = rewards.Where(l => Parse.Key(l.Text) == "CIAIM" || Parse.Key(l.Text) == "CIAIMED").ToList(); return heads.Count >= 2 ? Enumerable.Range(1, heads.Count - 1).Min(i => heads[i] - heads[i - 1]) / 2 : claims.Count > 0 ? 6 * Math.Max(8, claims[0].Box.Height) : 0; } public int PlaytimeExpSeconds = -1; internal static int ExpCardSeconds(List rewards, List played) { var exp = rewards.Where(ExpWord).OrderBy(l => l.Box.Y).FirstOrDefault(); if (exp == null) return -3; int half = CardHalf(rewards); if (half <= 0) half = 6 * Math.Max(8, exp.Box.Height); var col = rewards.Where(l => Math.Abs(l.CenterX - exp.CenterX) < half).ToList(); if (col.Any(l => l.Box.Y > exp.Box.Y && Parse.Key(l.Text) == "CIAIM")) return 0; if (col.Any(l => l.Box.Y > exp.Box.Y && Parse.Key(l.Text) == "CIAIMED")) return -2; foreach (var l in col.Where(x => x.Box.Y > exp.Box.Y)) { var m = Regex.Match(l.Text.Trim(), @"^(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$"); if (m.Success) return Math.Max(1, (m.Groups[1].Success ? int.Parse(m.Groups[1].Value) * 3600 : 0) + int.Parse(m.Groups[2].Value) * 60 + int.Parse(m.Groups[3].Value)); } var head = col.Where(l => l.Box.Y < exp.Box.Y && CardHeadRx.IsMatch(l.Text.Trim())).OrderBy(l => l.Box.Y).FirstOrDefault(); var pm = (played ?? new List()).Select(l => Regex.Match(l.Text, @"PLAYED\s*TODAY\s*:?\s*(\d{1,2}):(\d{2}):(\d{2})", RegexOptions.IgnoreCase)).FirstOrDefault(m => m.Success); if (head == null || pm == null) return -1; int left = int.Parse(CardHeadRx.Match(head.Text.Trim()).Groups[1].Value) * 60 - (int.Parse(pm.Groups[1].Value) * 3600 + int.Parse(pm.Groups[2].Value) * 60 + int.Parse(pm.Groups[3].Value)); return left > 0 ? left : -1; } public int ClaimPlaytimeRewards(bool exp = false, bool expOnly = false) { PlaytimeNextSeconds = -1; PlaytimeExpHeld = false; PlaytimeExpSeconds = -1; if (!OpenTab(Tab.Safehouse)) return 0; ScrollPage(0.54, 15); for (int look = 0; look < 4; look++) { List lines; using (var f = Capture()) lines = Ocr.Read(f, PageArea(f), PageScale(f), Prep.None); var title = lines.FirstOrDefault(l => Parse.Has(l.Text, "PLAYTIME REWARDS")); var claimable = lines.Any(l => Parse.Key(l.Text) == "CIAIM" || Parse.Key(l.Text) == "CIAIMED"); if (title != null && claimable) { var stats = lines.FirstOrDefault(l => Parse.Has(l.Text, "YOUR STATS") && l.Box.Y > title.Box.Bottom); int bottom = stats != null ? stats.Box.Y : int.MaxValue; var rewards = lines.Where(l => l.Box.Y > title.Box.Bottom && l.Box.Y < bottom).ToList(); var expAt = ExpClaims(rewards); PlaytimeExpHeld = !exp && expAt.Count > 0; PlaytimeExpSeconds = ExpCardSeconds(rewards, lines.Where(l => l.Box.Bottom <= title.Box.Bottom + title.Box.Height).ToList()); if (exp && expAt.Count > 0) PlaytimeExpSeconds = -2; PlaytimeNextSeconds = NextPlaytime(rewards.Where(l => !PlaytimeExpHeld || !expAt.Contains(l)).ToList()); Func isExp = c => expAt.Any(e => Math.Abs(e.CenterX - c.CenterX) < Math.Max(8, e.Box.Height) && Math.Abs(e.CenterY - c.CenterY) < Math.Max(8, e.Box.Height)); return ClaimButtons(l => l.Box.Y > title.Box.Bottom && l.Box.Y < bottom && (isExp(l) ? exp : !expOnly)); } ScrollPage(0.54, -4); } return 0; } public int ClaimContracts() { if (!OpenTab(Tab.Contracts)) return -1; OcrLine tasks; using (var f = Capture()) { var p = PageArea(f); tasks = Ocr.Read(f, new Rectangle(p.Left, p.Top, p.Width, Math.Max(60, p.Height / 4)), PageScale(f), Prep.None) .FirstOrDefault(l => Parse.Key(l.Text) == "TASKS"); } if (tasks != null) Press(tasks, 700); ScrollPage(0.65, 30); int n = 0; string lastSig = null; for (int page = 0; page < 12; page++) { n += ClaimButtons(l => true); string sig; using (var f = Capture()) sig = TaskListSignature(f); if (sig == lastSig) break; lastSig = sig; ScrollPage(0.65, -ListNotches(4)); } return n; } public string TaskListSignature(Frame f) { var p = PageArea(f); return string.Join("|", Ocr.Read(f, p, PageScale(f), Prep.None) .Where(l => l.CenterX < p.Left + p.Width / 2 && Regex.Matches(l.Text, "[A-Za-z]").Count >= 4 && !Regex.IsMatch(l.Text, @"\d")) .OrderBy(l => l.Box.Y).Select(l => Parse.Key(l.Text))); } bool claimSeen; int ClaimButtons(Func where) { int claimed = 0; for (int round = 0; round < 6; round++) { OcrLine target; using (var f = Capture()) target = Ocr.Read(f, PageArea(f), PageScale(f), Prep.None).FirstOrDefault(l => Parse.Key(l.Text) == "CIAIM" && where(l)); if (target == null) break; if (!claimSeen) { claimSeen = true; Snapshot("claim button", 0, true); } Press(target, 1000); claimed++; using (var f = Capture()) ClearPopupIfAny(f); } return claimed; } static Rectangle TabStrip(Rectangle p) { return new Rectangle(p.Left, p.Top, p.Width, Math.Max(60, p.Height * 2 / 5)); } public OcrLine FindCratesTab(Frame f) { var strip = TabStrip(PageArea(f)); int s = PageScale(f); return Ocr.Read(f, strip, s, Prep.None).FirstOrDefault(l => Parse.Key(l.Text) == "CRATES") ?? Ocr.Read(f, strip, s, Prep.DarkText).FirstOrDefault(l => Parse.Key(l.Text) == "CRATES") ?? Ocr.ReadWords(f, strip, s, Prep.DarkText).FirstOrDefault(l => Parse.Key(l.Text) == "CRATES") ?? Ocr.ReadWords(f, strip, s, Prep.None).FirstOrDefault(l => Parse.Key(l.Text) == "CRATES"); } public int OpenCrates(out bool done) { done = false; if (!OpenTab(Tab.Inventory)) return 0; OcrLine tab; bool cratesShow; using (var f = Capture()) { var p = PageArea(f); int s = PageScale(f); tab = FindCratesTab(f); cratesShow = tab == null && Ocr.Read(f, p, s, Prep.None).Any(l => Parse.Key(l.Text).EndsWith("CRATE")); } if (tab == null && !cratesShow) { log("Couldn't find the CRATES tab in the inventory"); Snapshot("inventory without crates tab", 60); return 0; } if (tab != null) Press(tab, 800); int tabBottom = tab != null ? tab.Box.Bottom + tab.Box.Height : 0; int opened = 0, menuTries = 0; bool openRetried = false; bool menuWaited = false; string chosen = "", chosenTag = ""; for (int round = 0; round < 60; round++) { List lines; Rectangle area; List cards; bool onlyCrates; using (var f = Capture()) { area = PageArea(f); lines = Ocr.Read(f, area, PageScale(f), Prep.None); onlyCrates = tab != null && ChosenTab(f, tab.Box); int top = Math.Max(area.Top, tabBottom); cards = ItemPictures(f, new Rectangle(area.Left, top, area.Width, area.Bottom - top)); } if (lines.Any(l => Parse.Has(l.Text, "NO CRATES"))) { done = true; break; } var open = lines.FirstOrDefault(l => Parse.Key(l.Text) == "OPENCRATE"); int many; var openAll = OpenAllIn(lines, open, out many); if (open == null) { var at = NextCrate(lines, cards, onlyCrates, Math.Max(tabBottom, area.Top + area.Height / 4), out chosen, out chosenTag); if (at == null) { done = onlyCrates || opened > 0; break; } if (menuTries > 0 && !menuWaited) { menuWaited = true; Wait(1200); continue; } menuWaited = false; if (++menuTries > 3) { if (LeaveCrate(chosen)) { log("Crates: " + chosen + " shows no OPEN CRATE - leaving it alone for 6 hours"); Snapshot("crate without open crate", 60); } break; } try { Press(chosen, at.Value.X, at.Value.Y, 900); } catch (NeverPressException) { LeaveCrate(chosen); } continue; } menuTries = 0; Press(openAll ?? open, 800); var got = new List(); int quiet = 0; for (int w = 0; w < (many > 0 ? 90 : 30); w++) { List reward; using (var f = Capture()) reward = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None); var collect = reward.FirstOrDefault(l => { string k = Parse.Key(l.Text); return k == "COIIECT" || k == "COIIECTAII"; }); var youGot = reward.FirstOrDefault(l => Parse.Has(l.Text, "YOU GOT")); if (collect != null) { var item = reward.Where(l => l.Box.Y > (youGot != null ? youGot.Box.Bottom : collect.Box.Y - 8 * collect.Box.Height) && l.Box.Bottom < collect.Box.Y - 4 && Math.Abs(l.CenterX - collect.CenterX) < 2 * collect.Box.Width && !Parse.Has(l.Text, "YOU GOT")).Select(l => l.Text).ToList(); got.Add(item.Count > 0 ? string.Join(" ", item) : "an item"); if (many > 0 && got.Count == 1) Snapshot("crates opened at once", 24 * 60, true); if (got.Count == 1 && chosenTag.Length > 0) Snapshot(chosenTag + " opened", 24 * 60, true); Press(collect, 900); if (many == 0) break; quiet = 0; continue; } if (got.Count > 0 && ++quiet > 6) break; Wait(300); } if (got.Count == 0) { CheckForPurchaseWindow(); bool eventUp; using (var f = Capture()) eventUp = CloseEventPopup(f); if (eventUp) { log("Crates: Roblox's \"Join Event\" window was over the crate - tried again next round"); break; } if (!openRetried) { openRetried = true; Wait(1000); continue; } if (LeaveCrate(chosen, 24)) { log("Crates: " + chosen + " didn't open - the game blocks some crates in some countries (it may open with a VPN). The bot tries it again in 24 hours"); openRetried = false; continue; } bool named = LeaveCrate(chosen); log(named ? "Crates: " + chosen + " opened but no COLLECT button showed up - stopping crates, and leaving that one alone for 6 hours" : "A crate opened but no COLLECT button showed up - stopping crates"); Snapshot("crate without collect", 60); break; } if (many > 0) { opened += many; log("Crates: opened " + many + " at once" + (chosen.Length > 0 ? " (" + chosen + ")" : "") + ", got " + string.Join(", ", got)); } else { opened++; log("Crate: got " + got[0] + (chosen.Length > 0 ? " from " + chosen : "")); } } return opened; } readonly Dictionary cratesLeft = new Dictionary(); bool LeftAlone(string name) { DateTime until; string k = Parse.Key(name); return k.Length > 0 && cratesLeft.TryGetValue(k, out until) && DateTime.UtcNow < until; } internal bool LeaveCrate(string name, int hours = 6) { string k = Parse.Key(name); if (k.Length == 0) return false; cratesLeft[k] = DateTime.UtcNow.AddHours(hours); return true; } public string CratesLeftText { get { return string.Join(";", cratesLeft.Where(kv => kv.Value > DateTime.UtcNow).Select(kv => kv.Key + "=" + kv.Value.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", System.Globalization.CultureInfo.InvariantCulture))); } set { cratesLeft.Clear(); foreach (var pair in (value ?? "").Split(';')) { int eq = pair.IndexOf('='); DateTime until; if (eq > 0 && DateTime.TryParse(pair.Substring(eq + 1), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AdjustToUniversal | System.Globalization.DateTimeStyles.AssumeUniversal, out until)) cratesLeft[pair.Substring(0, eq)] = until; } } } internal Point? NextCrate(List lines, List cards, bool onlyCrates, int below, out string name, out string tag) { foreach (var c in cards) { var pic = c; var tags = lines.Where(l => pic.Contains(l.CenterX, l.CenterY) && l.CenterY < pic.Y + pic.Height / 3 && Parse.Key(l.Text).EndsWith("CRATE")).ToList(); name = CardName(lines, pic); if ((!onlyCrates && tags.Count == 0 && !CrateName(name)) || LeftAlone(name)) continue; tag = tags.Count > 0 && Parse.Key(tags[0].Text) != "CRATE" ? tags[0].Text.Trim().ToLowerInvariant() : ""; return new Point(pic.X + pic.Width / 2, pic.Y + pic.Height / 2); } tag = ""; var first = lines.Where(l => l.Box.Y > below && CrateName(l.Text) && !Parse.Has(l.Text, "LEGENDARY") && !Parse.Has(l.Text, "EPIC") && !Parse.Has(l.Text, "RARE") && Parse.Key(l.Text) != "CRATE" && !LeftAlone(l.Text)) .OrderBy(l => l.Box.Y / Math.Max(1, 3 * l.Box.Height)).ThenBy(l => l.Box.X).FirstOrDefault(); name = first != null ? first.Text.Trim() : ""; if (first == null) return null; return new Point(first.CenterX, first.CenterY - (int)(2.2 * first.Box.Height)); } static bool CrateName(string text) { return Parse.Has(text, "CRATE") || Parse.Has(text, "STASH") || Parse.Has(text, "STRONGBOX") || Parse.Has(text, "DUFFEL") || Parse.Has(text, "LOCKBOX"); } static string CardName(List lines, Rectangle pic) { var name = lines.Where(l => l.CenterX > pic.X && l.CenterX < pic.Right && l.Box.Y >= pic.Bottom - 2 && l.CenterY < pic.Bottom + pic.Height * 55 / 100 && !l.Text.TrimStart().StartsWith("+") && !RarityKeys.Contains(Parse.Key(l.Text))) .OrderBy(l => l.Box.Y).Select(l => l.Text.Trim()); return string.Join(" ", name); } internal static bool ChosenTab(Frame f, Rectangle text) { int gold = 0, n = 0; for (int y = text.Top; y < text.Bottom; y++) for (int x = text.Left; x < text.Right; x++) { n++; if (GoldIsh(f, x, y)) gold++; } return n > 0 && gold * 4 >= n; } internal static List ItemPictures(Frame f, Rectangle area) { const int Edge = 0x3A3A3E, Inside = 0x18181A; area.Intersect(new Rectangle(0, 1, f.Width, f.Height - 5)); var found = new List(); int minW = Math.Max(50, area.Width / 20); for (int y = area.Top; y < area.Bottom; y++) for (int x = area.Left; x < area.Right; x += 8) { if (!f.Near(x, y, Edge, 6)) continue; int s = x, e = x; while (s > area.Left && f.Near(s - 1, y, Edge, 6)) s--; while (e + 1 < area.Right && f.Near(e + 1, y, Edge, 6)) e++; x = e; int m = (s + e) / 2, w = e - s + 1; if (w < minW || w * 4 > area.Width || f.Near(m, y - 1, Edge, 6)) continue; int t = y + 1; while (t < y + 5 && f.Near(m, t, Edge, 6)) t++; if (!f.Near(s + 3, t, Inside, 8) && !f.Near(e - 3, t, Inside, 8)) continue; int b = t; while (b < f.Height && f.Near(s, b, Edge, 6)) b++; b--; int h = b - y + 1; if (h * 100 < w * 65 || h * 100 > w * 87 || !f.Near(m, b, Edge, 6)) continue; if (found.Any(r => Math.Abs(r.X - s) < 4 && Math.Abs(r.Y - y) < 4)) continue; found.Add(new Rectangle(s, y, w, h)); } return found.OrderBy(r => r.Y / Math.Max(1, r.Height / 2)).ThenBy(r => r.X).ToList(); } static readonly Regex OpenAllRx = new Regex(@"^\s*OPEN\s*([0-9OoIlS]{1,3})\s*$", RegexOptions.IgnoreCase); public static OcrLine OpenAllIn(List lines, OcrLine open, out int many) { many = 0; if (open == null) return null; foreach (var l in lines) { var m = OpenAllRx.Match(l.Text); int n; if (m.Success && int.TryParse(Parse.Digits(m.Groups[1].Value), out n) && n >= 2 && Math.Abs(l.CenterX - open.CenterX) < open.Box.Width && l.Box.Y > open.Box.Y && l.Box.Y - open.Box.Bottom < 3 * Math.Max(8, open.Box.Height)) { many = n; return l; } } return null; } bool ClearPopupIfAny(Frame f) { var lines = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None); CheckForPurchasePrompt(lines); foreach (var l in lines) { string k = Parse.Key(l.Text); if (k == "DONE" || k == "OK" || k == "OKAY" || k == "CONTINUE" || k == "CIOSE" || k == "NICE" || k == "AWESOME" || k == "COOI" || k == "GOTIT") { Press(l, 800); return true; } } return CloseRulingFamily(f); } public sealed class Disconnect { public string Message = ""; public int Code = -1; public OcrLine Leave, Reconnect; public bool Teleport; public OcrLine Ok; public bool Foreign; string Words { get { return " " + Regex.Replace((Message ?? "").ToUpperInvariant(), "[^A-Z0-9]+", " ").Trim() + " "; } } public bool OtherDevice { get { return Code == 264 || Code == 273 || Regex.IsMatch(Words, " (ANOTHER|OTHER|DIFFERENT) DEVICE "); } } public bool Kicked { get { string w = Words; if (Code == 268 || Regex.IsMatch(w, @" (BAN|BANS|BANNED|BANNING|EXPLOIT\w*|CHEAT\w*|HACK\w*|MACRO\w*|AUTO ?CLICK\w*) ") || Regex.IsMatch(w, @" (NOT|CANNOT|CAN T|CANT|DON T|DONT|NEVER) REJOIN")) return true; if (Code == 278 || w.Contains(" DISCONNECTED FOR BEING IDLE ")) return false; if (Regex.IsMatch(w, @" SERVERS? (IS |ARE )?(BEING )?(RESTART\w*|REBOOT\w*|SHUT\w*|UPDAT\w*) ") || Regex.IsMatch(w, @" FOR (AN |A )?UPDATES? ")) return false; if (Code == 267 || Regex.IsMatch(w, @" KICK\w* ")) return true; return Reconnect == null && Leave != null && Code < 0; } } public override string ToString() { return (Message + (Code >= 0 ? " (Error Code: " + Code + ")" : "")).Trim(); } } public const string Disconnected = "Roblox disconnected the game"; static readonly Regex ErrorCodeText = new Regex(@"\bErr?[oa]r\s*C[oca0][a-z0-9]{1,3}\W{0,3}\s*([0-9OoIlSB|]{1,4})", RegexOptions.IgnoreCase); public Disconnect ReadDisconnect(Frame f) { var area = new Rectangle(f.Width * 18 / 100, f.Height * 12 / 100, f.Width * 64 / 100, f.Height * 80 / 100); Disconnect best = null; List first = null; foreach (int s in new[] { 2, 3 }) { var lines = Ocr.Read(f, area, s, Prep.None); if (first == null) first = lines; var d = DisconnectIn(f, lines); if (d == null) { if (best != null || !lines.Any(l => IsDialogTitle(l.Text) || ErrorCodeText.IsMatch(l.Text))) break; continue; } if (best == null) best = d; else { if (best.Leave == null && best.Reconnect == null && best.Ok == null) { best.Leave = d.Leave; best.Reconnect = d.Reconnect; best.Ok = d.Ok; } if (best.Code < 0) best.Code = d.Code; best.Teleport |= d.Teleport; if (d.Message.Length > best.Message.Length) best.Message = d.Message; } if (best.Code >= 0 && (best.Leave != null || best.Reconnect != null || best.Ok != null)) break; } if (best != null && best.Reconnect != null && best.Leave == null) { var r = best.Reconnect; int h = Math.Max(8, r.Box.Height), w = r.Box.Width; best.Leave = Ocr.Read(f, new Rectangle(r.Box.X - 3 * w, r.Box.Y - h, 3 * w - h / 2, 3 * h), View.ScaleFor(h), Prep.None, true) .FirstOrDefault(l => Parse.Key(l.Text) == "IEAVE" && Math.Abs(l.CenterY - r.CenterY) <= r.Box.Height); } return best ?? (first != null && DialogGrey(f) >= 0.4 ? ForeignWindow(f, first) : null); } static readonly Regex AnyCodeRx = new Regex(@"\([^()]{0,40}?[::]\s*([0-9]{3})\s*\)"); static double DialogGrey(Frame f) { int cx = f.Width / 2, cy = f.Height / 2, n = 0, grey = 0; for (int y = cy - 40; y <= cy + 40; y += 8) for (int x = cx - 100; x <= cx + 100; x += 10) { var c = f.Pixel(x, y); n++; if (Math.Abs(c.R - 0x39) <= 8 && Math.Abs(c.G - 0x3B) <= 8 && Math.Abs(c.B - 0x3D) <= 8) grey++; } return n == 0 ? 0 : (double)grey / n; } internal static Disconnect ForeignWindow(Frame f, List lines) { if (DialogGrey(f) < 0.4) return null; var code = lines.Select(l => AnyCodeRx.Match(l.Text)).FirstOrDefault(m => m.Success); bool buttons = false; for (int y = f.Height / 2; y < Math.Min(f.Height, f.Height / 2 + 260) && !buttons; y += 3) { int run = 0; for (int x = f.Width / 2 - 260; x < f.Width / 2 + 260; x++) { var c = f.Pixel(x, y); int mn = Math.Min(c.R, Math.Min(c.G, c.B)), mx = Math.Max(c.R, Math.Max(c.G, c.B)); run = mn >= 200 && mx - mn <= 30 ? run + 1 : 0; if (run >= 50) { buttons = true; break; } } } if (code == null && !buttons) return null; var d = new Disconnect { Foreign = true, Message = string.Join(" ", lines.Where(l => Math.Abs(l.CenterX - f.Width / 2) < f.Width / 5).Select(l => l.Text.Trim())) }; if (code != null) d.Code = int.Parse(code.Groups[1].Value); return d; } internal static Disconnect DisconnectIn(Frame f, List lines) { var title = lines.FirstOrDefault(l => IsDialogTitle(l.Text)); var code = lines.FirstOrDefault(l => ErrorCodeText.IsMatch(l.Text)); if (title == null && code == null) return null; int top = code != null ? code.CenterY : title.Box.Bottom + 2 * title.Box.Height; var words = new List(); foreach (var l in lines.Where(l => l.CenterY > top)) { string k = Parse.Key(l.Text); if (k.Length > 5 && k.StartsWith("IEAVE") && k.EndsWith("RECONNECT")) { int mid = l.Box.X + l.Box.Width * 5 / 14; words.Add(new OcrLine { Text = "Leave", Box = new Rectangle(l.Box.X, l.Box.Y, mid - l.Box.X - l.Box.Height / 2, l.Box.Height) }); words.Add(new OcrLine { Text = "Reconnect", Box = new Rectangle(mid + l.Box.Height / 2, l.Box.Y, l.Box.Right - mid - l.Box.Height / 2, l.Box.Height) }); } else words.Add(l); } var reconnect = words.Where(l => (Parse.Key(l.Text) == "RECONNECT" || Parse.Key(l.Text) == "RETRY") && (f == null || WhiteFace(f, l))).OrderByDescending(l => l.Box.Y).FirstOrDefault(); var leave = words.Where(l => Parse.Key(l.Text) == "IEAVE" && (f == null || WhiteFace(f, l) || (reconnect != null && Math.Abs(l.CenterY - reconnect.CenterY) <= reconnect.Box.Height && l.Box.Right < reconnect.Box.X))) .OrderByDescending(l => l.Box.Y).FirstOrDefault(); bool teleport = (title != null && IsTeleportTitle(title.Text)) || lines.Any(l => Regex.IsMatch(l.Text, @"\bTELEPORT", RegexOptions.IgnoreCase)); var ok = !teleport ? null : words.Where(l => (Parse.Key(l.Text) == "OK" || Parse.Key(l.Text) == "OKAY") && (f == null || WhiteFace(f, l))) .OrderByDescending(l => l.Box.Y).FirstOrDefault(); int evidence = (title != null ? 1 : 0) + (code != null ? 1 : 0) + (reconnect != null || leave != null || ok != null ? 1 : 0); if (evidence < 2) return null; var d = new Disconnect { Leave = leave, Reconnect = reconnect, Teleport = teleport, Ok = ok }; if (code != null) { var m = ErrorCodeText.Match(code.Text); int n; if (int.TryParse(Parse.Digits(m.Groups[1].Value), out n)) d.Code = n; } int from = title != null ? title.Box.Bottom : (code != null ? code.Box.Y - 6 * code.Box.Height : 0); int to = code != null ? code.Box.Bottom : top; int cx = (title ?? code).CenterX, half = Math.Max(120, (title ?? code).Box.Width * 2); var text = lines.Where(l => l != title && l.CenterY > from && l.CenterY <= to && Math.Abs(l.CenterX - cx) < half) .OrderBy(l => l.Box.Y).Select(l => l.Text.Trim()); d.Message = Regex.Replace(Regex.Replace(string.Join(" ", text), @"\(?\s*\bErr?[oa]r\s*C[oca0][a-z0-9]{1,3}\W.*$", "", RegexOptions.IgnoreCase), @"\s{2,}", " ").Trim(); return d; } static bool IsDialogTitle(string text) { string k = Parse.Key(text); return View.Distance(k, "DISCONNECTED") <= 2 || View.Distance(k, "CONNECTIONFAIIED") <= 2 || IsTeleportTitle(text); } static bool IsTeleportTitle(string text) { return View.Distance(Parse.Key(text), "TEIEPORTFAIIED") <= 2; } static bool WhiteFace(Frame f, OcrLine l) { int h = Math.Max(6, l.Box.Height), bright = 0; foreach (int x in new[] { l.Box.X - h * 6 / 10, l.Box.X - h, l.Box.Right + h * 6 / 10, l.Box.Right + h }) { var c = f.Pixel(x, l.CenterY); int min = Math.Min(c.R, Math.Min(c.G, c.B)), max = Math.Max(c.R, Math.Max(c.G, c.B)); if (min >= 200 && max - min <= 30) bright++; } return bright >= 3; } public bool PressDisconnect(bool reconnect, bool otherDeviceOk = false) { return PressDisconnect(() => { using (var f = Capture()) return ReadDisconnect(f); }, (x, y) => { Check(); Win.Click(x, y); }, reconnect, GuardText, otherDeviceOk); } internal static bool PressDisconnect(Func read, Action click, bool reconnect, Action guard = null, bool otherDeviceOk = false) { var d = read(); if (d == null || (d.OtherDevice && !otherDeviceOk) || d.Kicked) return false; var b = reconnect ? d.Reconnect : d.Leave; if (b == null) return false; if (guard != null) guard(b.Text); click(b.CenterX, b.CenterY); return true; } public bool PressTeleportOk() { return PressTeleportOk(() => { using (var f = Capture()) return ReadDisconnect(f); }, (x, y) => { Check(); Win.Click(x, y); }, GuardText); } internal static bool PressTeleportOk(Func read, Action click, Action guard = null) { var d = read(); if (d == null || !d.Teleport || d.Ok == null || d.OtherDevice || d.Kicked) return false; if (guard != null) guard(d.Ok.Text); click(d.Ok.CenterX, d.Ok.CenterY); return true; } public void LeaveByMenu() { Check(); Win.PressKey(0x1B); Wait(1500); Win.PressKey(0x4C); Wait(1500); Win.PressKey(0x0D); Wait(1000); } public bool KeepAlive() { Tab? open; using (var f = Capture()) { var v = ViewOf(f); open = v.Problem == null ? v.Selected(f) : null; } foreach (var t in Program.Whale ? new[] { Tab.Bank, Tab.Fight } : new[] { Tab.Safehouse, Tab.Bank, Tab.Jobs }) if (t != open && !NotInMenu(t) && OpenTab(t)) return true; return false; } public static readonly string[] StatNames = { "Max Energy", "Max Stamina", "Max Health", "Attack", "Defense", "Property Capacity" }; static readonly int[] StatSteps = { 2, 1, 10, 1, 1, 1 }; public int PointsCost = 1; internal static int StatNow(int stat, int value, int start, int pending) { return start < 0 ? value : Math.Max(value, start + pending * StatSteps[stat]); } static int SkillValue(Frame f, List lines, OcrLine row, OcrLine chip) { int h = Math.Max(8, row.Box.Height); Func number = t => { int v; t = (t ?? "").Replace(" ", ""); return Regex.IsMatch(t, @"^[0-9OoIlS][0-9OoIlS,.]{0,8}$") && int.TryParse(Parse.Digits(t), out v) ? v : -1; }; var near = lines.Where(l => l.Box.X >= row.Box.Right && l.Box.Right <= chip.Box.X + h / 2 && Math.Abs(l.CenterY - row.CenterY) < 2 * h) .Select(l => number(l.Text)).Where(v => v >= 0).ToList(); if (near.Count > 0) return near[0]; var strip = Rectangle.Intersect(Rectangle.FromLTRB(chip.Box.X - 9 * h, row.CenterY - h, chip.Box.X - h / 4, row.CenterY + 2 * h), new Rectangle(0, 0, f.Width, f.Height)); if (strip.Width < 8 || strip.Height < 8) return -1; int zoom = PopupScale(f); foreach (var t in new[] { Tuple.Create(zoom + 1, Prep.None), Tuple.Create(zoom + 2, Prep.None), Tuple.Create(zoom + 1, Prep.Contrast), Tuple.Create(zoom + 1, Prep.WhiteText), Tuple.Create(zoom, Prep.WhiteSoft), Tuple.Create(zoom + 2, Prep.WhiteSoft), Tuple.Create(zoom + 3, Prep.None) }) { var hit = Ocr.Read(f, strip, Math.Min(6, t.Item1), t.Item2).Select(l => number(l.Text)).Where(v => v >= 0).ToList(); if (hit.Count > 0) return hit[0]; } return -1; } static readonly Regex CostChipRx = new Regex(@"^\s*([0-9OoIlS]{1,3})?\s*P\s?[O0]\s?[I1l]\s?N\s?[T7](S?)\s*$", RegexOptions.IgnoreCase); internal static int ChipCost(string text) { var m = CostChipRx.Match(text ?? ""); int n; if (!m.Success) return -1; if (m.Groups[1].Success) return int.TryParse(Parse.Digits(m.Groups[1].Value), out n) ? n : -1; return m.Groups[2].Value.Length == 0 ? 1 : -1; } static readonly Regex AvailableRx = new Regex(@"([0-9OoIlS]{1,4})\s*AVA", RegexOptions.IgnoreCase); internal static bool ReadSkillRow(Frame f, List lines, Rectangle popup, int stat, out int available, out int cost, out Point plus) { int value; return ReadSkillRow(f, lines, popup, stat, false, out available, out cost, out plus, out value); } internal static bool ReadSkillRow(Frame f, List lines, Rectangle popup, int stat, bool wantValue, out int available, out int cost, out Point plus, out int value) { available = cost = value = -1; plus = Point.Empty; var have = lines.Select(l => AvailableRx.Match(l.Text)).FirstOrDefault(m => m.Success); int n; if (have != null && int.TryParse(Parse.Digits(have.Groups[1].Value), out n)) available = n; string name = Parse.Key(StatNames[stat]); var names = new HashSet(StatNames.Select(Parse.Key)); var column = lines.Where(l => names.Contains(Parse.Key(l.Text))).Select(l => l.Box.X).OrderBy(x => x).ToList(); int colX = column.Count > 0 ? column[column.Count / 2] : 0; var row = lines.Where(l => Parse.Key(l.Text) == name).OrderBy(l => Math.Abs(l.Box.X - colX)).FirstOrDefault(); if (row == null) return false; int h = Math.Max(8, row.Box.Height); Func, OcrLine> chipIn = ls => ls.Where(l => ChipCost(l.Text) >= 0 && l.Box.X > row.Box.Right && Math.Abs(l.CenterY - row.CenterY) < 2 * h) .OrderBy(l => Math.Abs(l.CenterY - row.CenterY)).FirstOrDefault(); var chip = chipIn(lines); var strip = Rectangle.Intersect(Rectangle.FromLTRB(row.Box.Right + h, row.CenterY - 2 * h, popup.Right, row.CenterY + 2 * h), new Rectangle(0, 0, f.Width, f.Height)); int zoom = PopupScale(f); foreach (var t in new[] { Tuple.Create(zoom + 1, Prep.None), Tuple.Create(zoom + 2, Prep.None), Tuple.Create(zoom + 1, Prep.Contrast) }) { if (chip != null || strip.Width < 8 || strip.Height < 8) break; chip = chipIn(Ocr.Read(f, strip, Math.Min(6, t.Item1), t.Item2)); } if (chip == null) return false; cost = ChipCost(chip.Text); if (wantValue) value = SkillValue(f, lines, row, chip); var runs = new List(); int runRight = -1; for (int x = Math.Min(popup.Right, f.Width - 1); x > chip.Box.Right; x--) { bool gold = GoldIsh(f, x, chip.CenterY); if (gold && runRight < 0) runRight = x; if (!gold && runRight >= 0) { runs.Add(new[] { x + 1, runRight }); runRight = -1; } } int gap = Math.Max(6, (int)(1.2 * chip.Box.Height)); var merged = new List(); foreach (var r in runs.OrderBy(r => r[0])) { var last = merged.Count > 0 ? merged[merged.Count - 1] : null; if (last != null && r[0] - last[1] <= Math.Max(gap, (int)(0.8 * Math.Min(r[1] - r[0], last[1] - last[0])))) last[1] = r[1]; else merged.Add(new[] { r[0], r[1] }); } var best = merged.OrderByDescending(r => r[1] - r[0]).FirstOrDefault(); if (best != null && best[1] - best[0] >= 4) plus = new Point((best[0] + best[1]) / 2, chip.CenterY); return true; } internal static Point ProfileClose(Frame f, OcrLine title, Rectangle popup) { int th = Math.Max(Math.Max(8, title.Box.Height), (int)(21 * Math.Min(f.Height / 1009.0, f.Width / 1790.0))); var area = Rectangle.FromLTRB(title.Box.Right + th, Math.Max(0, title.CenterY - th * 6 / 5), popup.Right, title.CenterY + th * 6 / 5); return CrossIn(f, area, th, false); } internal static Point CrossIn(Frame f, Rectangle area, int th, bool rightmost) { int top = Math.Max(0, area.Y), bottom = Math.Min(f.Height - 1, area.Bottom - 1); int left = Math.Max(0, area.X), right = Math.Min(f.Width - 1, area.Right - 1); if (right - left < 6 || bottom - top < 6) return Point.Empty; int w = right - left + 1, rows = bottom - top + 1; if ((long)w * rows > 4000000) return Point.Empty; var seen = new bool[w * rows]; var stack = new Stack(); Point best = Point.Empty; for (int y = top; y <= bottom; y++) for (int x = left; x <= right; x++) { int start = (y - top) * w + (x - left); if (seen[start] || !GreyAt(f, x, y)) continue; stack.Clear(); stack.Push(start); seen[start] = true; int x0 = x, x1 = x, y0 = y, y1 = y; while (stack.Count > 0) { int cur = stack.Pop(); int cx = left + cur % w, cy = top + cur / w; if (cx < x0) x0 = cx; if (cx > x1) x1 = cx; if (cy < y0) y0 = cy; if (cy > y1) y1 = cy; for (int dy = -1; dy <= 1; dy++) for (int dx = -1; dx <= 1; dx++) { int nx = cx + dx, ny = cy + dy; if ((dx == 0 && dy == 0) || nx < left || nx > right || ny < top || ny > bottom) continue; int ni = (ny - top) * w + (nx - left); if (seen[ni] || !GreyAt(f, nx, ny)) continue; seen[ni] = true; stack.Push(ni); } } int bw = x1 - x0 + 1, bh = y1 - y0 + 1; if (bw < th / 3 || bh < th / 3 || bw > 2 * th || bh > 2 * th || bw > 1.5 * bh || bh > 1.5 * bw) continue; int ccx = (x0 + x1) / 2, ccy = (y0 + y1) / 2; if (!GreyAt(f, ccx, ccy) || GreyAt(f, ccx, y0 + 1) || GreyAt(f, ccx, y1 - 1) || GreyAt(f, x0 + 1, ccy) || GreyAt(f, x1 - 1, ccy)) continue; if (best.IsEmpty || (rightmost ? ccx > best.X : ccx < best.X)) best = new Point(ccx, ccy); } return best; } internal static bool GreyAt(Frame f, int x, int y) { var c = f.Pixel(x, y); int mx = Math.Max(c.R, Math.Max(c.G, c.B)), mn = Math.Min(c.R, Math.Min(c.G, c.B)); return mx >= 110 && mx - mn <= 34; } internal static Point SkillWindowX(Frame f, OcrLine title, Rectangle popup) { int th = Math.Max(Math.Max(8, title.Box.Height), (int)(21 * Math.Min(f.Height / 1009.0, f.Width / 1790.0))); int right = Math.Min(popup.Right, f.Width - 1); for (int x = title.Box.Right + th; x < right; x++) { var c = f.Pixel(x, title.CenterY); if (c.R >= 150 && c.G >= 120 && c.R - c.B >= 50 || EventOrange(c)) { right = x; break; } } var r = Rectangle.Intersect(Rectangle.FromLTRB(title.Box.Right + th, title.CenterY - th * 6 / 5, right, title.CenterY + th * 6 / 5), new Rectangle(0, 0, f.Width, f.Height)); Func grey = (x, y) => { var c = f.Pixel(x, y); int mx = Math.Max(c.R, Math.Max(c.G, c.B)), mn = Math.Min(c.R, Math.Min(c.G, c.B)); return mx >= 110 && mx - mn <= 34; }; Func patch = (left, end) => { int x0 = int.MaxValue, y0 = int.MaxValue, x1 = -1, y1 = -1; for (int y = r.Top; y < r.Bottom; y++) for (int x = left; x < end; x++) if (grey(x, y)) { x0 = Math.Min(x0, x); x1 = Math.Max(x1, x); y0 = Math.Min(y0, y); y1 = Math.Max(y1, y); } if (x1 < 0) return Point.Empty; int w = x1 - x0 + 1, h = y1 - y0 + 1; if (w < th / 3 || h < th / 3 || w > 2 * th || h > 2 * th || w > 1.5 * h || h > 1.5 * w) return Point.Empty; int cx = (x0 + x1) / 2, cy = (y0 + y1) / 2; if (!grey(cx, cy) || grey(cx, y0 + 1) || grey(cx, y1 - 1) || grey(x0 + 1, cy) || grey(x1 - 1, cy)) return Point.Empty; return new Point(cx, cy); }; var whole = patch(r.Left, r.Right); if (!whole.IsEmpty) return whole; int patchEnd = -1, gap = 0; for (int x = r.Right - 1; x >= r.Left - 1; x--) { bool any = false; for (int y = r.Top; y < r.Bottom && x >= r.Left && !any; y++) any = grey(x, y); if (any) { if (patchEnd < 0) patchEnd = x; gap = 0; continue; } if (patchEnd < 0 || (++gap <= th / 3 && x >= r.Left)) continue; var p = patch(x + gap, patchEnd + 1); if (!p.IsEmpty) return p; patchEnd = -1; gap = 0; } return Point.Empty; } internal static int PointsToSpend(int window, int topBar, int put) { if (window >= 0) return window; return topBar >= 0 ? Math.Max(0, topBar - put) : -1; } public int SpentOn = -1; public string SavingFor; public bool PointsFailed; public bool PointsAtStop; public int SpendPoints(int stat, int available, int stopAt = 0) { SpentOn = stat; SavingFor = null; PointsFailed = PointsAtStop = false; if (available < 1) return 0; OcrLine button; using (var f = Capture()) { var v = ViewOf(f); int right = v.Problem == null && v.Cash != null ? v.Cash.Box.X : f.Width / 2; button = Ocr.Read(f, new Rectangle(168, 0, Math.Max(40, right - 168), v.Problem == null ? v.Header.Bottom : 120), 3, Prep.None) .FirstOrDefault(l => Parse.Has(l.Text, "POINTS")); } if (button == null) { log("Couldn't find the skill points button"); PointsFailed = true; return 0; } Press(button, 1000); List lines = null; for (int look = 0; look < 6; look++) { if (look == 3) Press(button, 1000); else if (look > 0) Wait(700); using (var f = Capture()) lines = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None); if (lines.Any(l => Parse.Has(l.Text, "SKILL POINTS"))) break; } if (!lines.Any(l => Parse.Has(l.Text, "SKILL POINTS"))) { log("The skill point window didn't open"); Snapshot("skill window missing", 60); PointsFailed = true; return 0; } int pending = 0, put = 0; int start = -1; bool interrupted = false; try { for (int i = 0; i < 40; i++) { int points, cost, value; Point plus; using (var f = Capture()) { lines = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None); if (!ReadSkillRow(f, lines, PopupArea(f), stat, stopAt > 0, out points, out cost, out plus, out value)) { log("Skill points: " + StatNames[stat] + "'s row or cost didn't read"); Snapshot("skill row " + StatNames[stat], 60); PointsFailed = true; break; } } if (cost > 0) PointsCost = cost; points = PointsToSpend(points, available, put); if (points < 0) { log("Skill points: the points available didn't read, in the window nor in the top bar"); Snapshot("skill points not read", 60); PointsFailed = true; break; } if (stopAt > 0) { int now = StatNow(stat, value, start, pending); if (start < 0) start = value; if (now >= stopAt) { PointsAtStop = true; break; } if (now < 0) { log("Skill points: " + StatNames[stat] + "'s number didn't read, so nothing is spent past your stop at " + stopAt); Snapshot("skill number " + StatNames[stat], 60); PointsFailed = true; break; } } if (pending == 0 && cost > points) SavingFor = string.Format("{0} ({1} points, you have {2})", StatNames[stat], cost, points); if (cost <= 0 || points < cost || plus == Point.Empty) break; Click(plus.X, plus.Y, 450); pending++; put += cost; } if (pending > 0) { var apply = lines.FirstOrDefault(l => Parse.Key(l.Text) == "APPIY"); using (var f = Capture()) apply = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None).FirstOrDefault(l => Parse.Key(l.Text) == "APPIY") ?? apply; if (apply == null) { log("Skill points: APPLY didn't read - the choice isn't kept"); Snapshot("skill apply missing", 60); pending = 0; PointsFailed = true; } else { Press(apply, 900); using (var f = Capture()) lines = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None); } } } catch (Exception) { interrupted = true; throw; } finally { var title = lines.FirstOrDefault(l => Parse.Has(l.Text, "SKILL POINTS")); var close = lines.FirstOrDefault(l => Parse.Key(l.Text) == "DONE") ?? (title == null ? null : lines.FirstOrDefault(l => (l.Text.Trim() == "x" || l.Text.Trim() == "X") && l.Box.X > title.Box.Right && Math.Abs(l.CenterY - title.CenterY) < 2 * title.Box.Height)); if (close != null && !interrupted) { Check(); Guard(close.Text); Win.Click(close.CenterX, close.CenterY); Thread.Sleep(700); } else if (title != null && !interrupted) { Point x; using (var f = Capture()) x = SkillWindowX(f, title, PopupArea(f)); if (!x.IsEmpty) { Check(); Guard("X"); Win.Click(x.X, x.Y); Thread.Sleep(700); } } } return pending; } readonly HashSet refused = new HashSet(); void Guard(string text) { GuardText(text); NotDisconnected(); } void GuardText(string text) { string why = NeverPress.Why(text); if (why == null) return; if (refused.Add(Parse.Key(text) + "|" + why)) { log("Refused to press \"" + (text ?? "").Trim() + "\" (" + why + ": on the never-press list) - skipping that for now"); Snapshot("never press " + why); } throw new NeverPressException(text, why); } void NotDisconnected() { using (var f = Win.Capture()) if (DisconnectShowing(f)) throw new WindowChangedException(Disconnected); } internal static bool DisconnectShowing(Frame f) { if (DialogGrey(f) < 0.4) return false; var lines = Ocr.Read(f, new Rectangle(f.Width * 18 / 100, f.Height * 12 / 100, f.Width * 64 / 100, f.Height * 80 / 100), 2, Prep.None); return DisconnectIn(null, lines) != null || ForeignWindow(f, lines) != null; } void Press(OcrLine button, int waitMs) { Press(button.Text, button.CenterX, button.CenterY, waitMs); } void Press(string text, int x, int y, int waitMs) { Guard(text); if (OnlyNavigate && !NavigationPress(text, x, y)) RefuseInCheck(text); Click(x, y, waitMs); } public sealed class CrewTotals { public int Attack = -1, Defense = -1, Henchmen = -1, Slots = -1; public bool Read { get { return Attack >= 0 && Defense >= 0; } } public override string ToString() { return Read ? "+" + Attack + " attack, +" + Defense + " defense" : "totals not read"; } } static readonly Regex CrewTotal = new Regex(@"(?:\+|•)\s*([0-9][0-9,]{0,6})\s*(ATTACK|DEFENSE)\b", RegexOptions.IgnoreCase); public CrewTotals ReadCrewTotals(Frame f) { var t = new CrewTotals(); var area = PageArea(f); foreach (var prep in new[] { Prep.None, Prep.DarkText, Prep.WhiteText }) { var lines = Ocr.Read(f, area, PageScale(f), prep); if (t.Henchmen < 0) foreach (var l in CountWrapped(lines)) { int hn, hm; if (HenchmenCount(l.Text, out hn, out hm)) { t.Henchmen = hn; t.Slots = hm; break; } } var found = lines.SelectMany(l => CrewTotal.Matches(l.Text).Cast().Select(m => new { L = l, M = m })).ToList(); foreach (var a in found.Where(x => !t.Read && x.M.Groups[2].Value.ToUpperInvariant() == "ATTACK").OrderBy(x => x.L.Box.Y)) { var d = found.FirstOrDefault(x => x.M.Groups[2].Value.ToUpperInvariant() == "DEFENSE" && (x.L == a.L ? x.M.Index > a.M.Index : x.L.Box.X > a.L.Box.Right && Math.Abs(x.L.CenterY - a.L.CenterY) <= Math.Max(a.L.Box.Height, x.L.Box.Height) / 2 + 2)); if (d == null) continue; t.Attack = int.Parse(a.M.Groups[1].Value.Replace(",", "")); t.Defense = int.Parse(d.M.Groups[1].Value.Replace(",", "")); break; } if (t.Read && t.Henchmen >= 0) return t; } return t; } internal OcrLine FindAutoEquip(Frame f, out Rectangle box, out string why) { box = Rectangle.Empty; var lines = SplitHeaderLine(f, CrewHeaderRead(f), "AUTOEQUIPBEST"); var button = lines.FirstOrDefault(l => Parse.Key(l.Text) == "AUTOEQUIPBEST") ?? LostAuto(lines, "EQUIPBEST"); if (button == null) { why = lines.Any(l => Parse.Key(l.Text).Contains("AUTOEQUIP")) ? "its text ran together with other words" : "it wasn't found on the Crew page"; return null; } var b = ButtonBox(f, button.Box); if (!HeaderFrameFits(f, b, button.Box)) { why = "its button's frame wasn't found around it"; return null; } var other = lines.FirstOrDefault(l => l != button && !button.Box.Contains(l.Box) && b.Contains(l.CenterX, l.CenterY)); if (other != null) { why = "\"" + other.Text + "\" is inside its frame too"; return null; } foreach (var u in lines.Where(l => Parse.Key(l.Text).Contains("UNEQUIP"))) { var ub = ButtonBox(f, u.Box); if (u.Box.IntersectsWith(b) || (!ub.IsEmpty && ub.IntersectsWith(b))) { why = "UNEQUIP ALL's box touches its box"; return null; } } box = b; why = null; return button; } internal static OcrLine LostAuto(List lines, string key) { var l = lines.FirstOrDefault(x => Parse.Key(x.Text) == key); if (l == null) { var m = lines.FirstOrDefault(x => { string k = Parse.Key(x.Text); return k.EndsWith(key) && k.Length - key.Length >= 2 && k.Length - key.Length <= 5 && !k.StartsWith("UN"); }); if (m == null) return null; var words = m.Text.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); return new OcrLine { Text = "AUTO " + string.Join(" ", words.Skip(words.Length > 1 ? 1 : 0)).ToUpperInvariant(), Box = m.Box }; } int add = l.Box.Width * 5 / Math.Max(1, l.Text.Trim().Length); return new OcrLine { Text = "AUTO " + l.Text.Trim(), Box = Rectangle.FromLTRB(l.Box.X - add, l.Box.Y, l.Box.Right, l.Box.Bottom) }; } internal bool HeaderFrameFits(Frame f, Rectangle b, Rectangle text) { int h = Math.Max(8, text.Height); int wide = Math.Max(2 * text.Width + 4 * h, PageArea(f).Width / 3); return !b.IsEmpty && b.Contains(text) && b.Width <= wide && b.Height <= 5 * h; } static List SplitHeaderLine(Frame f, List lines, string key) { lines = HeaderWrapped(lines); if (lines.Any(l => Parse.Key(l.Text) == key)) return lines; var joined = lines.FirstOrDefault(l => Parse.Key(l.Text).Contains(key)); if (joined == null) return lines; int h = Math.Max(8, joined.Box.Height); var words = Ocr.ReadWords(f, Rectangle.Inflate(joined.Box, h, h / 2), View.ScaleFor(h), Prep.None, true) .Where(w => w.CenterY >= joined.Box.Y && w.CenterY <= joined.Box.Bottom).OrderBy(w => w.Box.X).ToList(); for (int i = 0; i < words.Count; i++) { string acc = ""; for (int j = i; j < words.Count && key.StartsWith(acc + Parse.Key(words[j].Text)); j++) { acc += Parse.Key(words[j].Text); if (acc != key) continue; var own = words.GetRange(i, j - i + 1); var button = new OcrLine { Text = string.Join(" ", own.Select(w => w.Text)), Box = own.Skip(1).Aggregate(own[0].Box, (r, w) => Rectangle.Union(r, w.Box)), }; return lines.Where(l => l != joined).Concat(new[] { button }).Concat(words.Where(w => !own.Contains(w))).ToList(); } } return lines; } internal static List HeaderWrapped(List lines) { return JoinWrapped(JoinWrapped(lines, t => Parse.Key(t) == "AUTOSORT", t => Parse.Key(t) == "CREW"), t => Parse.Key(t) == "AUTOEQUIP", t => Parse.Key(t) == "BEST"); } internal List CrewHeaderRead(Frame f) { var area = PageArea(f); int scale = PageScale(f); var lines = Ocr.Read(f, area, scale, Prep.None); Func, bool> any = ls => ls.Any(l => { string k = Parse.Key(l.Text); return k.Contains("AUTOEQUIP") || k.Contains("UNEQUIP") || k.Contains("AUTOSORT") || k.Contains("GEARSET"); }); if (any(lines)) return lines; var column = lines.Where(l => { string k = Parse.Key(l.Text); return k == "HENCHMAN" || k == "POWER"; }).OrderBy(l => l.Box.Y).FirstOrDefault(); int bottom = column != null ? column.Box.Y - 2 : area.Y + area.Height / 5; if (bottom - area.Y < 20) return lines; var strip = new Rectangle(area.X, area.Y, area.Width, bottom - area.Y); foreach (var t in new[] { Tuple.Create(scale, Prep.DarkText), Tuple.Create(Math.Max(1, scale - 1), Prep.None) }) { var again = Ocr.Read(f, strip, t.Item1, t.Item2); if (!any(again)) continue; return lines.Where(l => l.CenterY >= bottom).Concat(again).ToList(); } var boxes = GoldFrames(f, strip); if (boxes.Count == 0) return lines; var inside = new List(); foreach (var b in boxes) { int inset = Math.Max(3, b.Height / 6); var read = Ocr.Read(f, Rectangle.Inflate(b, -inset, -inset), scale, Prep.None).Where(l => l.Text.Trim().Length > 0).OrderBy(l => l.Box.X).ToList(); if (read.Count == 0) continue; inside.Add(new OcrLine { Text = string.Join(" ", read.Select(l => l.Text.Trim())), Box = read.Skip(1).Aggregate(read[0].Box, (r, l) => Rectangle.Union(r, l.Box)) }); } if (!any(inside)) return lines; return lines.Where(l => !boxes.Any(b => b.Contains(l.CenterX, l.CenterY))).Concat(inside).ToList(); } internal static List GoldFrames(Frame f, Rectangle area) { area.Intersect(new Rectangle(0, 0, f.Width, f.Height)); var boxes = new List(); if (area.Width < 20 || area.Height < 20) return boxes; int minLen = Math.Max(40, area.Height / 3), minH = Math.Max(12, area.Height / 8); Func gold = c => c.R >= 170 && c.G >= 130 && c.B <= 110 && c.R - c.B >= 100 || EventOrange(c); var runs = new List(); for (int y = area.Top; y < area.Bottom; y++) { int start = -1; for (int x = area.Left; x <= area.Right; x++) { bool g = x < area.Right && gold(f.Pixel(x, y)); if (g && start < 0) start = x; else if (!g && start >= 0) { if (x - start >= minLen) runs.Add(new[] { y, start, x - 1 }); start = -1; } } } foreach (var top in runs) { var bottom = runs.FirstOrDefault(r => r[0] - top[0] >= minH && Math.Abs(r[1] - top[1]) <= 4 && Math.Abs(r[2] - top[2]) <= 4); if (bottom == null) continue; var b = Rectangle.FromLTRB(Math.Min(top[1], bottom[1]), top[0], Math.Max(top[2], bottom[2]) + 1, bottom[0] + 1); if (boxes.Any(o => o.IntersectsWith(b))) continue; boxes.Add(b); } return boxes.OrderBy(b => b.Y).ThenBy(b => b.X).ToList(); } static readonly string UnequipKey = Parse.Key("UNEQUIP ALL"); internal OcrLine FindUnequipAll(Frame f, out Rectangle box, out string why) { box = Rectangle.Empty; var lines = SplitHeaderLine(f, CrewHeaderRead(f), UnequipKey); var button = lines.FirstOrDefault(l => Parse.Key(l.Text) == UnequipKey); if (button == null) { why = lines.Any(l => Parse.Key(l.Text).Contains("UNEQUIP")) ? "its text ran together with other words" : "it wasn't found on the Crew page"; return null; } var b = ButtonBox(f, button.Box); if (!HeaderFrameFits(f, b, button.Box)) { why = "its button's frame wasn't found around it"; return null; } var other = lines.FirstOrDefault(l => l != button && b.Contains(l.CenterX, l.CenterY)); if (other != null) { why = "\"" + other.Text + "\" is inside its frame too"; return null; } foreach (var u in lines.Where(l => Parse.Key(l.Text).Contains("AUTOEQUIP"))) { var ub = ButtonBox(f, u.Box); if (u.Box.IntersectsWith(b) || (!ub.IsEmpty && ub.IntersectsWith(b))) { why = "AUTO EQUIP BEST's box touches its box"; return null; } } box = b; why = null; return button; } public bool UnequipAll(out bool pressed, out string why) { pressed = false; if (!OpenTab(Tab.Crew)) { why = "the Crew tab didn't open"; return false; } ScrollPage(0.62, 40); Wait(400); OcrLine button; Rectangle box; CrewTotals before; using (var f = Capture()) { before = ReadCrewTotals(f); button = FindUnequipAll(f, out box, out why); } for (int again = 0; again < 4 && button == null; again++) { CloseTerritoryToasts(); Wait(1500); using (var f = Capture()) { before = ReadCrewTotals(f); button = FindUnequipAll(f, out box, out why); } } if (button == null) { Snapshot("unequip all not pressed", 60); return false; } using (var f = Capture()) if (!ReadCrew(f).Page || ReadDismissWindow(f) != null) { why = "a window is open over the Crew page"; return false; } if (Parse.Key(button.Text) != UnequipKey || box.IsEmpty || !box.Contains(button.Box)) throw new NeverPressException(button.Text, "not the crew's UNEQUIP ALL"); NotDisconnected(); if (OnlyNavigate) RefuseInCheck(button.Text); Click(box.X + box.Width / 2, box.Y + box.Height / 2, 900); pressed = true; Snapshot("crew unequip all", 0, true); for (int look = 0; look < 3; look++) { using (var f = Capture()) { var popup = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None); CheckForPurchasePrompt(popup); if (popup.Any(l => { string k = Parse.Key(l.Text); return k.Contains("CONFIRM") || k.Contains("AREYOUSURE") || k == "YES" || k == "NO" || k == "CANCEI"; })) { why = "a window came up: \"" + string.Join(" ", popup.Select(l => l.Text)) + "\""; Snapshot("crew unequip all window", 0, true); return false; } var after = ReadCrewTotals(f); string toast = ToastIn(f); if ((before.Read && after.Read && after.Attack < before.Attack && after.Defense < before.Defense) || (toast != null && (Parse.Has(toast, "UNEQUIP") || Parse.Has(toast, "NOTHING EQUIPPED"))) || NothingEquipped(f)) { why = null; return true; } } Wait(700); } why = "the totals didn't go down"; Snapshot("crew unequip all no change", 60); return false; } internal static bool IsEquipToast(string t) { return t != null && (Parse.Has(t, "EQUIPPED") || Parse.Has(t, "ALREADY")) && Parse.Has(t, "BEST") && !Parse.Has(t, "NOTHING"); } static readonly Regex GearEmpty = new Regex(@"\b(WEAPON|ARMOR|VEHICLE)\s*[:;.,]?\s*EMPTY\b", RegexOptions.IgnoreCase); static readonly Regex GearKind = new Regex(@"\b(COMMON|UNCOMMON|RARE|EPIC|LEGENDARY|MYTHIC|SECRET|FORBIDDEN)\s*(WEAPON|ARMOR|VEHICLE)\b", RegexOptions.IgnoreCase); internal bool NothingEquipped(Frame f) { int empty = 0; foreach (var l in Ocr.Read(f, PageArea(f), PageScale(f), Prep.None)) { if (GearKind.IsMatch(l.Text)) return false; empty += GearEmpty.Matches(l.Text).Count; } return empty >= 6; } internal static Rectangle ButtonBox(Frame f, Rectangle text) { int y = text.Y + text.Height / 2, gap = Math.Max(3, text.Height / 2), x = text.X - gap; var face = f.Pixel(x, y); if (Math.Abs(Shade(f.Pixel(x - 2, y)) - Shade(face)) > 4 || Math.Abs(Shade(f.Pixel(text.Right + gap, y)) - Shade(face)) > 4) return Rectangle.Empty; int maxW = 2 * text.Width + 40, maxH = 5 * Math.Max(8, text.Height); int l = FrameEdge(f, x, y, -1, 0, face, maxW), r = FrameEdge(f, text.Right + gap, y, 1, 0, face, maxW); int t = FrameEdge(f, x, y, 0, -1, face, maxH), b = FrameEdge(f, x, y, 0, 1, face, maxH); if (l < 0 || r < 0 || t < 0 || b < 0) return Rectangle.Empty; return Rectangle.FromLTRB(l, t, r + 1, b + 1); } static int Shade(Color c) { return (c.R + c.G + c.B) / 3; } static int FrameEdge(Frame f, int x, int y, int dx, int dy, Color face, int max) { int shade = Shade(face); bool ring = false; for (int i = 0; i < max; i++, x += dx, y += dy) { if (x < 3 || y < 3 || x >= f.Width - 3 || y >= f.Height - 3) return -1; if (Shade(f.Pixel(x, y)) >= shade - 8) continue; bool frame = false; for (int k = 1; k <= 3 && !frame; k++) frame = Shade(f.Pixel(x - k * dx, y - k * dy)) > shade + 6; if (!frame && !ring) { int k = 1; while (k <= 3 && Shade(f.Pixel(x + k * dx, y + k * dy)) <= shade + 6) k++; if (k <= 3) { ring = true; x += (k - 1) * dx; y += (k - 1) * dy; i += k - 1; continue; } } if (!frame) return -1; return dx != 0 ? x - dx : y - dy; } return -1; } public sealed class EquipResult { public CrewTotals Before, After; public string Window; public bool Confirmed, Unchanged, Bare; public int Presses; public bool GearOn { get { return Window == null && (Confirmed || Unchanged); } } } public EquipResult EquipBest() { if (!OpenTab(Tab.Crew)) return null; var r = new EquipResult(); for (int press = 0; press < 2 && !r.Confirmed && r.Window == null; press++) { OcrLine button; Rectangle box; string why; CrewTotals before; using (var f = Capture()) { before = ReadCrewTotals(f); button = FindAutoEquip(f, out box, out why); } for (int again = 0; again < 4 && button == null; again++) { CloseTerritoryToasts(); Wait(1500); using (var f = Capture()) { before = ReadCrewTotals(f); button = FindAutoEquip(f, out box, out why); } } if (button == null) { log("Didn't press AUTO EQUIP BEST: " + why); Snapshot("auto equip best not pressed", 60); return press == 0 ? null : r; } if (!before.Read) { Wait(700); using (var f = Capture()) before = ReadCrewTotals(f); } if (press == 0 || !r.Before.Read) r.Before = before; Press(button.Text, box.X + box.Width / 2, box.Y + box.Height / 2, 900); r.Presses++; for (int look = 0; look < 3; look++) { if (look > 0) Wait(700); using (var f = Capture()) { var popup = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None); CheckForPurchasePrompt(popup); if (popup.Any(l => { string k = Parse.Key(l.Text); return k.Contains("CONFIRM") || k.Contains("AREYOUSURE") || k == "YES" || k == "NO"; })) r.Window = string.Join(" ", popup.Select(l => l.Text)); r.After = ReadCrewTotals(f); string toast = ToastIn(f); if (!IsEquipToast(toast)) { string framed; if (!AnyToastX(f, out framed).IsEmpty) toast = framed; } r.Confirmed = IsEquipToast(toast) || (r.Before.Read && r.After.Read && (r.After.Attack != r.Before.Attack || r.After.Defense != r.Before.Defense)); r.Bare = NothingEquipped(f); } if (r.Confirmed || r.Window != null) break; } } r.Unchanged = !r.Confirmed && r.Window == null && r.Presses >= 2 && !r.Bare && r.Before.Read && r.After != null && r.After.Read; return r; } public sealed class ShopItem { public string Name, PriceText; public double Price = -1; public bool Gold; public int Owned = -1; public int Rarity = -1; public OcrLine Buy; public bool BuyGold; public override string ToString() { return (Name ?? "an item") + " (" + (Gold ? Price + " gold" : SafehouseInfo.Money(Price)) + ")"; } } public sealed class ShopPage { public Rectangle Cash; public int StockSeconds = -1; public List Items = new List(); public string Signature = ""; } static readonly Regex OwnedRx = new Regex(@"[O0]\s?[.,']?\s?(?:w|vv|v)\s?ned\s*[:;.]?\s*([0-9OoIlS,]{1,7})", RegexOptions.IgnoreCase); internal static int OwnedIn(string text) { var m = OwnedRx.Match(text ?? ""); int n; return m.Success && int.TryParse(Parse.Digits(m.Groups[1].Value), out n) ? n : -1; } public static readonly string[] Rarities = { "COMMON", "UNCOMMON", "RARE", "EPIC", "LEGENDARY", "MYTHIC", "SECRET", "FORBIDDEN", "EXOTIC" }; public ShopPage ReadShop(Frame f) { var area = PageArea(f); var lines = Ocr.Read(f, area, PageScale(f), Prep.None); var equipment = lines.FirstOrDefault(l => Parse.Key(l.Text).Contains("EQUIPMENT")); var robux = lines.FirstOrDefault(l => Parse.Key(l.Text).Contains("ROBUXSHOP")); if (robux == null && equipment != null) robux = lines.FirstOrDefault(l => Parse.Key(l.Text).StartsWith("ROBUX") && l.Box.X > equipment.Box.Right && Math.Abs(l.CenterY - equipment.CenterY) < Math.Max(8, equipment.Box.Height)); if (equipment == null || robux == null || robux.Box.X <= equipment.Box.X) return null; int h = Math.Max(8, equipment.Box.Height); var page = new ShopPage(); int right = robux.Box.X - Math.Max(4, h / 3); page.Cash = Rectangle.FromLTRB(area.Left, equipment.Box.Bottom, right, area.Bottom); var stock = lines.FirstOrDefault(l => Parse.Key(l.Text).Contains("NEWSTOCK")); if (stock != null) { var m = Regex.Match(stock.Text, @"([0-9OoIl]{1,2})\s*[:.]\s*([0-9OoIlS]{2})\s*$"); int mm, ss; if (m.Success && int.TryParse(Parse.Digits(m.Groups[1].Value), out mm) && int.TryParse(Parse.Digits(m.Groups[2].Value), out ss)) page.StockSeconds = mm * 60 + ss; } var inCash = lines.Where(l => l.Box.Right <= right && l.Box.Y > equipment.Box.Bottom).ToList(); page.Signature = string.Join("|", inCash.Select(l => Parse.Key(l.Text))); var buys = inCash.Where(l => Parse.Key(l.Text) == "BUY").ToList(); foreach (var d in Ocr.Read(f, Rectangle.FromLTRB(area.Left, equipment.Box.Bottom, right, area.Bottom), PageScale(f), Prep.DarkText)) if (Parse.Key(d.Text) == "BUY" && d.Box.Right <= right && !buys.Any(b => b.Box.IntersectsWith(d.Box))) buys.Add(d); foreach (var face in ShopFaces(f, page.Cash, h)) { if (buys.Any(b => b.Box.IntersectsWith(face))) continue; var alone = new[] { Tuple.Create(PageScale(f) + 1, Prep.None), Tuple.Create(PageScale(f) + 1, Prep.Contrast), Tuple.Create(PageScale(f) + 2, Prep.None), Tuple.Create(PageScale(f) + 2, Prep.Contrast) } .Select(t => Ocr.Read(f, face, t.Item1, t.Item2).FirstOrDefault(l => Parse.Key(l.Text) == "BUY" && l.Box.Right <= right)).FirstOrDefault(l => l != null); if (alone != null) buys.Add(alone); } foreach (var b in buys.OrderBy(l => l.Box.Y)) { int bh = Math.Max(8, b.Box.Height); var above = inCash.Where(l => l != b && l.Box.Bottom <= b.Box.Y + bh / 3 && b.Box.Y - l.Box.Bottom < 2 * bh && l.Box.Right > b.Box.X && l.Box.X < b.Box.Right && Regex.IsMatch(l.Text, "[0-9]")).ToList(); var price = above.Where(l => l.Text.Contains("$") && GreenInk(f, l.Box)).OrderByDescending(l => l.Box.Right).FirstOrDefault() ?? above.OrderByDescending(l => l.Box.Bottom).FirstOrDefault(); var it = new ShopItem { Buy = b, BuyGold = GoldFace(f, b.Box) }; if (price != null) { it.PriceText = price.Text.Trim(); double v; if (Regex.IsMatch(it.PriceText, @"R\s?\$")) continue; if (it.PriceText.Contains("$")) { v = ShopLinePrice(it.PriceText); if (v >= 0 && GreenInk(f, price.Box)) it.Price = v; } else { string rest = Regex.Replace(it.PriceText, "[GC]OLD|BARS?", "", RegexOptions.IgnoreCase), d = Parse.Digits(rest); if (rest.Length < it.PriceText.Length && d.Length > 0 && d.Length <= 7 && !Regex.IsMatch(rest, "[A-Za-z]{2}")) { it.Gold = true; it.Price = double.Parse(d); } } } if (it.Price < 0 && it.BuyGold && !Regex.IsMatch(it.PriceText ?? "", @"R\s?\$")) { string again; double v2 = RereadShopPrice(f, b, page.Cash, out again); if (v2 >= 0) { it.Price = v2; it.PriceText = again; } } else if (it.Price >= 0 && !it.BuyGold && !it.Gold && (it.PriceText ?? "").Contains(".")) { string again; double v2 = RereadShopPrice(f, b, page.Cash, out again); if (v2 > it.Price) { it.Price = v2; it.PriceText = again; } } int top = (price != null ? price.Box.Y : b.Box.Y - 2 * bh) - bh / 2, bottom = b.Box.Bottom + bh / 2; int leftOf = Math.Min(b.Box.X, price != null ? price.Box.X : b.Box.X) - bh / 2; var card = inCash.Where(l => l.Box.Right < leftOf && l.CenterY >= top && l.CenterY <= bottom).ToList(); var owned = card.Select(l => OwnedRx.Match(l.Text)).FirstOrDefault(m => m.Success); int o; if (owned != null && int.TryParse(Parse.Digits(owned.Groups[1].Value), out o)) it.Owned = o; var name = card.Where(l => !OwnedRx.IsMatch(l.Text) && !Rarities.Any(r => Parse.Key(l.Text).StartsWith(Parse.Key(r))) && !l.Text.TrimStart().StartsWith("+") && !l.Text.Contains("%")) .OrderByDescending(l => l.Box.Height).ThenBy(l => l.Box.Y).FirstOrDefault(); if (name != null) it.Name = name.Text.Trim(); var over = card.Where(l => l != name && (name == null || l.CenterY < name.Box.Y + name.Box.Height / 3) && !l.Text.Contains("%")) .OrderBy(l => l.Box.Y).ToList(); foreach (var l in over) { int r = ShopRarityOf(l.Text); if (r >= 0) { it.Rarity = r; break; } } if (it.Rarity < 0 && (over.Count > 0 || name != null)) { Rectangle box; if (name != null) { int nh = Math.Max(8, name.Box.Height); box = Rectangle.Intersect(new Rectangle(name.Box.X - nh / 2, name.Box.Y - nh * 3 / 2, 14 * nh, nh * 3 / 2 + 2), area); } else box = Rectangle.Intersect(Rectangle.Inflate(over[0].Box, Math.Max(4, over[0].Box.Height / 2), Math.Max(4, over[0].Box.Height / 2)), area); foreach (var t in new[] { Tuple.Create(PageScale(f) + 1, Prep.None), Tuple.Create(PageScale(f) + 1, Prep.Contrast), Tuple.Create(PageScale(f) + 2, Prep.None) }) { if (box.Width < 10 || box.Height < 5) break; it.Rarity = Ocr.Read(f, box, t.Item1, t.Item2).Select(l => ShopRarityOf(l.Text)).Where(r => r >= 0).DefaultIfEmpty(-1).First(); if (it.Rarity >= 0) break; } } if (!it.Gold && it.Rarity >= 1 && it.Price >= 0 && it.Price < 1000) { string again; double v2 = RereadShopPrice(f, b, page.Cash, out again); if (v2 >= 1000) { it.Price = v2; it.PriceText = again; } else it.Price = -1; } page.Items.Add(it); } return page; } static List ShopFaces(Frame f, Rectangle area, int h) { var faces = new List(); area.Intersect(new Rectangle(0, 0, f.Width, f.Height)); int top = -1, left = int.MaxValue, rightEnd = -1; for (int y = area.Top; y <= area.Bottom; y++) { int first = -1, last = -1, runFirst = -1, runLast = -1; if (y < area.Bottom) for (int x = area.Left; x < area.Right; x++) { if (!GoldIsh(f, x, y)) continue; if (runLast < 0 || x - runLast > h) runFirst = x; runLast = x; if (runLast - runFirst > last - first) { first = runFirst; last = runLast; } } if (last - first >= 3 * h) { if (top < 0) top = y; left = Math.Min(left, first); rightEnd = Math.Max(rightEnd, last); continue; } if (top >= 0 && y - top >= h && rightEnd - left >= 3 * h && rightEnd - left <= 12 * h) faces.Add(Rectangle.FromLTRB(left, top, rightEnd + 1, y)); top = -1; left = int.MaxValue; rightEnd = -1; } return faces; } double RereadShopPrice(Frame f, OcrLine buy, Rectangle cash, out string text) { text = null; int bh = Math.Max(8, buy.Box.Height); var band = Rectangle.Intersect(Rectangle.FromLTRB(buy.Box.X - 3 * bh, buy.Box.Y - 3 * bh, buy.Box.Right + 3 * bh, buy.Box.Y - bh / 4), cash); if (band.Width < 10 || band.Height < 5) return -1; int s = PageScale(f); var tries = new[] { s + 1, s, s + 2, s + 3 }.SelectMany(z => new[] { Tuple.Create(z, Prep.None), Tuple.Create(z, Prep.Contrast) }).ToList(); foreach (var t in tries) foreach (var l in Ocr.Read(f, band, t.Item1, t.Item2).OrderByDescending(l => l.Box.Right)) { double v; if (!l.Text.Contains("$") || Regex.IsMatch(l.Text, @"R\s?\$") || !CleanPriceEnd(l.Text) || (v = ShopLinePrice(l.Text)) < 0 || !GreenInk(f, l.Box)) continue; text = l.Text.Trim(); return v; } var g = GreenBox(f, band); if (g.Width >= 6 && g.Height >= 4) foreach (var t in tries) foreach (var l in Ocr.Read(f, g, t.Item1, t.Item2)) { double v; if (!l.Text.Contains("$") || Regex.IsMatch(l.Text, @"R\s?\$") || !CleanPriceEnd(l.Text) || (v = ShopLinePrice(l.Text)) < 0) continue; text = l.Text.Trim(); return v; } if (g.Width >= 6 && g.Height >= 4) { var plain = tries.SelectMany(t => Ocr.Read(f, g, t.Item1, t.Item2)).Select(l => Regex.Match(l.Text.Trim(), @"\$\s*([0-9]{1,3}[.,][0-9])[0-9]{1,2}$")) .Where(m => m.Success).Select(m => m.Groups[1].Value.Replace(',', '.')).Distinct().ToList(); if (plain.Count == 1) foreach (int z in new[] { s, s + 1, s + 2 }) foreach (var l in Ocr.Read(f, g, z, Prep.DarkText)) { var m = Regex.Match(l.Text.Trim(), @"^[5$]([0-9]{1,3}[.,][0-9])\s*([KMBT])$"); double v; if (!m.Success || !plain[0].EndsWith(m.Groups[1].Value.Replace(',', '.')) || !Parse.Money("$" + plain[0] + m.Groups[2].Value, out v)) continue; text = "$" + plain[0] + m.Groups[2].Value + " (" + l.Text.Trim() + ")"; return v; } } return -1; } internal static bool CleanPriceEnd(string text) { var parts = Regex.Split(PriceJoined(text), @"\s+").Where(p => Regex.IsMatch(p, "[0-9]")).ToList(); return parts.Count > 0 && Regex.IsMatch(parts[parts.Count - 1], @"^\$?(?:[0-9][0-9.,]*[KMBT]|[0-9]{1,3}(?:[.,][0-9]{3})*)$", RegexOptions.IgnoreCase); } static Rectangle GreenBox(Frame f, Rectangle band) { band.Intersect(new Rectangle(0, 0, f.Width, f.Height)); int l = int.MaxValue, t = int.MaxValue, r = -1, b = -1; for (int y = band.Top; y < band.Bottom; y++) for (int x = band.Left; x < band.Right; x++) { var c = f.Pixel(x, y); if (c.G >= 100 && c.G - c.R >= 30 && c.G - c.B >= 30) { l = Math.Min(l, x); t = Math.Min(t, y); r = Math.Max(r, x); b = Math.Max(b, y); } } if (r < 0) return Rectangle.Empty; int pad = Math.Max(3, (b - t) / 3); return Rectangle.Intersect(Rectangle.FromLTRB(l - pad, t - pad, r + pad + 1, b + pad + 1), band); } internal static int ShopRarityOf(string text) { string k = Parse.Key(text ?? ""); int r = Array.FindIndex(Rarities, x => k.StartsWith(Parse.Key(x))); if (r >= 0) return r; string w = Parse.Key(Regex.Match((text ?? "").Trim(), @"^[A-Za-z0-9]+").Value); if (w.Length < 4) return -1; for (int i = 0; i < Rarities.Length; i++) { string x = Parse.Key(Rarities[i]); if (x.Length >= 6 && w.StartsWith(x.Substring(0, 4))) return i; if (x.Length >= 5 && w.Length >= x.Length - 1 && View.Distance(w.Substring(0, Math.Min(w.Length, x.Length)), x) <= 1) return i; } return -1; } static string PriceJoined(string text) { string s = Regex.Replace((text ?? "").Trim(), @"\$\s+", "$"); s = Regex.Replace(s, @"([0-9])\s+S\s?([KMBT])(?![A-Za-z])", "$1.5$2"); s = Regex.Replace(s, @"([0-9])[vV][1lI](?![0-9A-Za-z])", "$1M"); return Regex.Replace(s, @"([0-9])\s+([KMBT])(?![A-Za-z])", "$1$2"); } internal static double ShopLinePrice(string text) { var parts = Regex.Split(PriceJoined(text), @"\s+").Where(p => Regex.IsMatch(p, "[0-9]")).ToList(); if (parts.Count == 0) return -1; Func tidy = p => Regex.IsMatch(p, @"^\$?[0-9]{1,3}(?:\.[0-9]{3})+$") ? p.Replace('.', ',') : p; string last = tidy(parts[parts.Count - 1]); double v; if (last.Contains("$")) return Parse.Money(last.Substring(last.IndexOf('$')), out v) ? v : -1; if (parts.Count < 2) return -1; string before = tidy(parts[parts.Count - 2]); double old; var five = Regex.Match(last, @"^5([0-9][0-9.,]*\s*[KMBT]?)$", RegexOptions.IgnoreCase); if (!five.Success || !before.Contains("$") || !Parse.Money(before.Substring(before.IndexOf('$')), out old) || old <= 0) return -1; var fits = new List(); foreach (var c in new[] { "$" + five.Groups[1].Value, "$1" + five.Groups[1].Value }) if (Parse.Money(c, out v) && v < old && v >= old * 0.4) fits.Add(v); return fits.Count == 1 ? fits[0] : -1; } internal static double LastMoney(string text) { for (int i = (text ?? "").LastIndexOf('$'); i >= 0; i = i > 0 ? text.LastIndexOf('$', i - 1) : -1) { double v; if (Parse.Money(text.Substring(i), out v)) return v; } return -1; } internal static bool GreenInk(Frame f, Rectangle box) { box.Intersect(new Rectangle(0, 0, f.Width, f.Height)); int green = 0; for (int y = box.Top; y < box.Bottom; y++) for (int x = box.Left; x < box.Right; x++) { var c = f.Pixel(x, y); if (c.G >= 100 && c.G - c.R >= 30 && c.G - c.B >= 30) green++; } return green * 100 >= 3 * Math.Max(1, box.Width * box.Height); } void PressShopEntry(OcrLine label, Tab t, bool slow) { if ((t != Tab.Shop && t != Tab.Properties && t != Tab.BlackMarket) || View.Label(new List { label }, View.TabLabels[t]) == null) throw new NeverPressException(label.Text, "not the " + View.TabLabels[t] + " entry"); NotDisconnected(); if (OnlyNavigate) RefuseInCheck(label.Text); if (!slow) { Click(label.CenterX, label.CenterY, 650); return; } Check(); NoteLateFrame(); Win.SlowClick(label.CenterX, label.CenterY); Wait(650 + (FrameLag() > 0 ? 800 : 0)); } void PressShopBuy(OcrLine buy, ShopPage page) { if (Parse.Key(buy.Text) != "BUY" || !page.Cash.Contains(buy.Box)) throw new NeverPressException(buy.Text, "a BUY outside the shop's cash list"); NotDisconnected(); if (OnlyNavigate) RefuseInCheck(buy.Text); Click(buy.CenterX, buy.CenterY, 450); } public DateTime GoldShopHeldUntil = DateTime.MinValue; public List BuyFromShop(out int stockSeconds, double keep = 0, string prefer = null, List tooDear = null, double minPrice = 0, Func want = null, bool gold = false) { stockSeconds = -1; if (gold && DateTime.UtcNow < GoldShopHeldUntil) gold = false; var bought = new List(); if (!OpenTab(Tab.Shop, true)) return bought; ShopPage page = null; bool rulingClosed = false; for (int look = 0; look < 5 && page == null; look++) { if (look > 0) Wait(700); using (var f = Capture()) { page = ReadShop(f); if (page == null && !rulingClosed && CloseRulingFamily(f)) { rulingClosed = true; ForgetView(); } } } if (page == null) { log("Shop: couldn't read the page - not buying"); Snapshot("shop not read", 60); return bought; } stockSeconds = page.StockSeconds; int x = page.Cash.X + page.Cash.Width / 2, y = page.Cash.Y + page.Cash.Height / 2; Scroll(x, y, 15); Wait(400); var tried = new HashSet(); string beforeScroll = null; int scrolls = 0; ShopPage carried = null; double carriedCash = -1; bool rechecked = false; var doubt = new Dictionary(); var seenFine = new HashSet(); Func doubtOf = (i, cashNow) => { if (i.Gold || (want != null && !want(i))) return null; if (i.Price < 0) return i.BuyGold ? "its price didn't read" : null; if (i.Price < minPrice) return null; if (cashNow < 0) return i.BuyGold ? "the cash on hand didn't read" : null; if (!i.BuyGold && i.Price <= cashNow - Math.Max(0, keep)) return "its BUY looked grey though the cash covers it"; return null; }; for (int look = 0; look < 60; look++) { double cash = -1; bool fromCarried = carried != null; if (carried != null) { page = carried; cash = carriedCash; carried = null; } else { for (int again = 0; again < 3; again++) { if (again > 0) Wait(500); using (var f = Capture()) { page = ReadShop(f); cash = ReadCash(f); } if (page != null) break; } } if (page == null) break; if (beforeScroll != null && page.Signature == beforeScroll) break; beforeScroll = null; foreach (var i in page.Items) if (i.Name != null && i.Price >= 0 && doubtOf(i, cash) == null) { seenFine.Add(i.Name); doubt.Remove(i.Name); } if (tooDear != null) foreach (var i in TooDear(page, cash, keep, want)) { if (tried.Contains(ItemKey(i))) continue; var had = tooDear.FirstOrDefault(d => ItemKey(d) == ItemKey(i)); if (had == null) tooDear.Add(i); else if (had.Rarity < 0 && i.Rarity >= 0) had.Rarity = i.Rarity; } var can = page.Items.Where(i => !tried.Contains(ItemKey(i)) && i.Price >= 0 && i.BuyGold && (i.Gold ? gold && i.Price > 0 && (want == null || want(i)) : (want == null || want(i)) && i.Price >= minPrice && cash >= 0 && i.Price <= cash - Math.Max(0, keep))).ToList(); var next = (prefer != null ? can.FirstOrDefault(i => i.Name != null && Parse.Key(i.Name) == Parse.Key(prefer)) : null) ?? can.FirstOrDefault(); if (next == null) { var unsure = page.Items.Where(i => !tried.Contains(ItemKey(i)) && doubtOf(i, cash) != null).ToList(); if (!rechecked && (fromCarried || unsure.Count > 0 || cash < 0)) { rechecked = true; Wait(400); continue; } rechecked = false; if (unsure.Count > 0) { foreach (var i in unsure.Where(i => i.Name == null || !seenFine.Contains(i.Name))) doubt[i.Name ?? "an item"] = doubtOf(i, cash); if (doubt.Count > 0) Snapshot("shop card not acted on", 0, true); if (unsure.Any(i => i.Price < 0)) Snapshot("shop price not read", 60, failure: false); } foreach (var i in page.Items) tried.Add(ItemKey(i)); if (scrolls++ >= 12) break; beforeScroll = page.Signature; Scroll(x, y, -Math.Max(1, Math.Min(4, (page.Cash.Height - 150) / 98))); Wait(500); continue; } tried.Add(ItemKey(next)); rechecked = false; PressShopBuy(next.Buy, page); string toast = null; ShopPage after = null; List popup = null; double afterCash = -1; bool took = false; for (int settle = 0; settle < 2 && !took; settle++) { if (settle > 0) Wait(500); after = null; for (int again = 0; again < 3 && after == null; again++) { if (again > 0) Wait(500); using (var f = Capture()) { popup = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None); after = ReadShop(f); CheckForPurchasePrompt(popup, after != null); if (after != null) afterCash = ReadCash(f); else if (again < 2 && CloseRulingFamily(f)) continue; if (toast == null) { string t = ToastIn(f); if (t != null && (Parse.Has(t, "BOUGHT") || Parse.Has(t, "PURCHASED"))) toast = t; } } } if (after == null) break; var same = after.Items.FirstOrDefault(i => i.Name != null && i.Name == next.Name); took = same == null || (same != null && next.Owned >= 0 && same.Owned > next.Owned) || (toast != null && (Parse.Has(toast, "BOUGHT") || Parse.Has(toast, "PURCHASED"))); } if (afterCash >= 0) { carried = after; carriedCash = took && cash >= 0 && next.Price > 0 && !next.Gold ? Math.Min(afterCash, cash - next.Price) : afterCash; } if (next.Gold && (after == null || !took)) { GoldShopHeldUntil = DateTime.UtcNow.AddHours(6); gold = false; log("Shop: the gold buy of " + next + " didn't go through (not enough gold bars?) - no gold buys for 6 hours"); Snapshot("shop gold buy not taken", 0, true); } if (after == null) { log("Shop: a window came up after BUY on " + next + " - stopping the shop for now: \"" + string.Join(" ", popup.Select(l => l.Text)) + "\""); Snapshot("shop window after buy", 0, true); break; } if (took) bought.Add(next); else log("Shop: BUY on " + next + " did nothing" + (toast != null ? " (\"" + toast + "\")" : "")); } if (tooDear != null) tooDear.RemoveAll(d => bought.Any(b => ItemKey(b) == ItemKey(d))); foreach (var b in bought) if (b.Name != null) doubt.Remove(b.Name); if (doubt.Count > 0) log("Shop: couldn't buy " + string.Join(", ", doubt.Select(d => d.Key + " (" + d.Value + ")")) + " - left in the shop"); return bought; } static string ItemKey(ShopItem i) { return Parse.Key(i.Name ?? "") + "|" + Parse.Key(i.PriceText ?? ""); } public static List TooDear(ShopPage page, double cash, double keep = 0, Func want = null) { if (page == null || cash < 0) return new List(); return page.Items.Where(i => !i.Gold && i.Price > 0 && i.Price > cash - Math.Max(0, keep) && (want == null || want(i))).ToList(); } public sealed class PropertyCard { public string Name = ""; public double Income = -1; public double Price = -1; public int Level = -1; public OcrLine Build; public bool BlockFull; public int Gold = -1; public override string ToString() { return (Name.Length > 0 ? Name : "a property") + " (" + SafehouseInfo.Money(Price) + ", " + (Gold > 0 ? "+" + Gold + " gold bar" + (Gold == 1 ? "" : "s") + "/day" : "+" + SafehouseInfo.Money(Income) + "/hr") + ")"; } } public sealed class PropertiesPage { public bool Build; public OcrLine BuildTab, BlockTab; public Rectangle Area; public List Cards = new List(); public int LotsUsed = -1, LotsMax = -1, LotsOpen = -1; public bool Full; } static readonly Regex PerLotRx = new Regex(@"\$\s?([0-9OoIlS]{1,3}(?:[,.][0-9OoIlS]{1,3})*\s?[KMBT]?)\s*/\s*h", RegexOptions.IgnoreCase); internal static readonly Regex GoldPerDayRx = new Regex(@"\+?\s*([0-9OoIlS]{1,2})\s*G[o0]\s?[lI1]d\s*Bars?\s*/\s*d\s?a\s?y", RegexOptions.IgnoreCase); static readonly Regex BlockFullRx = new Regex(@"^\s*B\s?[LI1]\s?O\s?C\s?K\s*F\s?U\s?[LI1]\s?[LI1].*\$", RegexOptions.IgnoreCase); static readonly Regex BlockFullWordRx = new Regex(@"^\W{0,2}\w{3,6}\s*F\s?U\s?[LI1]\s?[LI1](?![A-Za-z])", RegexOptions.IgnoreCase); static readonly Regex BuildPriceRx = new Regex(@"^\s*BUI[LI1]D\s*\$\s?([0-9OoIlS]{1,3}(?:[,.][0-9OoIlS]{1,3})*\s?[KMBT]?)\s*$", RegexOptions.IgnoreCase); static double ShortPrice(Frame f, OcrLine line, double first, bool more = false) { int h = Math.Max(8, line.Box.Height); var r = Rectangle.Intersect(Rectangle.Inflate(line.Box, h / 2, h / 2), new Rectangle(0, 0, f.Width, f.Height)); var reads = new List>(); var tries = new List> { Tuple.Create(3, Prep.None), Tuple.Create(2, Prep.None), Tuple.Create(3, Prep.Contrast), Tuple.Create(4, Prep.None) }; if (more) tries.AddRange(new[] { Tuple.Create(5, Prep.None), Tuple.Create(3, Prep.DarkText), Tuple.Create(2, Prep.Contrast), Tuple.Create(4, Prep.Contrast), Tuple.Create(3, Prep.GreyText), Tuple.Create(4, Prep.GreyText) }); foreach (var t in tries) { string text = Ocr.ReadText(f, r, t.Item1, t.Item2); var m = Regex.Match(text, @"\$\s?([0-9OoIlS]{1,3}(?:[,.][0-9OoIlS]{1,3})*)\s?[KMBT](?![A-Za-z])", RegexOptions.IgnoreCase); double v; if (m.Success && Parse.Money(m.Value, out v) && v > 0) { reads.Add(Tuple.Create(v, Regex.IsMatch(m.Groups[1].Value, "[,.]"))); continue; } var b = Regex.Match(text, @"\$\s?([0-9OoIlS]{1,3}\.[0-9OoIlS]{1,2})8(?![0-9A-Za-z])"); if (b.Success && Parse.Money("$" + b.Groups[1].Value + "B", out v) && v > 0) reads.Add(Tuple.Create(v, true)); } var pointed = reads.Where(x => x.Item2).ToList(); var pool = pointed.Count > 0 ? pointed : reads; return pool.Count == 0 ? first : pool.GroupBy(x => x.Item1).OrderByDescending(g => g.Count()).ThenBy(g => g.Key).First().Key; } static double PerLotAgain(Frame f, OcrLine line) { int h = Math.Max(8, line.Box.Height); var r = Rectangle.Intersect(Rectangle.Inflate(line.Box, h / 2, h / 2), new Rectangle(0, 0, f.Width, f.Height)); foreach (var t in new[] { Tuple.Create(3, Prep.None), Tuple.Create(2, Prep.None), Tuple.Create(3, Prep.Contrast), Tuple.Create(4, Prep.None), Tuple.Create(5, Prep.None), Tuple.Create(3, Prep.DarkText) }) { var m = PerLotRx.Match(Ocr.ReadText(f, r, t.Item1, t.Item2)); double v; if (m.Success && Regex.IsMatch(m.Groups[1].Value, "[,.]") && Parse.Money(m.Value.Replace("/", " ").Trim(), out v) && v > 0) return v; } return -1; } public PropertiesPage ReadProperties(Frame f) { var area = PageArea(f); int s = PageScale(f); var lines = Ocr.Read(f, area, s, Prep.None); var block = lines.FirstOrDefault(l => Parse.Key(l.Text) == "YOURBIOCK" && l.Box.Y < area.Top + area.Height / 4); if (block == null) { var merged = lines.FirstOrDefault(l => Parse.Key(l.Text).EndsWith("YOURBIOCK") && Parse.Key(l.Text).Length > 12 && l.Box.Y < area.Top + area.Height / 4); if (merged != null) { int at = merged.Text.ToUpperInvariant().LastIndexOf("YOUR"); int mh = Math.Max(8, merged.Box.Height); int x0 = merged.Box.X + (int)(merged.Box.Width * (at > 0 ? at / (double)merged.Text.Length : 0.5)) - mh; var spot = Rectangle.Intersect(Rectangle.FromLTRB(x0, merged.Box.Y - 4, merged.Box.Right + 6, merged.Box.Bottom + 4), area); foreach (int sc in new[] { s, s + 1 }) { if (block != null || spot.Width < 10 || spot.Height < 5) break; block = Ocr.Read(f, spot, sc, Prep.None).FirstOrDefault(l => Parse.Key(l.Text) == "YOURBIOCK"); } if (block == null) block = new OcrLine { Text = "YOUR BLOCK", Box = Rectangle.FromLTRB(x0 + mh, merged.Box.Y, merged.Box.Right, merged.Box.Bottom) }; } } if (block == null) return null; int h = Math.Max(8, block.Box.Height); var tab = lines.FirstOrDefault(l => Parse.Key(l.Text) == "BUIID" && Math.Abs(l.CenterY - block.CenterY) < h && l.Box.X > block.Box.Right); var tabSpot = Rectangle.Intersect(new Rectangle(block.Box.Right + h / 2, block.Box.Y - h, block.Box.Width * 2, 3 * h), area); foreach (var t in new[] { Tuple.Create(s, Prep.None), Tuple.Create(s + 1, Prep.GreyText), Tuple.Create(s + 1, Prep.Contrast) }) { if (tab != null || tabSpot.Width < 10 || tabSpot.Height < 5) break; tab = Ocr.Read(f, tabSpot, t.Item1, t.Item2).FirstOrDefault(l => Parse.Key(l.Text) == "BUIID" && Math.Abs(l.CenterY - block.CenterY) < h); } var page = new PropertiesPage { BuildTab = tab, BlockTab = block, Area = area }; var all = new List(lines); foreach (var d in Ocr.Read(f, area, s, Prep.DarkText)) if (BuildPriceRx.IsMatch(d.Text) && !all.Any(l => l.Box.IntersectsWith(d.Box) && BuildPriceRx.IsMatch(l.Text))) all.Add(d); if (!lines.Any(l => Parse.Key(l.Text) == "IOTS")) foreach (var c in Ocr.Read(f, area, s + 1, Prep.Contrast)) { if (c.Text.Contains("$")) all.RemoveAll(l => l.Box.IntersectsWith(c.Box) && !l.Text.Contains("$")); if (!all.Any(l => l.Box.IntersectsWith(c.Box))) all.Add(c); } var incomes = all.Where(l => Parse.Has(l.Text, "PER LOT") && (PerLotRx.IsMatch(l.Text) || GoldPerDayRx.IsMatch(l.Text))).ToList(); page.Build = incomes.Count > 0; var incomeLine = new Dictionary(); foreach (var inc in incomes) { var card = new PropertyCard(); incomeLine[card] = inc; double v; var gm = GoldPerDayRx.Match(inc.Text); int gold; if (gm.Success && !PerLotRx.IsMatch(inc.Text)) { if (int.TryParse(Parse.Digits(gm.Groups[1].Value), out gold) && gold > 0) card.Gold = gold; } else if (Parse.Money(PerLotRx.Match(inc.Text).Value.Replace("/", " ").Trim(), out v)) card.Income = v; int ih = Math.Max(8, inc.Box.Height), half = Math.Max(inc.Box.Width / 2, 3 * ih); Func column = l => Math.Abs(l.CenterX - inc.CenterX) < half; var name = all.Where(l => l != inc && column(l) && l.Box.Bottom <= inc.Box.Y + ih / 2 && inc.Box.Y - l.Box.Bottom < 2 * ih && Regex.Matches(l.Text, "[A-Za-z]").Count >= 3 && !l.Text.Contains("$")) .OrderByDescending(l => l.Box.Bottom).FirstOrDefault(); if (name != null) card.Name = name.Text.Trim(); var below = all.Where(l => l != inc && column(l) && l.Box.Y >= inc.Box.Bottom - ih / 2 && l.Box.Y - inc.Box.Bottom < 3 * ih) .OrderBy(l => l.Box.Y).ToList(); OcrLine priced = null; foreach (var b in below) { var bm = BuildPriceRx.Match(b.Text); if (bm.Success) { if (Parse.Money("$" + bm.Groups[1].Value, out v)) { card.Price = v; card.Build = b; priced = b; } break; } var lm = Regex.Match(b.Text, @"^\s*LEVEL\s*([0-9OoIlS]{1,3})\s*$", RegexOptions.IgnoreCase); int lv; if (lm.Success && int.TryParse(Parse.Digits(lm.Groups[1].Value), out lv)) { card.Level = lv; break; } if (Parse.Has(b.Text, "YOU HAVE") && Parse.Money(b.Text, out v)) { card.Price = ShortPrice(f, b, v); priced = b; break; } if (BlockFullRx.IsMatch(b.Text) && Parse.Money(b.Text.Substring(b.Text.IndexOf('$')), out v) && v > 0) { card.Price = ShortPrice(f, b, v, !b.Text.Contains(".")); card.BlockFull = true; priced = b; break; } if (BlockFullWordRx.IsMatch(b.Text)) { card.BlockFull = true; card.Price = ShortPrice(f, b, -1, true); if (card.Price > 0) priced = b; break; } } if (priced != null && card.Price > 0 && card.Price < 1000 && (card.Income >= 1e6 || card.Gold > 0)) card.Price = ShortPrice(f, priced, -1, true); page.Cards.Add(card); } foreach (var kv in incomeLine) { var c = kv.Key; var said = PerLotRx.Match(kv.Value.Text); if (c.Income <= 0 || c.Price <= 0 || !said.Success || Regex.IsMatch(said.Groups[1].Value, "[,.]")) continue; var others = page.Cards.Where(o => o != c && o.Income > 0 && o.Price > 0).Select(o => o.Price / o.Income).OrderBy(x => x).ToList(); if (others.Count < 2) continue; double usual = others[others.Count / 2]; if (c.Price / c.Income >= usual / 10) continue; double again = PerLotAgain(f, kv.Value); log("Properties: " + c + " pays back too fast to be right - read again: " + (again > 0 ? "+" + SafehouseInfo.Money(again) + "/hr" : "no point, left out this visit")); c.Income = again; } if (!page.Build) { var lots = lines.FirstOrDefault(l => Parse.Key(l.Text) == "IOTS"); if (lots != null) { int lh = Math.Max(6, lots.Box.Height); var frac = lines.Where(l => l.Box.Y > lots.Box.Y && l.Box.Y - lots.Box.Bottom < 3 * lh && Math.Abs(l.CenterX - lots.CenterX) < 4 * lh).ToList(); var said = frac.Select(l => l.Text).ToList(); foreach (var t in said) if (LotsCount(t, page)) break; var spot = Rectangle.Intersect(new Rectangle(lots.Box.X - lh, lots.Box.Bottom, 14 * lh, 3 * lh), new Rectangle(0, 0, f.Width, f.Height)); if (page.LotsMax < 0 && spot.Width > 10 && spot.Height > 5) foreach (var t in new[] { Tuple.Create(4, Prep.Contrast), Tuple.Create(3, Prep.Contrast), Tuple.Create(2, Prep.WhiteText), Tuple.Create(6, Prep.Contrast), Tuple.Create(5, Prep.DarkText) }) { var more = Ocr.Read(f, spot, t.Item1, t.Item2).Select(l => l.Text).ToList(); said.AddRange(more); if (more.Any(x => LotsCount(x, page))) break; } page.Full = LotsInColour(f, Rectangle.Intersect(new Rectangle(spot.X, spot.Y, spot.Width, (int)((Bridge.On ? 1.45 : 2.6) * lh)), spot)); if (page.Full && page.LotsMax < 0) { foreach (var t in said) { var m = Regex.Match(t, @"^\s*(?:/\s*)?([0-9OoIlS]{1,3})\s*$|^\s*([0-9OoIlS]{1,2})\s*[\s1lI|\[\]]\s*([0-9OoIlS]{1,2})\s*$"); int a, b; if (!m.Success) continue; if (m.Groups[1].Success && int.TryParse(Parse.Digits(m.Groups[1].Value), out a) && a > 0) { page.LotsUsed = page.LotsMax = a; break; } if (m.Groups[2].Success && int.TryParse(Parse.Digits(m.Groups[2].Value), out a) && int.TryParse(Parse.Digits(m.Groups[3].Value), out b) && a == b && a > 0) { page.LotsUsed = page.LotsMax = a; break; } } } var open = lines.Select(l => new { l, m = Regex.Match(l.Text, @"^\s*([0-9OoIlS]{1,3})\s*[O0][PO0R]EN\s*$", RegexOptions.IgnoreCase) }) .FirstOrDefault(x => Math.Abs(x.l.CenterY - lots.CenterY) < 2 * lh && x.m.Success); int o; if (open != null && int.TryParse(Parse.Digits(open.m.Groups[1].Value), out o)) page.LotsOpen = o; else if (page.LotsMax > 0 && page.LotsUsed >= 0) page.LotsOpen = page.LotsMax - page.LotsUsed; else if (page.Full) page.LotsOpen = 0; if (page.Full) page.LotsOpen = 0; } } return page; } static bool LotsCount(string text, PropertiesPage page) { var m = Regex.Match(text ?? "", @"([0-9OoIlS]{1,3})\s*[/|\\]\s*([0-9OoIlS]{1,3})"); int a, b; if (!m.Success || !int.TryParse(Parse.Digits(m.Groups[1].Value), out a) || !int.TryParse(Parse.Digits(m.Groups[2].Value), out b) || a > b || b <= 0) return false; page.LotsUsed = a; page.LotsMax = b; return true; } static bool LotsInColour(Frame f, Rectangle spot) { int colour = 0, white = 0; for (int y = spot.Top; y < spot.Bottom; y++) for (int x = spot.Left; x < spot.Right; x++) { var p = f.Pixel(x, y); int hi = Math.Max(p.R, Math.Max(p.G, p.B)), lo = Math.Min(p.R, Math.Min(p.G, p.B)); if (hi > 150 && hi - lo > 90) colour++; else if (lo > 200) white++; } return colour >= 20 && colour > 3 * white; } public sealed class TutorialBox { public string Hint = ""; public OcrLine Skip, Done; } public TutorialBox ReadTutorial(Frame f) { var p = PageArea(f); var area = new Rectangle(p.Left, p.Top + p.Height * 3 / 4, p.Width, p.Height - p.Height * 3 / 4); int s = PageScale(f); var lines = Ocr.Read(f, area, s, Prep.None); var button = lines.FirstOrDefault(l => Parse.Key(l.Text) == "SKIP") ?? lines.FirstOrDefault(l => Parse.Key(l.Text) == "DONE"); List plainWords = null; foreach (var prep in new[] { Prep.DarkText, Prep.None }) { if (button != null) break; var words = Ocr.ReadWords(f, area, s, prep); if (prep == Prep.None) plainWords = words; button = words.FirstOrDefault(w => Parse.Key(w.Text) == "SKIP") ?? words.FirstOrDefault(w => Parse.Key(w.Text) == "DONE"); } if (button == null && plainWords != null) { var said = plainWords.Where(w => w.Box.X > p.Left + p.Width / 6 && Regex.IsMatch(w.Text, "[A-Za-z]{2}")).ToList(); if (said.Count >= 5) { int mh = said.Select(w => w.Box.Height).OrderBy(x => x).ElementAt(said.Count / 2); var block = said.Where(w => w.Box.Height >= 0.7 * mh).ToList(); int right = block.Max(w => w.Box.Right), top = block.Min(w => w.Box.Y), bottom = block.Max(w => w.Box.Bottom); var spot = Rectangle.Intersect(new Rectangle(right + mh / 3, top - mh, 12 * mh, bottom - top + 2 * mh), area); foreach (var t in new[] { Tuple.Create(3, Prep.DarkText), Tuple.Create(3, Prep.Contrast), Tuple.Create(4, Prep.None), Tuple.Create(2, Prep.DarkText) }) { if (spot.Width < 10 || spot.Height < 10) break; button = Ocr.Read(f, spot, t.Item1, t.Item2).FirstOrDefault(l => Parse.Key(l.Text) == "DONE" || Parse.Key(l.Text) == "SKIP"); if (button != null) break; } } } if (button == null) return null; int h = Math.Max(8, button.Box.Height); var hint = lines.Where(l => l != button && l.Box.X < button.Box.X - h && Math.Abs(l.CenterY - button.CenterY) < 3 * h && l.Box.X > p.Left + p.Width / 6 && Parse.Key(l.Text) != "DONE" && Parse.Key(l.Text) != "SKIP") .OrderBy(l => l.Box.Y / Math.Max(1, h)).ThenBy(l => l.Box.X).ToList(); string text = string.Join(" ", hint.Select(l => Regex.Replace(l.Text.Trim(), @"\s+D\s?o(ne)?\s*$", ""))); if (Regex.Matches(text, "[A-Za-z]").Count < 12) return null; var box = new TutorialBox { Hint = text }; if (Parse.Key(button.Text) == "SKIP") box.Skip = button; else box.Done = button; return box; } public void PressTutorial(OcrLine button) { string k = Parse.Key(button.Text); if (k != "DONE" && k != "SKIP") throw new NeverPressException(button.Text, "not the tutorial's DONE or SKIP"); Press(button, 800); } void PressPropertyBuild(PropertyCard c, PropertiesPage page, double cash) { var b = c.Build; if (b == null || !page.Build || !BuildPriceRx.IsMatch(b.Text) || (c.Income <= 0 && c.Gold <= 0) || c.Price <= 0 || cash < c.Price || !page.Area.Contains(b.Box)) throw new NeverPressException(b != null ? b.Text : "BUILD", "not a property's BUILD within the cash on hand"); NotDisconnected(); if (OnlyNavigate) RefuseInCheck(b.Text); Click(b.CenterX, b.CenterY, 900); } bool BuildSeen(PropertyCard c, double cashBefore, int lotsBefore) { for (int look = 0; look < 3; look++) { if (look > 0) Wait(600); PropertiesPage p; double cash; using (var f = Capture()) { p = ReadProperties(f); cash = ReadHeader(f).Cash; } double down = cashBefore - cash; if (cashBefore > 0 && cash >= 0 && c.Price > 0 && down >= c.Price * 0.9 && down <= c.Price * 1.1) return true; if (p != null && !p.Build && lotsBefore > 0 && p.LotsOpen >= 0 && p.LotsOpen < lotsBefore) return true; } return false; } public sealed class BlockLot { public string Name = ""; public double Income = -1; public int Gold = -1; public Point Picture; public override string ToString() { return (Name.Length > 0 ? Name : "a property") + " (" + (Gold > 0 ? Gold + " gold bar" + (Gold == 1 ? "" : "s") + "/day" : SafehouseInfo.Money(Income) + "/hr") + ")"; } } static readonly Regex LotGoldRx = new Regex(@"^\s*(?:\S{1,2}\s+)?\+?\s*([0-9OoIlS]{1,2})\s*G[o0]\s?[lI1]d\s*Bars?\s*/\s*d\s?a\s?y\s*$", RegexOptions.IgnoreCase); static readonly Regex LotIncomeRx = new Regex(@"^\s*(?:\S{1,2}\s+)?\$\s?[0-9OoIlS]{1,3}(?:[,.][0-9OoIlS]{1,3})*\s?[KMBT]?\s*/\s*hr?\s*$", RegexOptions.IgnoreCase); public List ReadBlockLots(Frame f) { return ReadBlockLots(f, null); } public List ReadBlockLots(Frame f, List goldCards) { var area = PageArea(f); int s = PageScale(f); var lines = new List(); for (int i = 0; i < 5; i++) { int top = area.Top + area.Height * 15 * i / 100; var band = Rectangle.FromLTRB(area.Left, top, area.Right, Math.Min(area.Bottom, top + area.Height * 40 / 100)); foreach (var l in Ocr.Read(f, band, s, Prep.None)) { if ((band.Top > area.Top && l.Box.Y < band.Top + 4) || (band.Bottom < area.Bottom && l.Box.Bottom > band.Bottom - 4)) continue; if (!lines.Any(x => x.Text == l.Text && Math.Abs(x.CenterX - l.CenterX) < 6 && Math.Abs(x.CenterY - l.CenterY) < 6)) lines.Add(l); } } var income = lines.FirstOrDefault(l => Parse.Key(l.Text) == "INCOME"); var notNames = new[] { "PROPERTIES", "YOURBLOCK", "BUILD", "INCOME", "LOTS", "GOLDBARS" }.Select(Parse.Key).ToArray(); var lots = new List(); foreach (var inc in lines.Where(l => LotIncomeRx.IsMatch(l.Text) || LotGoldRx.IsMatch(l.Text))) { int h = Math.Max(8, inc.Box.Height); if (income != null && inc.Box.Y >= income.Box.Y && inc.Box.Y - income.Box.Bottom < 3 * h && Math.Abs(inc.Box.X - income.Box.X) < 4 * h) continue; var name = lines.Where(l => l != inc && l.Box.Bottom < inc.Box.Y && inc.Box.Y - l.Box.Bottom > 3 * h && inc.Box.Y - l.Box.Bottom < 18 * h && Math.Abs(l.CenterX - inc.CenterX) < Math.Max(3 * h, l.Box.Width / 2) && Regex.Matches(l.Text, "[A-Za-z]").Count >= 4 && !l.Text.Contains("$") && !l.Text.Contains("+") && !LotGoldRx.IsMatch(l.Text)) .OrderByDescending(l => l.Box.Bottom).FirstOrDefault(); if (name == null || notNames.Contains(Parse.Key(name.Text)) || name.Box.Y < area.Top + 2 || inc.Box.Bottom > area.Bottom - 2) continue; double v = -1; int gold = -1; var gm = LotGoldRx.Match(inc.Text); if (gm.Success) { if (!int.TryParse(Parse.Digits(gm.Groups[1].Value), out gold) || gold <= 0) continue; } else if (!Parse.Money(inc.Text.Substring(inc.Text.IndexOf('$')).Replace("/", " "), out v) || v <= 0) continue; var at = new Point(name.CenterX, (name.CenterY + inc.CenterY) / 2); if (lots.Any(x => Math.Abs(x.Picture.X - at.X) < 3 * h && Math.Abs(x.Picture.Y - at.Y) < 3 * h)) continue; lots.Add(new BlockLot { Name = name.Text.Trim(), Income = v, Gold = gold, Picture = at }); } if (goldCards != null) foreach (var l in lines) { var card = goldCards.FirstOrDefault(c => c.Gold > 0 && SameName(l.Text, c.Name)); if (card == null || l.Box.Y < area.Top + 2) continue; int h = Math.Max(8, l.Box.Height); if (lots.Any(x => Math.Abs(x.Picture.X - l.CenterX) < 3 * h && x.Picture.Y > l.CenterY && x.Picture.Y - l.CenterY < 12 * h)) continue; var at = new Point(l.CenterX, l.CenterY + 4 * h); if (lots.Any(x => Math.Abs(x.Picture.X - at.X) < 3 * h && Math.Abs(x.Picture.Y - at.Y) < 6 * h)) continue; lots.Add(new BlockLot { Name = l.Text.Trim(), Gold = card.Gold, Picture = at }); } return lots.OrderBy(l => l.Picture.Y).ThenBy(l => l.Picture.X).ToList(); } bool OpenYourBlock() { if (!OpenTab(Tab.Properties, true)) return false; for (int look = 0; look < 3; look++) { PropertiesPage page; using (var f = Capture()) page = ReadProperties(f); if (page == null) { Wait(700); continue; } if (!page.Build) return true; if (page.BlockTab == null) return false; Press(page.BlockTab, 1000); } return false; } internal static double PointLost(double read, double plan) { if (read <= 0 || plan <= 0) return read; foreach (double times in new[] { 10.0, 100.0 }) if (Math.Abs(read / plan - times) <= times * 0.01) return plan; return read; } public bool BuildReady(PropertyCard target) { double need; return BuildReady(target, out need); } public bool BuildReady(PropertyCard target, out double need) { need = -1; PropertiesPage page; var card = FindCard(target, out page); if (card != null && card.Build != null && card.Price > 0) return true; if (card != null && card.BlockFull) { double price = card.Price > 0 ? PointLost(card.Price, target.Price) : target.Price; Header h; using (var f = Capture()) h = ReadHeader(f); if (price > 0 && h.Cash >= price) return true; if (price > 0 && h.Cash >= 0 && h.Banked > 0 && h.Cash + h.Banked >= price) { need = price; return false; } } Snapshot(card == null ? "property card not found" : "property build not gold", 60); return false; } PropertyCard FindCard(PropertyCard target, out PropertiesPage page) { page = null; if (target == null || !OpenTab(Tab.Properties, true)) return null; bool top = false; string last = null; for (int look = 0; look < 12; look++) { using (var f = Capture()) page = ReadProperties(f); if (page == null) { Wait(700); continue; } if (!page.Build) { if (page.BuildTab == null) return null; Press(page.BuildTab, 1000); continue; } if (!top) { top = true; ScrollPage(0.62, 15); Wait(500); continue; } var card = page.Cards.FirstOrDefault(c => SameCard(c, target)); if (card != null) return card; string sig = string.Join("|", page.Cards.Select(c => c.Name + "@" + (c.Build != null ? c.Build.Box.Y : 0))); if (sig == last) break; last = sig; ScrollPage(0.62, -ListNotches(3)); Wait(500); } page = null; return null; } public List ReadBuildCards() { if (!OpenTab(Tab.Properties, true)) return null; var all = new List(); bool top = false, read = false; string last = null; for (int look = 0; look < 14; look++) { PropertiesPage page; using (var f = Capture()) page = ReadProperties(f); if (page == null) { Wait(700); continue; } if (!page.Build) { if (page.BuildTab == null) return null; Press(page.BuildTab, 1000); continue; } if (!top) { top = true; ScrollPage(0.62, 15); Wait(500); continue; } read = true; foreach (var c in page.Cards.Where(c => c.Level < 0 && c.Price > 0 && (c.Income > 0 || c.Gold > 0))) { all.RemoveAll(x => SameCard(x, c)); c.Build = null; all.Add(c); } string sig = string.Join("|", page.Cards.Select(c => c.Name + c.Price)); if (sig == last) break; last = sig; ScrollPage(0.62, -ListNotches(3)); Wait(500); } return read ? all : null; } public bool BuildCard(PropertyCard target, out string why) { bool grey; return BuildCard(target, out why, out grey); } public bool BuildCard(PropertyCard target, out string why, out bool grey) { why = null; grey = false; PropertiesPage page; var card = FindCard(target, out page); if (card == null) { why = target.Name + "'s card wasn't found on the BUILD page"; Snapshot("property card not found", 60); return false; } double cash; using (var f = Capture()) cash = ReadHeader(f).Cash; if (card.Price > 0) card.Price = PointLost(card.Price, target.Price); if (card.Build == null && !card.BlockFull && cash > 0 && target.Price > 0 && cash >= target.Price * 1.01) { Wait(800); var again = FindCard(target, out page); if (again != null && again.Build != null) { card = again; if (card.Price > 0) card.Price = PointLost(card.Price, target.Price); } else if (again != null && !again.BlockFull) { grey = true; Snapshot("property build not gold", 60); } } if (card.Build == null || card.Price <= 0 || cash < 0 || card.Price > cash) { why = target.Name + "'s BUILD isn't showing gold"; if (card.Build != null) grey = false; return false; } PressPropertyBuild(card, page, cash); string toast = ReadToast(1500, t => Parse.Has(t, "BOUGHT")); using (var f = Capture()) CheckForPurchasePrompt(Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None)); if (toast != null || BuildSeen(card, cash, -1)) return true; why = "BUILD on " + card + " did nothing"; Snapshot("property build did nothing", 60); return false; } public List ReadBlock(List goldCards, out int lotsOpen, int notches = 3) { lotsOpen = -1; if (!OpenYourBlock()) return null; ScrollPage(0.62, 15); Wait(400); PropertiesPage top; for (int look = 0; look < 3 && lotsOpen < 0; look++) { if (look == 2) ScrollPage(0.62, 15); if (look > 0) Wait(700); using (var f = Capture()) top = ReadProperties(f); if (top != null && !top.Build) lotsOpen = top.LotsOpen; } var all = new List(); string last = null; for (int page = 0; page < 24 / notches; page++) { List seen; using (var f = Capture()) seen = ReadBlockLots(f, goldCards); string sig = string.Join("|", seen.Select(l => l.Name + l.Income + l.Gold + "@" + l.Picture.Y)); if (sig == last) break; last = sig; all.AddRange(seen); ScrollPage(0.62, -ListNotches(notches)); } return all; } public int GoldLotsOwned(List lots) { var kinds = new List(); foreach (var l in lots.Where(l => l.Gold > 0)) if (!kinds.Any(k => SameLot(k, l))) kinds.Add(l); if (kinds.Count == 0) return 0; int total = 0; foreach (var kind in kinds) { var lot = LotInSight(kind); if (lot == null) return -1; NotDisconnected(); if (OnlyNavigate) RefuseInCheck(lot.Name); Click(lot.Picture.X, lot.Picture.Y, 900); LotWindow w = null; for (int look = 0; look < 3 && w == null; look++) { if (look > 0) Wait(500); using (var f = Capture()) w = ReadLotWindow(f); } if (w == null) { Snapshot("gold lot window not read", 60, true); return -1; } bool ok = !w.Question && w.Names(kind) && w.Owned > 0; int owned = w.Owned; CloseLotWindow(w); if (!ok) { Snapshot("gold lot window not read", 60, true); return -1; } total += owned; } return total; } BlockLot LotInSight(BlockLot want) { if (!OpenYourBlock()) return null; ScrollPage(0.62, 15); var gold = want.Gold > 0 ? new List { new PropertyCard { Name = want.Name, Gold = want.Gold } } : null; string last = null; for (int page = 0; page < 8; page++) { List seen; using (var f = Capture()) seen = ReadBlockLots(f, gold); var lot = seen.FirstOrDefault(l => SameLot(l, want)); if (lot != null) return lot; string sig = string.Join("|", seen.Select(l => l.Name + l.Income + l.Gold + "@" + l.Picture.Y)); if (sig == last) break; last = sig; ScrollPage(0.62, -ListNotches(3)); } return null; } public static BlockLot WorstLot(params List[] reads) { return reads.Where(r => r != null).SelectMany(r => r).Where(l => l.Income > 0 && l.Gold <= 0).OrderBy(l => l.Income).FirstOrDefault(); } public static BlockLot WorstGoldLot(params List[] reads) { return reads.Where(r => r != null).SelectMany(r => r).Where(l => l.Gold > 0).OrderBy(l => l.Gold).FirstOrDefault(); } void PressDemolish(OcrLine b, bool ok, string why) { if (b == null || !ok || Parse.Key(b.Text) != "DEMOIISH") throw new NeverPressException(b != null ? b.Text : "DEMOLISH", why); NotDisconnected(); if (OnlyNavigate) RefuseInCheck(b.Text); Click(b.CenterX, b.CenterY, 900); } static bool SameName(string a, string b) { string x = Parse.Key(a ?? ""), y = Parse.Key(b ?? ""); return x.Length >= 4 && y.Length >= 4 && (x == y || x.Contains(y) || y.Contains(x) || View.Distance(x, y) <= Math.Max(1, Math.Min(x.Length, y.Length) / 8) || WordLost(a, b)); } static bool WordLost(string a, string b) { Func> words = s => (s ?? "").Split(' ').Select(Parse.Key).Where(k => k.Length > 0).ToList(); List shorter = words(a), longer = words(b); if (shorter.Count > longer.Count) { var t = shorter; shorter = longer; longer = t; } if (shorter.Count < 2 || shorter.Count == longer.Count) return false; int i = 0; foreach (var w in longer) if (i < shorter.Count && w == shorter[i]) i++; return i == shorter.Count; } static bool SameIncome(double a, double b) { return a > 0 && b > 0 && Math.Abs(a - b) <= 0.02 * Math.Max(a, b); } static bool SameLot(BlockLot a, BlockLot b) { return SameName(a.Name, b.Name) && (b.Gold > 0 ? a.Gold == b.Gold : a.Gold <= 0 && SameIncome(a.Income, b.Income)); } internal static bool SameCard(PropertyCard a, PropertyCard b) { return SameName(a.Name, b.Name) && (b.Gold > 0 ? a.Gold == b.Gold : a.Gold <= 0 && SameIncome(a.Income, b.Income)); } public sealed class LotWindow { public string Name = ""; public double Income = -1; public OcrLine Demolish, Close, Cancel; public bool Question; public OcrLine Move; public OcrLine X; public int Gold = -1; public int Owned = -1; public bool Names(BlockLot lot) { return SameName(Name, lot.Name) && (lot.Gold > 0 ? Gold == lot.Gold : SameIncome(Income, lot.Income)); } public string Says { get { return Name + " (" + (Gold > 0 ? Gold + " gold bars/day" : SafehouseInfo.Money(Income) + "/hr") + ")"; } } } static readonly Regex GoldBarsRx = new Regex(@"([0-9OoIlS]{1,2})\s*G[o0]\s?[lI1]d\s*Bars?", RegexOptions.IgnoreCase); static int GoldIn(string text) { var m = GoldBarsRx.Match(text ?? ""); int n; return m.Success && int.TryParse(Parse.Digits(m.Groups[1].Value), out n) && n > 0 ? n : -1; } public LotWindow ReadLotWindow(Frame f) { var lines = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None); CheckForPurchasePrompt(lines); var demolish = lines.Where(l => Parse.Key(l.Text) == "DEMOIISH").ToList(); foreach (var d in demolish) { int h = Math.Max(8, d.Box.Height); Func level = l => Math.Abs(l.CenterY - d.CenterY) < h; var close = lines.FirstOrDefault(l => Parse.Key(l.Text) == "CIOSE" && level(l) && l.Box.X > d.Box.Right); var cancel = lines.FirstOrDefault(l => Parse.Key(l.Text) == "CANCEI" && level(l) && l.Box.Right < d.Box.X); var move = close != null ? null : lines.FirstOrDefault(l => (Parse.Key(l.Text) == "MOVE" || Parse.Key(l.Text) == "BUIIDANOTHER") && level(l) && l.Box.X > d.Box.Right); var w = new LotWindow { Demolish = d, Close = close, Cancel = cancel, Move = move }; double v; if (close != null || move != null) { if (move != null) w.X = lines.Where(l => l.Box.Bottom < d.Box.Y && l.Box.X > move.Box.X && Regex.IsMatch(l.Text.Trim(), "^[xX×]$")).OrderBy(l => l.Box.Y).FirstOrDefault(); int titleLeft = Math.Max(6 * h, (int)(110 * Math.Min(f.Height / 1009.0, f.Width / 1790.0))); var title = lines.Where(l => l.Box.Bottom < d.Box.Y && l.Box.Height * (Bridge.On ? 4 : 1) >= (Bridge.On ? 5 : 2) * h && Regex.Matches(l.Text, "[A-Za-z]").Count >= 4 && l.Box.X >= d.Box.X - titleLeft && !Regex.IsMatch(l.Text, @"/\s*(day|hr)", RegexOptions.IgnoreCase)) .OrderByDescending(l => l.Box.Height).FirstOrDefault(); var each = lines.FirstOrDefault(l => Parse.Has(l.Text, "EACH EARNS") && l.Box.Bottom < d.Box.Y); var money = each == null ? null : lines.Where(l => l != each && Math.Abs(l.CenterY - each.CenterY) < Math.Max(8, each.Box.Height) && l.Box.X > each.Box.Right) .FirstOrDefault(l => l.Text.Contains("$") || GoldIn(l.Text) > 0); int gold = money != null ? GoldIn(money.Text) : each != null ? GoldIn(each.Text) : -1; var owned = lines.Select(l => Regex.Match(l.Text, @"TOTAL\s*OWNED\s*:?\s*([0-9OoIlS]{1,3})", RegexOptions.IgnoreCase)).FirstOrDefault(m => m.Success); int n; if (owned != null && int.TryParse(Parse.Digits(owned.Groups[1].Value), out n)) w.Owned = n; else { var label = lines.FirstOrDefault(l => Parse.Has(l.Text, "TOTAL OWNED") && l.Box.Bottom < d.Box.Y); if (label != null && money != null && money.Box.Right > label.Box.Right) { int lh = Math.Max(8, label.Box.Height); var ink = InkBox(f, Rectangle.FromLTRB(label.Box.Right + 2 * lh, label.Box.Y - lh / 2, money.Box.Right + lh, label.Box.Bottom + lh / 2)); int o = ink.Width > 0 && ink.Height >= lh / 2 && ink.Height <= 2 * lh ? ReadLoneNumber(f, Rectangle.Inflate(ink, Math.Max(4, lh / 4), Math.Max(4, lh / 4)), new[] { 2, 3, 4 }) : -1; if (o > 0) w.Owned = o; } } if (title == null) continue; if (move != null && w.X == null) { var pa = PopupArea(f); int th = title.Box.Height; foreach (int half in new[] { th / 2, th }) foreach (var prep in new[] { Prep.None, Prep.Contrast }) { if (w.X != null) break; var strip = Rectangle.Intersect(Rectangle.FromLTRB(title.Box.Right + th, title.Box.Y - half, pa.Right, title.Box.Bottom + half), new Rectangle(0, 0, f.Width, f.Height)); if (strip.Width > th && strip.Height > th) w.X = Ocr.Read(f, strip, 2, prep).Where(l => Regex.IsMatch(l.Text.Trim(), "^[xX×]$")).OrderByDescending(l => l.Box.X).FirstOrDefault(); } } if (gold > 0) { w.Name = title.Text.Trim(); w.Gold = gold; return w; } if (money == null || !Parse.Money(money.Text.Replace("/", " "), out v)) continue; w.Name = title.Text.Trim(); w.Income = v; return w; } if (cancel != null) { var title = lines.FirstOrDefault(l => l.Box.Bottom < d.Box.Y && Regex.IsMatch(l.Text, @"^\s*Demolish\s+[^?]+\?\s*(NO\s*REFUND\s*)?$", RegexOptions.IgnoreCase)); var stops = lines.FirstOrDefault(l => Parse.Has(l.Text, "STOPS EARNING") && l.Box.Bottom < d.Box.Y); if (title == null || stops == null) continue; int gold = GoldIn(stops.Text); v = -1; var m = Regex.Match(stops.Text, @"\$\s?[0-9OoIlS]{1,3}(?:[,.][0-9OoIlS]{1,3})*\s?[KMBT]?"); if (gold <= 0 && (!m.Success || !Parse.Money(m.Value, out v))) continue; w.Question = true; w.Name = Regex.Replace(title.Text.Trim(), @"^\s*Demolish\s+|\?.*$", "", RegexOptions.IgnoreCase).Trim(); if (gold > 0) w.Gold = gold; else w.Income = v; return w; } } return null; } public bool DemolishLot(BlockLot worst, out string why) { why = null; if (!OpenYourBlock()) { why = "YOUR BLOCK didn't open"; return false; } ScrollPage(0.62, 15); PropertiesPage first; using (var f = Capture()) first = ReadProperties(f); if (first != null && first.LotsOpen > 0) { why = "a lot is free already (" + first.LotsOpen + " open)"; return false; } BlockLot lot = null; string last = null; for (int page = 0; page < 8 && lot == null; page++) { List seen; using (var f = Capture()) seen = ReadBlockLots(f); lot = seen.FirstOrDefault(l => SameLot(l, worst)); if (lot != null) break; string sig = string.Join("|", seen.Select(l => l.Name + l.Income + "@" + l.Picture.Y)); if (sig == last) break; last = sig; ScrollPage(0.62, -ListNotches(3)); } if (lot == null) { why = "no " + worst + " in sight on YOUR BLOCK"; return false; } NotDisconnected(); if (OnlyNavigate) RefuseInCheck(lot.Name); Click(lot.Picture.X, lot.Picture.Y, 900); LotWindow w = null; for (int look = 0; look < 3 && w == null; look++) { if (look > 0) Wait(500); using (var f = Capture()) w = ReadLotWindow(f); } if (w == null) { why = "its window didn't show"; Snapshot("property lot window not read", 60); return false; } if (w.Question || !w.Names(worst)) { why = "the window showed " + w.Says; CloseLotWindow(w); return false; } PressDemolish(w.Demolish, w.Close != null || w.Move != null, "not the DEMOLISH of the lot's own window"); LotWindow q = null; for (int look = 0; look < 3 && q == null; look++) { if (look > 0) Wait(500); using (var f = Capture()) q = ReadLotWindow(f); } if (q == null || !q.Question || !q.Names(worst)) { why = q == null ? "no \"Demolish ...?\" question after DEMOLISH" : "the question named " + q.Says; Snapshot("property demolish question", 0, true); if (q != null) CloseLotWindow(q); return false; } PressDemolish(q.Demolish, q.Cancel != null, "not the DEMOLISH beside CANCEL of the question"); string toast = ReadToast(1500, t => Parse.Has(t, "DELETED")); PropertiesPage after = null; for (int look = 0; look < 3 && after == null; look++) { if (look > 0) Wait(600); using (var f = Capture()) { CheckForPurchasePrompt(Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None)); after = ReadProperties(f); if (toast == null) { var corner = new Rectangle(f.Width * 3 / 5, 0, f.Width - f.Width * 3 / 5, f.Height / 5); var said = Ocr.Read(f, corner, View.ScaleFor(20.0 * f.Height / 1009), Prep.None).FirstOrDefault(l => Parse.Has(l.Text, "DELETED")); if (said != null) toast = said.Text; } } } if (toast != null || (after != null && after.LotsOpen > 0)) return true; why = "the game didn't say it went"; Snapshot("property demolish not seen", 60); return false; } void CloseLotWindow(LotWindow w) { if (w.Close != null) { Press(w.Close, 700); return; } if (w.X != null) { Press(w.X, 700); return; } if (w.Cancel == null || Parse.Key(w.Cancel.Text) != "CANCEI") return; NotDisconnected(); Click(w.Cancel.CenterX, w.Cancel.CenterY, 700); } public sealed class CollectionState { public int Deposited = -1, Total = -1, Attack = -1, Defense = -1; public override string ToString() { return (Deposited >= 0 ? Deposited + " of " + Total : "? of ?") + (Attack >= 0 ? ", attack +" + Attack : "") + (Defense >= 0 ? ", defense +" + Defense : ""); } } static readonly Regex DepositedRx = new Regex(@"(?:sited\s*|^\s*)([0-9OoIlS,]{1,5})\s*(?:[o0O]f)\b\s*([0-9OoIlS,]{1,5})?", RegexOptions.IgnoreCase); static readonly Regex BonusRx = new Regex(@"(Attack|Defense)\s*\+\s*([0-9OoIlS,]{1,7})", RegexOptions.IgnoreCase); static readonly Regex CollectedRx = new Regex(@"([0-9OoIlS,]{1,5})\s*[/|]\s*([0-9OoIlS,]{1,5})", RegexOptions.IgnoreCase); string collectionSeen; public OcrLine FindCollectionDeposit(Frame f, out CollectionState state) { state = new CollectionState(); var area = PageArea(f); int s = PageScale(f); OcrLine button = null; bool sawTitle = false, sawCounter = false, sawDeposit = false, sawWithdraw = false; int cardLines = 0; foreach (var prep in new[] { Prep.None, Prep.DarkText, Prep.WhiteText }) { var lines = Ocr.Read(f, area, s, prep); if (BankCard(lines) != null) { collectionSeen = "the Bank's page"; return null; } var top = lines.Where(l => l.Box.Y < area.Top + area.Height / 4).ToList(); ReadCollectionCounts(top, state); cardLines = Math.Max(cardLines, lines.Count(l => Parse.Has(l.Text, "DEPOSIT FOR") || Parse.Has(l.Text, "NOT DISCOVERED"))); sawTitle |= top.Any(l => Parse.Key(l.Text) == "COIIECTION"); sawCounter |= top.Any(l => Parse.Key(l.Text).Contains("COIIECTED")); sawDeposit |= top.Any(l => Parse.Key(l.Text) == "DEPOSITAII"); sawWithdraw |= top.Any(l => Parse.Key(l.Text).Contains("DRAWAII")); if (button != null) continue; var title = top.FirstOrDefault(l => Parse.Key(l.Text) == "COIIECTION"); var hint = top.FirstOrDefault(l => Parse.Has(l.Text, "PERMANENT") || Parse.Has(l.Text, "EARN PERM") || Parse.Has(l.Text, "DEPOSIT ITEMS")); foreach (var b in top.Where(l => Parse.Key(l.Text) == "DEPOSITAII")) { int h = Math.Max(8, b.Box.Height); Func level = l => Math.Abs(l.CenterY - b.CenterY) < 2 * h; bool titleLeft = title != null && title.Box.Right < b.Box.X && level(title); bool hintLeft = hint != null && hint.Box.Right <= b.Box.X + h && b.Box.X - hint.Box.Right < 8 * h && level(hint); bool countRight = top.Any(l => DepositedRx.IsMatch(l.Text) && l.Box.X >= b.Box.Right - h && l.Box.X - b.Box.Right < 8 * h && level(l)); bool withdraw = top.Any(l => Parse.Key(l.Text).Contains("WITHDRAW")); bool collectedAbove = title != null && title.Box.Right < b.Box.X && title.Box.Bottom <= b.Box.Y && b.Box.Y - title.Box.Bottom < 5 * h && top.Any(l => Parse.Key(l.Text).Contains("COIIECTED") && l.Box.X > title.Box.Right && l.Box.Y < b.Box.Y && b.Box.Y - l.Box.Bottom < 5 * h); bool withdrawRight = top.Any(l => Parse.Key(l.Text).Contains("DRAWAII") && l.Box.X >= b.Box.Right && l.Box.X - b.Box.Right < Math.Max(12 * h, 5 * b.Box.Width) && level(l)); bool redesigned = withdrawRight && cardLines >= 2; bool counterAbove = withdrawRight && state.Deposited >= 0 && top.Any(l => Parse.Key(l.Text).Contains("COIIECTED") && l.Box.Bottom <= b.Box.Y && b.Box.Y - l.Box.Bottom < 5 * h && l.Box.Right > b.Box.X - 2 * h && l.Box.X < b.Box.Right); if (((titleLeft || hintLeft || countRight) && !withdraw) || collectedAbove || redesigned || counterAbove) { button = b; break; } } } collectionSeen = (button != null ? "found" : "not found") + "; read: page title " + (sawTitle ? "yes" : "no") + ", COLLECTED counter " + (sawCounter ? "yes" : "no") + ", DEPOSIT ALL " + (sawDeposit ? "yes" : "no") + ", WITHDRAW ALL " + (sawWithdraw ? "yes" : "no") + ", " + cardLines + " card lines"; if (button != null && (state.Attack < 0 || state.Defense < 0)) { var header = new Rectangle(area.X, area.Y, area.Width, button.Box.Bottom - area.Y + 10); var labels = Ocr.Read(f, header, s, Prep.None); ReadCollectionCounts(labels, state); foreach (var label in labels) { string key = Parse.Key(label.Text); if (!((key == "ATTACK" && state.Attack < 0) || (key == "DEFENSE" && state.Defense < 0))) continue; int h = Math.Max(8, label.Box.Height); var valueArea = new Rectangle(label.Box.Right, label.Box.Y - h / 2, Math.Min(14 * h, area.Right - label.Box.Right), 2 * h); var value = Ocr.Read(f, valueArea, Math.Max(3, s), Prep.None) .FirstOrDefault(l => Regex.IsMatch(l.Text.Trim(), @"^\+\s*[0-9OoIlS,]+$")); int bonus; if (value == null || !int.TryParse(Parse.Digits(value.Text), out bonus)) continue; if (key == "ATTACK") state.Attack = bonus; else state.Defense = bonus; } } return button; } static void ReadCollectionCounts(List top, CollectionState state) { var collected = top.FirstOrDefault(l => Parse.Key(l.Text).Contains("COIIECTED")); if (collected != null && state.Deposited < 0) { int h = Math.Max(8, collected.Box.Height); foreach (var l in top.Where(l => l == collected || (l.Box.X >= collected.Box.Right - h && l.Box.X - collected.Box.Right < 26 * h && Math.Abs(l.CenterY - collected.CenterY) < h))) { var m = CollectedRx.Match(l.Text); int count, total; if (m.Success && int.TryParse(Parse.Digits(m.Groups[1].Value), out count) && int.TryParse(Parse.Digits(m.Groups[2].Value), out total) && count <= total) { state.Deposited = count; state.Total = total; break; } } } foreach (var l in top) { var d = DepositedRx.Match(l.Text); int a, b; if (d.Success && state.Deposited < 0 && int.TryParse(Parse.Digits(d.Groups[1].Value), out a)) { state.Deposited = a; if (d.Groups[2].Success && int.TryParse(Parse.Digits(d.Groups[2].Value), out b)) state.Total = b; else { int h = Math.Max(8, l.Box.Height); var below = top.FirstOrDefault(n => n != l && n.Box.Y > l.Box.Y && n.Box.Y - l.Box.Bottom < h && n.Box.X < l.Box.Right && n.Box.Right > l.Box.X && Regex.IsMatch(n.Text.Trim(), "^[0-9OoIlS,]{1,5}$")); if (below != null && int.TryParse(Parse.Digits(below.Text), out b)) state.Total = b; } } foreach (Match m in BonusRx.Matches(l.Text)) { int v; if (!int.TryParse(Parse.Digits(m.Groups[2].Value), out v)) continue; if (m.Groups[1].Value.ToUpperInvariant() == "ATTACK" && state.Attack < 0) state.Attack = v; if (m.Groups[1].Value.ToUpperInvariant() == "DEFENSE" && state.Defense < 0) state.Defense = v; } string key = Parse.Key(l.Text); if (key == "ATTACK" || key == "DEFENSE") { int h = Math.Max(8, l.Box.Height); var value = top.Where(n => n.Box.X >= l.Box.Right && n.Box.X - l.Box.Right < 12 * h && Math.Abs(n.CenterY - l.CenterY) < h / 2 + 1 && Regex.IsMatch(n.Text.Trim(), @"^\+\s*[0-9OoIlS,]+$")) .OrderBy(n => n.Box.X).FirstOrDefault(); int v; if (value != null && int.TryParse(Parse.Digits(value.Text), out v)) { if (key == "ATTACK" && state.Attack < 0) state.Attack = v; if (key == "DEFENSE" && state.Defense < 0) state.Defense = v; } } } } public sealed class DepositResult { public CollectionState Before, After; public string Toast, Window; } bool depositPictured; public DepositResult DepositCollection() { if (!OpenTab(Tab.Collection)) return null; var r = new DepositResult(); OcrLine button; using (var f = Capture()) button = FindCollectionDeposit(f, out r.Before); if (button == null) { Wait(700); using (var f = Capture()) button = FindCollectionDeposit(f, out r.Before); } if (button == null) { CloseTerritoryToasts(); Wait(2500); using (var f = Capture()) button = FindCollectionDeposit(f, out r.Before); } if (button == null) { log("Collection: DEPOSIT ALL not found - nothing pressed (" + collectionSeen + ")"); Snapshot("collection deposit not found", 60); return null; } CheckForPurchaseWindow(); Press(button, 300); if (!depositPictured) { depositPictured = true; Snapshot("collection deposit pressed", 0, true); } r.Toast = ReadToast(1500, t => (Parse.Has(t, "DEPOSIT") || Parse.Has(t, "COLLECTION")) && !Parse.Has(t, "BANKED")); for (int look = 0; look < 2; look++) { using (var f = Capture()) { var popup = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None); CheckForPurchasePrompt(popup); if (popup.Any(l => { string k = Parse.Key(l.Text); return k.Contains("CONFIRM") || k.Contains("AREYOUSURE") || k == "YES" || k == "NO"; })) r.Window = string.Join(" ", popup.Select(l => l.Text)); FindCollectionDeposit(f, out r.After); } if (r.Window != null || r.Toast != null || r.After.Deposited != r.Before.Deposited || r.After.Attack != r.Before.Attack) break; Wait(700); } return r; } static readonly string[] StatsRows = { "RESPECT", "ATTACK POWER", "DEFENSE POWER", "PROPERTY INCOME" }; static readonly string[] CrewRows = { "HENCHMEN", "EQUIPMENT" }; static readonly string[] UpgradeRows = { "SKILL POINTS", "ENERGY", "STAMINA", "HEALTH", "BASE ATTACK", "BASE DEFENSE", "PROPERTY CAPACITY" }; static readonly string[] FamilyRows = { "RANK", "MEMBERS" }; static readonly string[] PanelTitles = { "YOUR STATS", "YOUR CREW", "YOUR FAMILY", "YOUR UPGRADES", "YOUR RESIDENCE" }; public SafehouseInfo ReadSafehouse() { if (!OpenTab(Tab.Safehouse)) return null; ScrollPage(0.54, 15); var s = new SafehouseInfo(); string last = null; for (int page = 0; page < 8 && !s.Complete; page++) { string sig; using (var f = Capture()) sig = ReadSafehouseFrame(f, s); if (sig == last) break; last = sig; ScrollPage(0.54, -ListNotches(5)); } return s; } static readonly Regex UpgradePriceRx = new Regex(@"^\s*UPGRADE\s*(?:\$|S(?=\s?[0-9]))\s?([0-9OoIlS]{1,3}(?:[,.][0-9OoIlS]{1,3})*\s*[KMBT]?)\s*$", RegexOptions.IgnoreCase); public sealed class SafehouseUpgrade { public int Level = -1, MaxLevel = -1; public string Next; public double Price = -1; public bool Gold; public bool Upgraded, Confirmed; public SafehouseUpgrade After; public string Toast, Window; public string Why; public int NeedLevel = -1; public string Seen; } static readonly Regex RequiresLevelRx = new Regex(@"REQUIRES\s*LEVE[LI1]\s*([0-9OoIlS]{1,4})", RegexOptions.IgnoreCase); OcrLine ReadUpgradeButton(Frame f, SafehouseUpgrade r) { var area = PageArea(f); int scale = PageScale(f); var lines = Ocr.Read(f, area, scale, Prep.None); var s = new SafehouseInfo(); SafehouseTop(f, scale, lines, s); if (s.Level <= 0) foreach (var t in new[] { Tuple.Create(scale, Prep.WhiteText), Tuple.Create(scale, Prep.WhiteSoft), Tuple.Create(scale + 1, Prep.None) }) { var more = Ocr.Read(f, area, t.Item1, t.Item2); SafehouseTop(f, t.Item1, more, s); lines.AddRange(more); if (s.Level > 0) break; } r.Level = s.Level; r.MaxLevel = s.MaxLevel; r.Next = s.Next; lines = JoinWrapped(lines, t => Parse.Key(t) == "REQUIRES", t => Regex.IsMatch(t, @"^\s*LEVE[LI1]\s*[0-9OoIlS]{1,4}\s*$", RegexOptions.IgnoreCase)); lines = JoinWrapped(JoinUpgrade(lines), t => Parse.Key(t) == "UPGRADE", t => Regex.IsMatch(t, @"^\s*\$\s?[0-9OoIlS]")); var needs = lines.FirstOrDefault(l => RequiresLevelRx.IsMatch(l.Text) && l.CenterX > area.X + area.Width / 2); int lv; if (needs != null && (s.Level > 0 || s.Next != null) && int.TryParse(Parse.Digits(RequiresLevelRx.Match(needs.Text).Groups[1].Value), out lv) && lv > 0) { r.NeedLevel = lv; return null; } if (s.Level <= 0) return null; Func priceOf = l => { double v; if (l.CenterX < area.X + area.Width / 2 || !UpgradePriceRx.IsMatch(l.Text)) return -1; string p = UpgradePriceRx.Match(l.Text).Groups[1].Value.Trim(); if (Parse.Money("$" + p, out v) && v >= 1000) return v; var e8 = Regex.Match(p, @"^([0-9]{1,3}(?:[.,][0-9]{1,2})?)8$"); return e8.Success && Parse.Money("$" + e8.Groups[1].Value.Replace(',', '.') + "B", out v) && v >= 1e9 ? v : -1; }; var up = lines.FirstOrDefault(l => priceOf(l) > 0) ?? JoinWrapped(JoinUpgrade(Ocr.Read(f, area, scale, Prep.DarkText)), t => Parse.Key(t) == "UPGRADE", t => Regex.IsMatch(t, @"^\s*\$\s?[0-9OoIlS]")).FirstOrDefault(l => priceOf(l) > 0) ?? RereadUpgrade(f, lines, area, scale, priceOf); if (up == null) { var five = lines.FirstOrDefault(l => l.CenterX >= area.X + area.Width / 2 && Regex.IsMatch(l.Text, @"^\s*UPGRADE\s*5[0-9]{1,2}(?:[.,][0-9])?\s*[KMBT]\s*$", RegexOptions.IgnoreCase)); double v; string rest = five == null ? "" : Regex.Match(five.Text, @"5([0-9.,]+\s*[KMBT])", RegexOptions.IgnoreCase).Groups[1].Value.Replace(" ", ""); if (five != null && GoldFace(f, five.Box) && Parse.Money("$1" + rest, out v) && v >= 1e6) { r.Price = v; r.Gold = true; return new OcrLine { Text = "UPGRADE $1" + rest, Box = five.Box }; } } if (up == null) { r.Seen = string.Join(" / ", lines.Where(l => Parse.Key(l.Text).Contains("UPGRADE") && l.CenterX >= area.X + area.Width / 2).Select(l => l.Text.Trim())); return null; } r.Price = priceOf(up); r.Gold = GoldFace(f, up.Box); return up; } static OcrLine RereadUpgrade(Frame f, List lines, Rectangle area, int scale, Func priceOf) { var word = lines.Where(l => Parse.Key(l.Text).StartsWith("UPGRADE") && l.CenterX >= area.X + area.Width / 2).OrderBy(l => l.Box.Y).FirstOrDefault(); if (word == null) return null; int h = Math.Max(8, word.Box.Height); var box = Rectangle.Intersect(Rectangle.FromLTRB(word.Box.X - h, word.Box.Y - h / 2, word.Box.Right + 4 * h, word.Box.Bottom + h / 2), area); var read = new List(); foreach (var t in new[] { Tuple.Create(scale, Prep.WhiteSoft), Tuple.Create(scale + 1, Prep.WhiteSoft), Tuple.Create(scale + 1, Prep.None), Tuple.Create(scale, Prep.GreyText), Tuple.Create(scale + 1, Prep.Contrast), Tuple.Create(scale + 1, Prep.DarkText) }) { var l = Ocr.Read(f, box, t.Item1, t.Item2).FirstOrDefault(x => priceOf(x) > 0); if (l == null) continue; var same = read.FirstOrDefault(x => priceOf(x) == priceOf(l)); if (same != null) return same; read.Add(l); } return null; } static bool GoldFace(Frame f, Rectangle text) { int h = Math.Max(6, text.Height), gold = 0, n = 0; foreach (int side in new[] { -1, 1 }) { int x0 = side < 0 ? text.X - h * 7 / 10 : text.Right + h / 5, x1 = x0 + h / 2; for (int x = x0; x <= x1; x++) for (int y = text.Y + h / 4; y <= text.Bottom - h / 4; y++) { if (x < 0 || y < 0 || x >= f.Width || y >= f.Height) continue; n++; if (GoldIsh(f, x, y)) gold++; } } return n > 0 && gold >= n * 6 / 10; } public SafehouseUpgrade UpgradeSafehouse(double keep) { var r = new SafehouseUpgrade(); if (!OpenTab(Tab.Safehouse)) { r.Why = "the Safehouse didn't open"; return r; } ScrollPage(0.54, 15); OcrLine button; double cash; using (var f = Capture()) { button = ReadUpgradeButton(f, r); cash = ReadHeader(f).Cash; } for (int again = 0; again < 2 && (button == null || r.Next == null) && r.NeedLevel <= 0 && !(r.Level > 0 && r.Level >= r.MaxLevel); again++) { Wait(800); r = new SafehouseUpgrade(); using (var f = Capture()) { button = ReadUpgradeButton(f, r); cash = ReadHeader(f).Cash; } } if (r.Level > 0 && r.MaxLevel > 0 && r.Level >= r.MaxLevel) { r.Why = "top level"; return r; } if (button == null && r.NeedLevel > 0) { r.Why = "needs level " + r.NeedLevel; return r; } if (button == null || r.Next == null) { r.Why = "its UPGRADE didn't read (" + (r.Level > 0 ? "level " + r.Level : "level not read") + ", " + (r.Next != null ? "next " + r.Next : "NEXT not read") + (button != null ? ", \"" + button.Text.Trim() + "\"" : !string.IsNullOrEmpty(r.Seen) ? ", read \"" + r.Seen + "\"" : ", no UPGRADE read") + ")"; Snapshot("safehouse upgrade unread", 60); return r; } if (cash < 0) { r.Why = "the cash on hand didn't read"; return r; } if (!r.Gold || r.Price > cash - Math.Max(0, keep)) { r.Why = "saving up"; return r; } PressSafehouseUpgrade(button, r, cash, keep); r.Toast = ReadToast(1500, t => Parse.Has(t, "UPGRADED") || Parse.Has(t, r.Next)); if (r.Toast != null) r.Upgraded = true; for (int look = 0; look < 4; look++) { if (look > 0) Wait(800); using (var f = Capture()) { var popup = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None); CheckForPurchasePrompt(popup); var after = new SafehouseUpgrade(); ReadUpgradeButton(f, after); if (after.Level > r.Level) { r.Upgraded = true; r.After = after; break; } if (r.Upgraded) continue; if (look >= 1 && ConfirmUpgrade(popup, r)) continue; if (look == 3) r.Window = string.Join(" ", popup.Select(l => l.Text.Trim())); } } return r; } void PressSafehouseUpgrade(OcrLine b, SafehouseUpgrade r, double cash, double keep) { if (b == null || !UpgradePriceRx.IsMatch(b.Text) || r.Level <= 0 || r.Next == null || !r.Gold || r.Price <= 0 || r.Price > cash - Math.Max(0, keep)) throw new NeverPressException(b != null ? b.Text : "UPGRADE", "not the Safehouse's UPGRADE within the cash on hand"); NotDisconnected(); if (OnlyNavigate) RefuseInCheck(b.Text); Click(b.CenterX, b.CenterY, 900); } bool ConfirmUpgrade(List popup, SafehouseUpgrade r) { if (r.Confirmed || popup.Any(l => Parse.Has(l.Text, "ROBUX") || l.Text.Contains("R$"))) return false; bool samePrice = popup.Any(l => { double v; return l.Text.Contains("$") && !UpgradePriceRx.IsMatch(l.Text) && Parse.Money(l.Text, out v) && Math.Abs(v - r.Price) <= r.Price * 0.01; }); var yes = popup.FirstOrDefault(l => { string k = Parse.Key(l.Text); return k == "CONFIRM" || k == "YES"; }); if (!samePrice || yes == null) return false; r.Confirmed = true; log("Safehouse: a window asked to confirm the upgrade (\"" + string.Join(" ", popup.Select(l => l.Text.Trim())) + "\") - " + yes.Text.Trim() + " pressed"); Snapshot("safehouse upgrade confirm", 0, false, false); Press(yes, 900); return true; } public string ReadSafehouseFrame(Frame f, SafehouseInfo s) { var area = PageArea(f); int scale = PageScale(f); var lines = Ocr.Read(f, area, scale, Prep.None); SafehouseTop(f, scale, lines, s); foreach (var l in lines) { var pm = Regex.Match(l.Text, @"PRESTIGE\s*([0-9OoIl])\s*OF\s*([0-9])", RegexOptions.IgnoreCase); if (pm.Success) s.Prestige = int.Parse(Regex.Replace(Regex.Replace(pm.Groups[1].Value, "[Oo]", "0"), "[Il]", "1")); } if (s.Level < 0 && lines.Any(l => { string k = Parse.Key(l.Text); return k.StartsWith("UPGRADE") || Regex.IsMatch(k, "^NEXT[A-Z]{4}") || RequiresLevelRx.IsMatch(l.Text); })) foreach (var t in new[] { Tuple.Create(scale, Prep.WhiteText), Tuple.Create(scale, Prep.WhiteSoft), Tuple.Create(scale + 1, Prep.None) }) { SafehouseTop(f, t.Item1, Ocr.Read(f, area, t.Item1, t.Item2), s); if (s.Level > 0) break; } var titles = new Dictionary(); foreach (var t in PanelTitles) { var l = View.Label(lines, t); if (l != null) titles[t] = l; } OcrLine rt; int split = titles.TryGetValue("YOUR UPGRADES", out rt) || titles.TryGetValue("YOUR RESIDENCE", out rt) ? rt.Box.X - Math.Max(8, rt.Box.Height) : area.X + area.Width / 2; var left = Rectangle.FromLTRB(area.Left, area.Top, split, area.Bottom); var right = Rectangle.FromLTRB(split, area.Top, area.Right, area.Bottom); rowPitch = 0; var stats = SafehousePanel(f, scale, lines, titles, "YOUR STATS", left, StatsRows); if (stats != null) { SetInt(stats, 0, ref s.Respect); SetInt(stats, 1, ref s.AttackPower); SetInt(stats, 2, ref s.DefensePower); double money; if (s.Income < 0 && stats[3] != null && Parse.Money(stats[3], out money)) s.Income = money; } var up = SafehousePanel(f, scale, lines, titles, "YOUR UPGRADES", right, UpgradeRows); var crew = SafehousePanel(f, scale, lines, titles, "YOUR CREW", left, CrewRows); if (crew != null) { SetOf(crew[0], ref s.Hired, ref s.HireMax); SetOf(crew[1], ref s.Gear, ref s.GearMax); } if (up != null) { SetInt(up, 0, ref s.SkillPoints); SetInt(up, 1, ref s.Energy); SetInt(up, 2, ref s.Stamina); SetInt(up, 3, ref s.Health); SetInt(up, 4, ref s.BaseAttack); SetInt(up, 5, ref s.BaseDefense); SetInt(up, 6, ref s.Capacity); } OcrLine famTitle; if (titles.TryGetValue("YOUR FAMILY", out famTitle)) { if (famTitle.Box.Bottom + 8 * Math.Max(8, famTitle.Box.Height) <= f.Height || View.Label(lines.Where(l => l.Box.Y > famTitle.Box.Bottom).ToList(), "MEMBERS") != null) s.FamilySeen = true; var fam = SafehousePanel(f, scale, lines, titles, "YOUR FAMILY", left, FamilyRows); if (fam != null) { if (s.Rank == null && fam[0] != null && Regex.IsMatch(fam[0], "[A-Za-z]{3}")) s.Rank = fam[0].Trim(); SetOf(fam[1], ref s.Members, ref s.MembersMax); } if (s.Family == null) { var rank = View.Label(lines.Where(l => l.Box.X < split && l.Box.Y > famTitle.Box.Bottom).ToList(), "RANK"); int bottom = rank != null ? rank.Box.Y : famTitle.Box.Bottom + 5 * famTitle.Box.Height; var name = lines.Where(l => l.Box.X < split && l.Box.Y > famTitle.Box.Bottom && l.Box.Bottom <= bottom && Regex.Matches(l.Text, "[A-Za-z]").Count >= 3 && Parse.Key(l.Text) != "OPEN").OrderByDescending(l => l.Box.Height).ToList(); if (name.Any(l => Parse.Key(l.Text) == "NONEYET")) s.Family = "None yet"; else if (name.Count > 0) { var big = name[0]; int h = Math.Max(6, big.Box.Height), z = View.ScaleFor(h); var r = Rectangle.Inflate(big.Box, h / 2, h / 2); string text = new[] { Tuple.Create(z, Prep.WhiteSoft), Tuple.Create(z, Prep.WhiteBright), Tuple.Create(z + 1, Prep.WhiteText) } .Select(t => Ocr.ReadText(f, r, t.Item1, t.Item2, true)).FirstOrDefault(t => Regex.Matches(t, "[A-Za-z]").Count >= 3); s.Family = FamilyName(text ?? big.Text); } } } return string.Join(",", lines.Where(l => !Regex.IsMatch(l.Text, @"\d")).Select(l => Parse.Key(l.Text) + "@" + l.Box.Y)); } static void SetInt(string[] values, int i, ref int field) { if (field >= 0 || values[i] == null) return; var m = Regex.Match(values[i].Trim(), @"([0-9OoIlSB][0-9OoIlSB,]*)$"); int n; if (m.Success && int.TryParse(Parse.Digits(m.Groups[1].Value), out n)) field = n; } static void SetOf(string text, ref int a, ref int b) { if (a >= 0 || text == null) return; var m = Regex.Match(text, @"([0-9OoIlS]{1,4})\s*of\s*([0-9OoIlS]{1,4})", RegexOptions.IgnoreCase); int x, y; if (m.Success && int.TryParse(Parse.Digits(m.Groups[1].Value), out x) && int.TryParse(Parse.Digits(m.Groups[2].Value), out y) && x <= y) { a = x; b = y; } } static string FamilyName(string text) { string t = Regex.Replace(text.Trim(), @"\s{2,}", " "); if (t.Contains("[") && !t.Contains("]")) t = ("Il1|".IndexOf(t[t.Length - 1]) >= 0 ? t.Substring(0, t.Length - 1) : t) + "]"; return t; } void SafehouseTop(Frame f, int scale, List lines, SafehouseInfo s) { var prestige = lines.Where(l => Parse.Key(l.Text).Contains("PRESTIGE")).OrderBy(l => l.Box.Y).FirstOrDefault(); if (prestige != null) lines = lines.Where(l => l.Box.Bottom <= prestige.Box.Y).ToList(); var chip = lines.FirstOrDefault(l => LevelChip.IsMatch(l.Text.ToUpperInvariant())) ?? lines.FirstOrDefault(l => { string k = Parse.Key(l.Text); return k.StartsWith("SAFEHOUSE") && k.Length > 11; }); if (chip != null && s.Level < 0) { var m = LevelChip.Match(ReadAgain(f, chip, scale, t => TopLevelOk(LevelChip.Match(t.ToUpperInvariant()))).ToUpperInvariant()); int a, b; if (m.Success && int.TryParse(Parse.Digits(m.Groups[1].Value), out a) && int.TryParse(Parse.Digits(m.Groups[2].Value), out b) && a > 0 && a <= b && b >= 10) { s.Level = a; s.MaxLevel = b; } } OcrLine lone = null; if (chip == null || s.Level < 0) { lone = lines.FirstOrDefault(l => LoneOf.IsMatch(l.Text) && l.CenterX < PageArea(f).X + PageArea(f).Width / 2 && lines.Any(n => n.Box.Height * (Bridge.On ? 5 : 2) >= (Bridge.On ? 6 : 3) * l.Box.Height && n.Box.Bottom <= l.Box.Y + (Bridge.On ? l.Box.Height / 2 : 2) && n.Box.Bottom >= l.Box.Y - 3 * n.Box.Height && Math.Abs(n.Box.X - l.Box.X) <= 3 * l.Box.Height && !Regex.IsMatch(n.Text, @"\d") && Regex.Matches(n.Text, "[A-Za-z]").Count >= 4)); var m = lone == null ? Match.Empty : LoneOf.Match(ReadAgain(f, lone, scale, t => TopLevelOk(LoneOf.Match(t)))); int a, b; if (m.Success && s.Level < 0 && int.TryParse(Parse.Digits(m.Groups[1].Value), out a) && int.TryParse(Parse.Digits(m.Groups[2].Value), out b) && a > 0 && a <= b && b >= 10) { s.Level = a; s.MaxLevel = b; } if (chip == null) chip = lone; } var bonus = lines.FirstOrDefault(l => Parse.Has(l.Text, "INCOME") && Parse.Has(l.Text, "DEFENSE")); var beside = lone == null ? null : lines.FirstOrDefault(l => l != lone && Percent.IsMatch(l.Text) && Math.Abs(l.CenterY - lone.CenterY) <= lone.Box.Height && l.Box.X > lone.Box.Right && l.Box.X < lone.Box.Right + 15 * lone.Box.Height); foreach (var line in new[] { bonus, beside }.Where(l => l != null)) { if (s.Bonus >= 0) break; var m = Percent.Match(ReadAgain(f, line, scale, t => Percent.IsMatch(t))); if (m.Success) s.Bonus = int.Parse(Parse.Digits(m.Groups[1].Value)); } if (lone != null && s.Bonus < 0) { int h = Math.Max(6, lone.Box.Height); var r = Rectangle.Intersect(Rectangle.FromLTRB(lone.Box.Right + h, lone.Box.Y - h / 2, lone.Box.Right + 15 * h, lone.Box.Bottom + h / 2), PageArea(f)); foreach (var how in new[] { Tuple.Create(scale + 1, Prep.WhiteSoft), Tuple.Create(scale + 1, Prep.None), Tuple.Create(scale + 2, Prep.WhiteSoft), Tuple.Create(scale, Prep.WhiteText) }) { var m = r.Width > 0 && r.Height > 0 ? PercentExact.Match(Ocr.ReadText(f, r, how.Item1, how.Item2)) : Match.Empty; if (m.Success) { s.Bonus = int.Parse(m.Groups[1].Value); break; } } } if (chip != null && s.Estate == null) { var name = lines.Where(l => l.Box.Bottom <= chip.Box.Y + (Bridge.On ? chip.Box.Height / 2 : 2) && l.Box.Bottom >= chip.Box.Y - 3 * Math.Max(l.Box.Height, chip.Box.Height) && l.Box.X < chip.CenterX && !Regex.IsMatch(l.Text, @"\d") && Regex.Matches(l.Text, "[A-Za-z]").Count >= 4 && (lone == null || l.Box.Height * (Bridge.On ? 5 : 2) >= (Bridge.On ? 6 : 3) * lone.Box.Height)) .OrderByDescending(l => l.Box.Bottom).FirstOrDefault(); if (name != null) s.Estate = name.Text.Trim(); } var upgrade = JoinUpgrade(lines).FirstOrDefault(l => Parse.Key(l.Text).StartsWith("UPGRADE")); if (upgrade != null && s.UpgradeCost < 0) { double cost; if (Parse.Money(ReadAgain(f, upgrade, scale, t => { double c; return Parse.Money(t, out c) && c >= 1000; }), out cost) && cost >= 1000) s.UpgradeCost = cost; } if (s.Next == null) { OcrLine next = upgrade == null ? lines.FirstOrDefault(l => Parse.Key(l.Text).StartsWith("NEXT")) : lines.Where(l => l != upgrade && l.Box.Bottom <= upgrade.Box.Y + 2 && l.Box.Bottom >= upgrade.Box.Y - 3 * upgrade.Box.Height && l.Box.Right > upgrade.Box.X - upgrade.Box.Width && l.Box.X < upgrade.Box.Right && Regex.Matches(l.Text, "[A-Za-z]").Count >= 6) .OrderBy(l => l.Box.Y).FirstOrDefault(); if (next != null) { string t = next.Text.Trim(); int colon = t.IndexOf(':'); if (colon >= 0 && colon < 8) t = t.Substring(colon + 1); else t = Regex.Replace(t, @"^(NEXT[^A-Za-z0-9\s]?|N\S{1,3}[.:])\s+", "", RegexOptions.IgnoreCase); string name = t.Split(',')[0].Trim(); if (name.Length >= 3) s.Next = name; var near = lines.Where(l => l == next || (Math.Abs(l.Box.X - next.Box.Right) < 3 * next.Box.Height && Math.Abs(l.CenterY - next.CenterY) <= next.Box.Height) || (l.Box.Y > next.Box.Y && l.Box.Y < next.Box.Bottom + next.Box.Height && l.Box.Right > next.Box.X && l.Box.X < next.Box.Right)); var pm = near.Select(l => PercentExact.Match(l.Text)).FirstOrDefault(x => x.Success); int h = Math.Max(6, next.Box.Height); var r = Rectangle.FromLTRB(next.Box.X - h, next.Box.Y - h, PageArea(f).Right - 4, next.Box.Bottom + 2 * h); var hows = new[] { Tuple.Create(scale, Prep.WhiteSoft), Tuple.Create(scale + 1, Prep.WhiteSoft), Tuple.Create(scale, Prep.WhiteText), Tuple.Create(scale + 1, Prep.None) }; if (pm == null) { foreach (var how in hows) if ((pm = PercentExact.Match(Ocr.ReadText(f, r, how.Item1, how.Item2))).Success) break; if (!pm.Success) pm = null; } if (pm == null) pm = near.Select(l => Percent.Match(l.Text)).FirstOrDefault(x => x.Success); if (pm == null) foreach (var how in hows) if ((pm = Percent.Match(Ocr.ReadText(f, r, how.Item1, how.Item2))).Success) break; if (pm != null && pm.Success) s.NextBonus = int.Parse(Parse.Digits(pm.Groups[1].Value)); } } } static readonly Regex LevelChip = new Regex(@"LEVE\s?[LI1|]\s*([0-9OIlSB|]{1,3})\s*OF\s*([0-9OIlSB|]{1,3})"); static bool TopLevelOk(Match m) { int b; return m.Success && int.TryParse(Parse.Digits(m.Groups[2].Value), out b) && b >= 10; } static readonly Regex LoneOf = new Regex(@"^\s*([0-9OIlS]{1,2})\s*OF\s*([0-9OIlS]{1,2})\s*$"); static readonly Regex Percent = new Regex(@"\+\s*([0-9OIlS]{1,3}?)\s*(?:[0Oo]/[0Oo]|%|/)"); static readonly Regex PercentExact = new Regex(@"\+\s*([0-9]{1,3})\s*%"); static string ReadAgain(Frame f, OcrLine l, int scale, Func ok) { if (ok(l.Text)) return l.Text; int h = Math.Max(6, l.Box.Height); var r = new Rectangle(l.Box.X - h, l.Box.Y - h, l.Box.Width + 2 * h, l.Box.Height + 2 * h); foreach (var t in new[] { Tuple.Create(scale + 1, Prep.None), Tuple.Create(Math.Max(2, scale - 1), Prep.None), Tuple.Create(scale + 2, Prep.None), Tuple.Create(Math.Max(2, scale - 1), Prep.Contrast), Tuple.Create(scale + 3, Prep.WhiteSoft), Tuple.Create(scale, Prep.WhiteText) }) { string text = Ocr.ReadText(f, r, t.Item1, t.Item2); if (ok(text)) return text; } return l.Text; } double rowPitch; string[] SafehousePanel(Frame f, int scale, List lines, Dictionary titles, string title, Rectangle column, string[] rows) { OcrLine t; if (!titles.TryGetValue(title, out t)) return null; int bottom = titles.Values.Where(o => o != t && o.Box.Y > t.Box.Bottom && o.CenterX >= column.Left && o.CenterX < column.Right) .Select(o => o.Box.Y).DefaultIfEmpty(column.Bottom).Min(); var inPanel = lines.Where(l => l != t && l.CenterY > t.Box.Bottom && l.CenterY < bottom && l.CenterX >= column.Left && l.CenterX < column.Right).ToList(); int valueLeft = column.Left + column.Width * 6 / 10; var y = new int[rows.Length]; var found = new bool[rows.Length]; var heights = new List(); for (int i = 0; i < rows.Length; i++) { var label = View.Label(inPanel.Where(l => l.Box.Right < valueLeft || Regex.IsMatch(l.Text, @"\d")).ToList(), rows[i]); if (label == null) continue; y[i] = label.CenterY; found[i] = true; heights.Add(label.Box.Height); } var steps = new List(); for (int i = 0; i < rows.Length; i++) for (int j = i + 1; j < rows.Length; j++) if (found[i] && found[j] && y[j] > y[i]) steps.Add((double)(y[j] - y[i]) / (j - i)); double pitch = steps.Count > 0 ? steps.OrderBy(x => x).ElementAt(steps.Count / 2) : 0; if (pitch > 0 && rowPitch <= 0) rowPitch = pitch; if (pitch <= 0) pitch = rowPitch; int rh = heights.Count > 0 ? heights.OrderBy(x => x).ElementAt(heights.Count / 2) : Math.Max(8, t.Box.Height * 2 / 3); var values = new string[rows.Length]; for (int i = 0; i < rows.Length; i++) { if (!found[i]) { if (pitch <= 0 || !found.Any(x => x)) continue; int k = Enumerable.Range(0, rows.Length).Where(j => found[j]).OrderBy(j => Math.Abs(j - i)).First(); y[i] = y[k] + (int)Math.Round((i - k) * pitch); if (y[i] <= t.Box.Bottom || y[i] >= bottom - rh / 2 || y[i] > column.Bottom - rh) continue; } int band = Math.Max(rh * 8 / 10, (int)(pitch * 0.35)); var value = inPanel.Where(l => l.Box.Right >= valueLeft && Math.Abs(l.CenterY - y[i]) <= band).OrderByDescending(l => l.Box.Right).FirstOrDefault(); if (value != null && RowValueOk(rows[i], value.Text)) { values[i] = value.Text; continue; } if (NumberRow(rows[i])) { int reach = pitch > 0 ? (int)Math.Min(pitch * 0.7, 1.6 * rh) : rh * 6 / 5; var ink = InkBox(f, Rectangle.FromLTRB(valueLeft, y[i] - reach, column.Right - 2, y[i] + reach)); int n = ink.Height >= rh / 2 && ink.Height <= 2 * rh ? ReadLoneNumber(f, ink) : -1; if (n >= 0) { values[i] = n.ToString(); continue; } } values[i] = ReadRowValue(f, scale, rows[i], Rectangle.FromLTRB(column.Left + 2, y[i] - rh, column.Right - 2, y[i] + rh), valueLeft); } return values; } static bool NumberRow(string row) { return row != "PROPERTY INCOME" && row != "HENCHMEN" && row != "EQUIPMENT" && row != "MEMBERS" && row != "RANK"; } static Rectangle InkBox(Frame f, Rectangle r) { int x0 = int.MaxValue, y0 = int.MaxValue, x1 = -1, y1 = -1; r.Intersect(new Rectangle(0, 0, f.Width, f.Height)); for (int y = r.Top; y < r.Bottom; y++) { int first = -1, last = -1, n = 0; for (int x = r.Left; x < r.Right; x++) { var c = f.Pixel(x, y); if ((c.R + c.G + c.B) / 3 < 170 && !(c.R >= 180 && c.G >= 140 && c.B < 120) && !EventOrange(c)) continue; if (first < 0) first = x; last = x; n++; } if (first < 0 || n > r.Width / 2) continue; x0 = Math.Min(x0, first); x1 = Math.Max(x1, last); y0 = Math.Min(y0, y); y1 = Math.Max(y1, y); } return x1 < 0 ? Rectangle.Empty : Rectangle.FromLTRB(x0, y0, x1 + 1, y1 + 1); } internal static int ReadLoneNumber(Frame f, Rectangle ink) { return ReadLoneNumber(f, ink, new[] { 2, 3 }); } internal static int ReadLoneNumber(Frame f, Rectangle ink, int[] tries) { var crop = Rectangle.Inflate(ink, 2, 2); var back = f.Pixel(crop.X, crop.Y); int m = Math.Max(4, ink.Height); foreach (int copies in tries) { var bmp = new Bitmap(2 * m + copies * crop.Width + (copies - 1) * m, crop.Height + 2 * m, System.Drawing.Imaging.PixelFormat.Format32bppArgb); using (var g = Graphics.FromImage(bmp)) { g.Clear(back); for (int k = 0; k < copies; k++) g.DrawImage(f.Bitmap, new Rectangle(m + k * (crop.Width + m), m, crop.Width, crop.Height), crop, GraphicsUnit.Pixel); } using (var copy = new Frame(bmp)) foreach (var t in new[] { Tuple.Create(2, Prep.None), Tuple.Create(3, Prep.None), Tuple.Create(2, Prep.WhiteText), Tuple.Create(2, Prep.Contrast), Tuple.Create(3, Prep.WhiteBright) }) { string text = Ocr.ReadText(copy, new Rectangle(0, 0, copy.Width, copy.Height), t.Item1, t.Item2); if (Regex.IsMatch(text, @"[A-Za-z]{2}") && !Regex.IsMatch(text, @"^[\sOo0]+$")) continue; string d = Parse.Digits(text); if (d.Length == 0 || d.Length % copies != 0) continue; string one = d.Substring(0, d.Length / copies); int n; if (one.Length <= 7 && Enumerable.Range(1, copies - 1).All(k => d.Substring(k * one.Length, one.Length) == one) && int.TryParse(one, out n)) return n; } } return -1; } static bool RowValueOk(string row, string text) { double money; if (row == "PROPERTY INCOME") return Parse.Money(text, out money); if (row == "HENCHMEN" || row == "EQUIPMENT" || row == "MEMBERS") return Regex.IsMatch(text, @"[0-9OoIlS]{1,4}\s*of\s*[0-9OoIlS]{1,4}", RegexOptions.IgnoreCase); if (row == "RANK") return Regex.IsMatch(text.Trim(), @"^[A-Za-z]{3,}$"); return Regex.IsMatch(text.Trim(), @"(^|\s)[0-9OoIlSB][0-9OoIlSB,]*$") && Regex.IsMatch(text, @"[0-9]|^\s*[Oo]\s*$"); } static string ReadRowValue(Frame f, int scale, string row, Rectangle r, int valueLeft) { var strip = Rectangle.FromLTRB(valueLeft, r.Top, r.Right, r.Bottom); foreach (var t in new[] { Tuple.Create(r, scale, Prep.None), Tuple.Create(r, Math.Max(2, scale - 1), Prep.None), Tuple.Create(r, scale + 1, Prep.Contrast), Tuple.Create(strip, 2, Prep.WhiteText), Tuple.Create(strip, 2, Prep.WhiteSoft), Tuple.Create(strip, 3, Prep.WhiteText), Tuple.Create(r, scale + 1, Prep.None), }) { var value = Ocr.Read(f, t.Item1, t.Item2, t.Item3).Where(l => l.Box.Right >= valueLeft).OrderByDescending(l => l.Box.Right).FirstOrDefault(); if (value != null && RowValueOk(row, value.Text)) return value.Text; } return null; } } sealed class NeverPressException : Exception { public NeverPressException(string text, string why) : base("refused to press \"" + (text ?? "").Trim() + "\" (" + why + ")") { } } static class NeverPress { static readonly string[] Anywhere = { "ROBUX", "WITHDRAW", "SABOTAGE", "FINISH NOW", "CANCEL", "REPLACE", "DELETE", "START A HEIST", "BLACK MARKET", "PROPERTIES", "MAKE BOSS", "LEAVE FAMILY", "UNEQUIP", "DISMISS", "AUTO ROLL", "UNLOCK", "RECALL", "DECLARE WAR", "DONATE", "INVITE", "UPGRADE", "TRIBUTE", "GARRISON", "TICKET", "PURCHASE", "REFILL", "GOLD BARS", "GAME PASS", "GAMEPASS", "USE DEFAULT", "RANDOM COLOR", "NOT ELIGIBLE", "PICK A DISTRICT", "RESET ALL", "DEMOLISH", "INCREASE YOUR LUCK", "OFFLINE OPERATION", "RECENT ATTACKS", "LEADERBOARD", "WAR TARGETS ONLY", "ATTACK BACK", "PRESTIGE", "REACH LEVEL", "BREAK SHIELD", "STOP ALL AUTO ROLLS", "AUTO ROLLING", "ROLLING FOR", "FAVORS LEFT", "LEDGER", "GEAR SETS", "BANDAGE", "MEDKIT", "COLD BARS", }; static readonly string[] Word = { "SHIELD", "SHIELDS" }; static readonly string[] WordStart = { "BUY", "SELL", "TRADE", "SHOP", "GIFT", "HIRE", "PAY", "SPEND", "SAVE", "EDIT", "JOIN", "GIVE", "ROLL", "RANKS", "ACCEPT", "SEND", "SETTINGS", "PRODUCTS", "HOURS", "TRAIN", "REWARDS", }; static readonly string[] Whole = { "OPEN", "DEPOSIT", "STOP", "REFRESH", "SEARCH", "AUTO" }; static readonly string[] Allowed = { "GIVE 5" }; static readonly string[] AllowedKeys = Allowed.Select(K).ToArray(); static readonly string[] AnywhereKeys = Anywhere.Select(K).ToArray(); static readonly string[] WordStartKeys = WordStart.Select(K).ToArray(); static readonly string[] WholeKeys = Whole.Select(K).ToArray(); static readonly string[] WordKeys = Word.Select(K).ToArray(); static string K(string s) { return Parse.Key(s).Replace('Q', 'R'); } public static string Why(string text) { if (string.IsNullOrWhiteSpace(text)) return null; string raw = text.Trim(); if (Regex.IsMatch(raw, @"R\s?\$")) return "R$ (Robux)"; if (Regex.IsMatch(raw, @"\$\s?[0-9OoIlS]")) return "a $ price"; if (Regex.IsMatch(raw, @"^[+\-−–—]$")) return "a + or - button"; if (Regex.IsMatch(raw, @"(^|\s)[0-9OoIl]{1,4}\s*KEYS?\b", RegexOptions.IgnoreCase)) return "N KEYS"; string key = K(raw), words = " " + Fold(raw) + " "; if (AllowedKeys.Contains(key)) return null; if (Regex.IsMatch(key, "^ATTACKA[GC6]A[A-Z0-9]?N$")) return null; for (int i = 0; i < AnywhereKeys.Length; i++) if (key.Contains(AnywhereKeys[i]) || AlmostIn(key, AnywhereKeys[i])) return Anywhere[i]; for (int i = 0; i < WordStartKeys.Length; i++) if (words.Contains(" " + WordStartKeys[i])) return WordStart[i]; for (int i = 0; i < WordKeys.Length; i++) if (words.Contains(" " + WordKeys[i] + " ")) return Word[i]; for (int i = 0; i < WholeKeys.Length; i++) if (key == WholeKeys[i]) return Whole[i]; var one = new List { new OcrLine { Text = raw } }; foreach (var t in NeverTabs) if (View.Label(one, View.TabLabels[t]) != null) return View.TabLabels[t]; return null; } static readonly Tab[] NeverTabs = { Tab.Trade, Tab.BlackMarket, Tab.Shop, Tab.Properties }; static bool AlmostIn(string key, string entry) { int off = entry.Length >= 10 ? 2 : 1; if (entry.Length < 7 || key.Length < entry.Length - off) return false; for (int start = 0; start + entry.Length - off <= key.Length; start++) for (int len = entry.Length - off; len <= entry.Length + off && start + len <= key.Length; len++) if (View.Distance(key.Substring(start, len), entry) <= off) return true; return false; } static string Fold(string s) { var words = Regex.Split((s ?? "").ToUpperInvariant(), @"[^\p{L}\p{N}]+").Where(w => w.Length > 0).Select(K); return string.Join(" ", words); } } sealed class SafehouseInfo { public string Estate, Next, Family, Rank; public int Level = -1, MaxLevel = -1, Bonus = -1, NextBonus = -1; public double UpgradeCost = -1, Income = -1; public int Respect = -1, AttackPower = -1, DefensePower = -1; public int Hired = -1, HireMax = -1, Gear = -1, GearMax = -1; public int SkillPoints = -1, Energy = -1, Stamina = -1, Health = -1, BaseAttack = -1, BaseDefense = -1, Capacity = -1; public int Members = -1, MembersMax = -1; public int Prestige = -1; public bool FamilySeen; public bool NoFamily { get { return Family != null && Parse.Key(Family) == "NONEYET"; } } public bool Complete { get { return Level > 0 && Respect >= 0 && AttackPower >= 0 && DefensePower >= 0 && Income >= 0 && Hired >= 0 && Gear >= 0 && SkillPoints >= 0 && Energy >= 0 && Stamina >= 0 && Health >= 0 && BaseAttack >= 0 && BaseDefense >= 0 && Capacity >= 0 && FamilySeen && (Members >= 0 || Family == null || NoFamily); } } public int Found { get { return new[] { Level, Respect, AttackPower, DefensePower, Hired, Gear, SkillPoints, Energy, Stamina, Health, BaseAttack, BaseDefense, Capacity, Members }.Count(v => v >= 0) + (Income >= 0 ? 1 : 0) + (UpgradeCost >= 0 ? 1 : 0); } } static string N(int v) { return v >= 0 ? v.ToString("N0", System.Globalization.CultureInfo.InvariantCulture) : "?"; } static string Of(int a, int b) { return a >= 0 ? a + " of " + b : "?"; } public static string Money(double v) { if (v < 0) return "?"; var ci = System.Globalization.CultureInfo.InvariantCulture; if (v >= 1e12) return "$" + (v / 1e12).ToString("0.##", ci) + "T"; if (v >= 1e9) return "$" + (v / 1e9).ToString("0.##", ci) + "B"; if (v >= 1e6) return "$" + (v / 1e6).ToString("0.##", ci) + "M"; return "$" + v.ToString("N0", ci); } public string[] Lines() { return new[] { "Safehouse: " + (Estate ?? "?") + ", level " + Of(Level, MaxLevel) + (Bonus >= 0 ? " (+" + Bonus + "% income, attack and defense)" : "") + (Next != null || UpgradeCost >= 0 ? ". Next: " + (Next ?? "?") + (NextBonus >= 0 ? " (+" + NextBonus + "%)" : "") + (UpgradeCost >= 0 ? " for " + Money(UpgradeCost) : "") : ""), "Your stats: respect " + N(Respect) + ", attack power " + N(AttackPower) + ", defense power " + N(DefensePower) + ", property income " + Money(Income) + " an hour", "Your crew: " + Of(Hired, HireMax) + " henchmen hired, " + Of(Gear, GearMax) + " equipment slots filled", "Your upgrades: " + N(SkillPoints) + " skill points, energy " + N(Energy) + ", stamina " + N(Stamina) + ", health " + N(Health) + ", base attack " + N(BaseAttack) + ", base defense " + N(BaseDefense) + ", property capacity " + N(Capacity), "Your family: " + (Family ?? (FamilySeen ? "none read" : "?")) + (Rank != null ? ", rank " + Rank : "") + (Members >= 0 ? ", " + Of(Members, MembersMax) + " members" : ""), }; } } }