using System.Collections.Generic;using System.Text.RegularExpressions; enum GameEvent { Cash, EnergyRegen, StaminaRegen, HealthRegen, OpsSpeed, BossDamage, TakedownDamage, Mastery, CrateLuck, RecruitLuck, ShopSale, Exp, Other } sealed partial class Game public sealed class EventBadge public override string ToString() { return Box.X + "," + Box.Y + " " + Box.Width + "x" + Box.Height + " (" + Colour.R + "," + Colour.G + "," + Colour.B + ")"; } public sealed class ActiveEvent public GameEvent Kind = GameEvent.Other; public string Title = "", Text = ""; public int SecondsLeft = -1; public override string ToString() { return (Title.Length > 0 ? Title : Kind.ToString()) + (SecondsLeft >= 0 ? " (ends in " + Parse.Short(SecondsLeft) + ")" : ""); } static bool Saturated(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 >= 110 && max - min >= 50; static bool SameColour(Color a, Color b, int tol) { return Math.Abs(a.R - b.R) <= tol && Math.Abs(a.G - b.G) <= tol && Math.Abs(a.B - b.B) <= tol; } public static List<EventBadge> FindBadges(Frame f) var found = new List<EventBadge>(); int x0 = Math.Max(Math.Max(f.Width * 12 / 100, 180), Math.Min(f.Width * 17 / 100, f.Height * 32 / 100)), x1 = f.Width * 55 / 100, y1 = Math.Min(f.Height, f.Height * 6 / 100 + 4); int minRun = Math.Max(8, f.Width / 110), maxRun = f.Width * 5 / 100 + 10, minH = Math.Max(8, f.Height * 12 / 1000), maxH = f.Height * 45 / 1000 + 2; var runs = new List<Tuple<int, int, int, Color>>(); for (int y = 2; y < y1; y++) for (int x = x0; x < x1; ) if (!Saturated(c)) { x++; continue; } while (x < x1 && Saturated(f.Pixel(x, y)) && SameColour(f.Pixel(x, y), c, 16)) x++; if (x - s >= minRun && x - s <= maxRun) runs.Add(Tuple.Create(y, s, x - 1, c)); foreach (var top in runs) if (found.Any(b => Math.Abs(b.Box.X - top.Item2) <= 2 && top.Item1 >= b.Box.Y && top.Item1 <= b.Box.Bottom)) continue; var bottom = runs.Where(r => r.Item1 >= top.Item1 + minH && r.Item1 <= top.Item1 + maxH && Math.Abs(r.Item2 - top.Item2) <= 2 && Math.Abs(r.Item3 - top.Item3) <= 2 && SameColour(r.Item4, top.Item4, 24)) .OrderByDescending(r => r.Item1).FirstOrDefault(); if (bottom == null) continue; for (int y = top.Item1; y <= bottom.Item1; y++) for (int dx = 0; dx >= -2; dx--) if (SameColour(f.Pixel(Math.Max(0, top.Item2 + dx), y), top.Item4, 30)) { side++; break; } if (side * 10 < n * 7) continue; found.Add(new EventBadge { Box = Rectangle.FromLTRB(top.Item2, top.Item1, top.Item3 + 1, bottom.Item1 + 1), Colour = top.Item4 }); return found.OrderBy(b => b.Box.X).ToList(); public static bool LuckBadge(List<EventBadge> badges) return badges.Any(b => SameColour(b.Colour, Color.FromArgb(110, 155, 220), 16)); public static string BadgeSignature(List<EventBadge> badges) return string.Join(";", badges.Select(b => (b.Colour.R / 32) + "." + (b.Colour.G / 32) + "." + (b.Colour.B / 32) + "@" + b.Box.Width / 4)); static readonly Regex EndsRx = new Regex(@"ENDS\s*[I1l]N\s*(.+)$", RegexOptions.IgnoreCase); public static ActiveEvent ReadTooltip(Frame f, EventBadge b) var frame = TooltipFrame(f, b); var inside = Rectangle.FromLTRB(frame.Left + 3, frame.Top + 3, frame.Right - 3, frame.Bottom - 3); var all = Ocr.Read(f, inside, f.Height < 800 ? 3 : 2, Prep.None).OrderBy(l => l.Box.Y).ThenBy(l => l.Box.X).ToList(); var t = new ActiveEvent { Title = all[0].Text.Trim(), Text = string.Join(" ", all.Select(l => l.Text.Trim())) }; var endsLine = all.FirstOrDefault(l => EndsRx.IsMatch(l.Text)) ?? all.FirstOrDefault(l => Parse.Key(l.Text).StartsWith("ENDSIN")); if (endsLine != null && EndsRx.IsMatch(endsLine.Text)) t.SecondsLeft = Parse.Countdown(EndsRx.Match(endsLine.Text).Groups[1].Value); if (t.SecondsLeft < 0) t.SecondsLeft = TooltipEnds(f, inside, endsLine); t.Kind = EventKind(t.Text); var area = Rectangle.Intersect(new Rectangle(b.Box.X - f.Width / 100, b.Box.Bottom + 1, f.Width * 24 / 100, f.Height * 15 / 100), new Rectangle(0, 0, f.Width, f.Height)); if (area.Width < 20 || area.Height < 20) return null; int scale = f.Height < 800 ? 3 : 2; var lines = Ocr.Read(f, area, scale, Prep.None).OrderBy(l => l.Box.Y).ThenBy(l => l.Box.X).ToList(); var ends = lines.FirstOrDefault(l => EndsRx.IsMatch(l.Text)); var mine = lines.Where(l => ends == null || l.Box.Y <= ends.Box.Y + 2).Where(l => l.Box.X >= b.Box.X - f.Width / 100 - 2).ToList(); if (mine.Count == 0) return null; var e = new ActiveEvent { Title = mine[0].Text.Trim(), Text = string.Join(" ", mine.Select(l => l.Text.Trim())) }; if (ends != null) e.SecondsLeft = Parse.Countdown(EndsRx.Match(ends.Text).Groups[1].Value); e.Kind = EventKind(e.Text); if (ends == null && e.Kind == GameEvent.Other) return null; static Rectangle TooltipFrame(Frame f, EventBadge b) int x = b.Box.X + 2, maxY = Math.Min(f.Height - 2, b.Box.Bottom + 2 * b.Box.Height); for (int y = b.Box.Bottom + 1; y < maxY; y++) if (!SameColour(f.Pixel(x, y), b.Colour, 30)) continue; for (int xx = x, miss = 0; xx < f.Width && miss < 3; xx++) if (SameColour(f.Pixel(xx, y), b.Colour, 30)) { right = xx; miss = 0; } else miss++; if (right - x < 3 * b.Box.Width) continue; int left = x, bottom = y; while (left > 0 && SameColour(f.Pixel(left - 1, y), b.Colour, 30)) left--; while (bottom < f.Height - 1 && SameColour(f.Pixel(left, bottom + 1), b.Colour, 30)) bottom++; if (bottom - y < 2 * b.Box.Height) continue; return Rectangle.FromLTRB(left, y, right + 1, bottom + 1); static int TooltipEnds(Frame f, Rectangle inside, OcrLine endsLine) int h = Math.Max(6, endsLine.Box.Height); row = Rectangle.Intersect(Rectangle.FromLTRB(inside.Left, endsLine.Box.Y - h / 2, inside.Right, endsLine.Box.Bottom + h / 2), inside); else row = Rectangle.FromLTRB(inside.Left, inside.Top + inside.Height * 2 / 3, inside.Right, inside.Bottom); var tries = new[] { Tuple.Create(2, Prep.None), Tuple.Create(3, Prep.None), Tuple.Create(4, Prep.None), Tuple.Create(3, Prep.Contrast), Tuple.Create(2, Prep.GreyText), Tuple.Create(2, Prep.Contrast) }; var words = Ocr.ReadWords(f, row, 3, Prep.None).OrderBy(w => w.Box.X).ToList(); int ends = words.FindIndex(w => Parse.Key(w.Text).StartsWith("ENDS")); var inWord = ends >= 0 ? words.Skip(ends + 1).FirstOrDefault(w => Regex.IsMatch(w.Text, @"^[I1l]N$", RegexOptions.IgnoreCase)) : null; var time = Rectangle.FromLTRB(inWord.Box.Right + 2, row.Top, row.Right, row.Bottom); int s = Parse.Countdown(Ocr.ReadText(f, time, t.Item1, t.Item2).Trim()); var m = EndsRx.Match(Ocr.ReadText(f, row, t.Item1, t.Item2)); int s = m.Success ? Parse.Countdown(m.Groups[1].Value) : -1; if (s < 0 && m.Success) s = LostColon(m.Groups[1].Value); internal static int LostColon(string s) var m = Regex.Match(s ?? "", @"^\s*(\d{1,2})\s*[:.]\s*([0-5]\d)([0-5]\d)\s*$"); return m.Success ? int.Parse(m.Groups[1].Value) * 3600 + int.Parse(m.Groups[2].Value) * 60 + int.Parse(m.Groups[3].Value) : -1; internal static GameEvent EventKind(string text) string k = Parse.Key(text); if (k.Contains("RECRUIT") || k.Contains("RARERHENCHMEN")) return GameEvent.RecruitLuck; if (k.Contains("CRATE") && (k.Contains("IUCK") || k.Contains("RARER"))) return GameEvent.CrateLuck; if (k.Contains("TAKEDOWN") && k.Contains("DAMAGE")) return GameEvent.TakedownDamage; if (k.Contains("BOSS") && k.Contains("DAMAGE")) return GameEvent.BossDamage; if (k.Contains("OPERATION") && (k.Contains("SPEED") || k.Contains("FAST") || k.Contains("TIMER"))) return GameEvent.OpsSpeed; if (k.Contains("MASTERY")) return GameEvent.Mastery; if (Regex.IsMatch(k, @"^[0-9OIS]{0,3}X?EXP")) return GameEvent.Exp; if (k.Contains("SHOP") && (k.Contains("SAIE") || k.Contains("OFF"))) return GameEvent.ShopSale; if (k.Contains("ENERGY") && (k.Contains("REFIII") || k.Contains("REGEN") || k.Contains("FAST"))) return GameEvent.EnergyRegen; if (k.Contains("STAMINA") && (k.Contains("REFIII") || k.Contains("REGEN") || k.Contains("FAST"))) return GameEvent.StaminaRegen; if (k.Contains("HEAITH") && (k.Contains("REFIII") || k.Contains("REGEN") || k.Contains("FAST"))) return GameEvent.HealthRegen; if (k.Contains("CASH") || k.Contains("PAYDOUBIE")) return GameEvent.Cash; public List<ActiveEvent> ReadEvents(List<EventBadge> badges) var events = new List<ActiveEvent>(); foreach (var b in badges) Win.Hover(b.Box.X + b.Box.Width / 2, b.Box.Y + b.Box.Height / 2); for (int look = 0; look < 3 && (e == null || e.Kind == GameEvent.Other || e.SecondsLeft < 0); look++) Wait(look == 0 ? 350 : 300); using (var f = Capture()) var again = ReadTooltip(f, b); if (again != null && (e == null || again.Kind != GameEvent.Other || again.SecondsLeft >= 0)) e = again; if (e != null && e.Kind == GameEvent.Other) using (var f = Capture()) var again = ReadTooltip(f, b); if (again != null && again.Kind != GameEvent.Other) e = again; if (e != null && e.SecondsLeft < 0 && Parse.Key(e.Title).StartsWith(Parse.Key("DAILY BUFF"))) e.SecondsLeft = (int)(DateTime.UtcNow.Date.AddDays(1) - DateTime.UtcNow).TotalSeconds; if (e != null) events.Add(e); for (int tries = 0; tries < 3; tries++) using (var f = Capture()) open = badges.FirstOrDefault(b => TooltipOpen(f, b)); if (tries == 2) { Snapshot("event tooltip left open", 60); break; } Win.Hover(open.Box.X + open.Box.Width / 2, open.Box.Y + open.Box.Height / 2); public static bool TooltipOpen(Frame f, EventBadge b) { return !TooltipFrame(f, b).IsEmpty; } public sealed class BriefcasePage public int ButtonFree = -1; public int NextSeconds = -1; public int Keys = -1, Left = -1; public List<Rectangle> Cases = new List<Rectangle>(); static readonly Regex FreePicksRx = new Regex(@"([0-9]{1,2})\s*FREE\s*PICKS?", RegexOptions.IgnoreCase); static readonly Regex ButtonPicksRx = new Regex(@"([0-9]{1,2})\s*FREE\s*PICKS?"); static readonly Regex NoPickRx = new Regex(@"^\s*FREE\s*PICK\s*[I1l]N\s*(.+)$", RegexOptions.IgnoreCase); static readonly Regex NextPickRx = new Regex(@"\+\s*1\s*[I1l]N\s*([^)]+)", RegexOptions.IgnoreCase); static readonly Regex HaveRx = new Regex(@"have\s*([0-9]{1,2}|no)\s*free\s*pick", RegexOptions.IgnoreCase); static readonly Regex KeysRx = new Regex(@"^\s*([0-9O]{1,3})\s*KEYS?\s*$", RegexOptions.IgnoreCase); static readonly Regex LeftRx = new Regex(@"^\s*([0-9]{1,2})\s*LEFT\s*$", RegexOptions.IgnoreCase); static readonly Regex PaidRx = new Regex(@"Case\s*\S{1,3}\s*paid\s*(.+?)\.?\s*(Added|$)", RegexOptions.IgnoreCase); static bool IsPlate(Color c) { return Math.Abs(c.R - 245) <= 14 && Math.Abs(c.G - 236) <= 14 && Math.Abs(c.B - 215) <= 16; } public BriefcasePage ReadBriefcases(Frame f) var page = new BriefcasePage(); var lines = Ocr.Read(f, area, PageScale(f), Prep.None); bool title = lines.Any(l => Parse.Key(l.Text).Contains("BRIEFCASES") && l.Box.Y < area.Y + area.Height / 4); var board = lines.FirstOrDefault(l => Parse.Key(l.Text).Contains("ONTHISBOARD")); page.Page = title && (board != null || lines.Any(l => Parse.Key(l.Text).Contains("FREEPICK"))); if (!page.Page) return page; var titleLine = lines.Where(l => Parse.Key(l.Text).Contains("BRIEFCASES")).OrderBy(l => l.Box.Y).First(); int th = Math.Max(10, titleLine.Box.Height); var band = Rectangle.Intersect(Rectangle.FromLTRB(area.X + area.Width / 2, titleLine.Box.Y - th, area.Right, titleLine.Box.Bottom + th), area); Func<OcrLine, bool> button = l => (ButtonPicksRx.IsMatch(l.Text) && !Regex.IsMatch(l.Text, @"\bhave\b", RegexOptions.IgnoreCase)) || NoPickRx.IsMatch(l.Text.Trim()); foreach (var t in new[] { Tuple.Create(1, Prep.None), Tuple.Create(2, Prep.Contrast), Tuple.Create(3, Prep.None) }) var more = Ocr.Read(f, band, t.Item1, t.Item2, true); if (more.Any(button)) break; string t = l.Text.Trim(); if ((m = FreePicksRx.Match(t)).Success && page.Free < 0) page.Free = int.Parse(m.Groups[1].Value); if ((m = ButtonPicksRx.Match(t)).Success && page.ButtonFree < 0 && !Regex.IsMatch(t, @"\bhave\b", RegexOptions.IgnoreCase)) page.ButtonFree = int.Parse(m.Groups[1].Value); if ((m = NoPickRx.Match(t)).Success) { page.Free = page.ButtonFree = 0; page.NextSeconds = Parse.Countdown(m.Groups[1].Value); } if ((m = NextPickRx.Match(t)).Success && page.NextSeconds < 0) page.NextSeconds = Parse.Countdown(m.Groups[1].Value); if ((m = HaveRx.Match(t)).Success && page.Free < 0) page.Free = m.Groups[1].Value.ToLowerInvariant() == "no" ? 0 : int.Parse(m.Groups[1].Value); if ((m = KeysRx.Match(t)).Success) page.Keys = int.Parse(m.Groups[1].Value.Replace('O', '0').Replace('o', '0')); if ((m = LeftRx.Match(t)).Success) page.Left = int.Parse(m.Groups[1].Value); if ((m = PaidRx.Match(t)).Success) page.Paid = m.Groups[1].Value.Trim(); int right = board != null ? board.Box.X - board.Box.Height : area.X + area.Width * 2 / 3; int minW = f.Width * 4 / 100, minH = Math.Max(6, (int)(15 * Math.Min(f.Height / 1009.0, f.Width / 1920.0))); var seen = new List<Rectangle>(); for (int y = area.Y + area.Height / 6; y < area.Bottom; y += 2) for (int x = area.X; x < right; ) if (!IsPlate(f.Pixel(x, y))) { x += 2; continue; } while (x < right && IsPlate(f.Pixel(x, y))) x++; if (x - s < minW || seen.Any(r => r.Contains(s + (x - s) / 2, y))) continue; int top = y, bottom = y, cx = s + Math.Max(2, (x - s) / 20); while (top > area.Y && IsPlate(f.Pixel(cx, top - 1))) top--; while (bottom < area.Bottom - 1 && IsPlate(f.Pixel(cx, bottom + 1))) bottom++; var plate = Rectangle.FromLTRB(s, top, x, bottom + 1); if (plate.Height >= minH && plate.Height <= plate.Width) page.Cases.Add(plate); page.Cases = page.Cases.OrderBy(r => r.Y / Math.Max(1, minH * 2)).ThenBy(r => r.X).ToList(); OcrLine EventEntry(Frame f, View v, Func<string, bool> isEntry) var lines = Ocr.Read(f, v.Menu, View.ScaleFor(f, 11, 11.0 * f.Height / 1009), Prep.None); return lines.FirstOrDefault(l => isEntry(Parse.Key(l.Text))); public bool OpenBriefcases() return OpenEventPage(k => k.Contains("BRIEFCASE"), f => ReadBriefcases(f).Page); bool OpenEventPage(Func<string, bool> isEntry, Func<Frame, bool> isPage) bool rolled = false, unfolded = false; for (int attempt = 0; attempt < 4; attempt++) using (var f = Capture()) if (v.Problem != null) { bool cleared = ClearPopupIfAny(f); ForgetView(); if (!cleared) Wait(1000); continue; } if (isPage(f)) return true; entry = EventEntry(f, v, isEntry); if (entry == null && !unfolded && !v.ReadMenu(f)) { ForgetView(); Wait(1000); continue; } if (entry == null && !unfolded && v.EventsBar != null && !v.EventsOpen) Press(v.EventsBar.Text, v.EventsBar.CenterX, v.EventsBar.CenterY, 900); if (rolled) return false; Scroll(v.Menu.X + v.Menu.Width / 2, v.Menu.Y + v.Menu.Height / 2, 10); if (entry.CenterY < ChatZoneBottom) using (var f = Capture()) if (HideChat(f)) { ForgetView(); continue; } Press(entry.Text, entry.CenterX, entry.CenterY, 900); for (int look = 0; look < 3; look++) using (var f = Capture()) if (isPage(f)) return true; using (var f = Capture()) if (v.Problem != null || !v.ReadMenu(f)) { ForgetView(); return false; } if (v.EventsBar == null || !v.EventsOpen) return false; Press(v.EventsBar.Text, v.EventsBar.CenterX, v.EventsBar.CenterY, 700); public string PickBriefcase(BriefcasePage page, out BriefcasePage after) if (page == null || page.Free <= 0 || page.Cases.Count == 0) return null; var plate = page.Cases[0]; Press("briefcase", plate.X + plate.Width / 2, plate.Y + plate.Height / 2, 900); for (int look = 0; look < 4; look++) using (var f = Capture()) if (look == 0) CheckForPurchasePrompt(Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None)); after = ReadBriefcases(f); Snapshot("lucky briefcase picked", 0, true); return after.Paid != null && after.Paid != page.Paid ? after.Paid : ""; Snapshot("lucky briefcase pick not seen", 60); internal static bool Picked(BriefcasePage before, BriefcasePage after) if (before == null || after == null || !after.Page) return false; return (after.Paid != null && after.Paid != before.Paid) || after.Cases.Count < before.Cases.Count || (after.Free >= 0 && before.Free >= 0 && after.Free < before.Free) || (after.Left >= 0 && before.Left >= 0 && after.Left < before.Left); public enum BossState { Unknown, Settled, Upcoming, Live } public sealed class GlobalBossPage public string Status = ""; public int YourHealth = -1, YourHealthMax = -1; public List<string> Received = new List<string>(); static readonly Regex BossTimerRx = new Regex(@"(CLOSES|ENDS|STARTS|BEGINS|OPENS|SPAWNS|ARRIVES|NEXT)\s*[I1l]N\s*:?\s*([^)]+)", RegexOptions.IgnoreCase); static readonly Regex BossNameRx = new Regex(@"(?:DID\s*NOT\s*HIT|YOU\s*HIT)\s+([A-Z][A-Z' .-]{2,30}?)\s*$"); static readonly Regex BossCardHealthRx = new Regex(@"(\d{1,6})?\s*/\s*(\d{2,6})\s*$"); static bool IsGlobalBossEntry(string key) string k = key.Replace('I', 'L').Replace('1', 'L').Replace('|', 'L').Replace('0', 'O'); return k.Contains("GLOBALBOSS") || k.Contains("LOBALBOSS") || k.Contains("GLOBALBO"); public GlobalBossPage ReadGlobalBoss(Frame f) var page = new GlobalBossPage(); var lines = Ocr.Read(f, area, PageScale(f), Prep.None); var title = lines.Where(l => IsGlobalBossEntry(Parse.Key(l.Text)) && l.Box.Y < area.Y + area.Height / 5).OrderBy(l => l.Box.Y).FirstOrDefault(); if (title == null) return page; int th = Math.Max(10, title.Box.Height); var status = lines.Where(l => l != title && Math.Abs(l.CenterY - title.CenterY) < th && l.Box.X > title.Box.X).OrderBy(l => l.Box.X); string rest = Regex.Replace(title.Text, @"^.*?B\s*[O0]\s*S\s*S", "", RegexOptions.IgnoreCase).Trim(); page.Status = (rest + " " + string.Join(" ", status.Select(l => l.Text))).Trim(); string all = Parse.Key(string.Join(" ", lines.Select(l => l.Text))); string st = Parse.Key(page.Status); var m = BossTimerRx.Match(page.Status); if (m.Success) page.Seconds = Parse.Countdown(m.Groups[2].Value); string word = m.Success ? m.Groups[1].Value.ToUpperInvariant() : ""; page.ClaimReady = st.Contains(Parse.Key("READY TO CLAIM")); if (st.Contains(Parse.Key("SETTLED")) || st.Contains(Parse.Key("DEFEATED")) || all.Contains("TAKENDOWN") || all.Contains(Parse.Key("NO MEDALS THIS TIME")) || all.Contains("YOUDIDNOTHIT") || word == "CLOSES") page.State = BossState.Settled; else if (word == "STARTS" || word == "BEGINS" || word == "OPENS" || word == "SPAWNS" || word == "ARRIVES" || word == "NEXT" || st.Contains("UPCOMING") || st.Contains("SOON")) page.State = BossState.Upcoming; page.State = BossState.Live; if (page.State == BossState.Live && word != "ENDS") page.Seconds = -1; var n = BossNameRx.Match(l.Text.Trim()); if (n.Success) { page.Boss = n.Groups[1].Value.Trim(); break; } if (page.State == BossState.Settled) page.Claim = GlobalBossClaimButton(lines, page.ClaimReady); page.Received = GlobalBossReceived(lines); if (page.State != BossState.Live) return page; page.Attack = lines.Where(l => Parse.Key(l.Text) == "ATTACK" && l.CenterY > area.Y + area.Height / 2 && l.CenterX < area.X + area.Width / 2) .OrderBy(l => l.Box.Y).FirstOrDefault(); var you = lines.FirstOrDefault(l => Parse.Key(l.Text).StartsWith("YOU") && Parse.Key(l.Text).Length <= 12 && l.CenterY > area.Y + area.Height / 2 && l.CenterX < area.X + area.Width / 2); int yh = Math.Max(10, you.Box.Height); foreach (var l in lines.Where(x => Math.Abs(x.CenterY - you.CenterY) < yh && x.Box.X >= you.Box.X).OrderBy(x => x.Box.X)) var hm = BossCardHealthRx.Match(Regex.Replace(l.Text, @"^\s*Y\s*O\s*U\s*", "", RegexOptions.IgnoreCase)); if (!hm.Success || !int.TryParse(hm.Groups[2].Value, out mx) || mx <= 0) continue; if (hm.Groups[1].Success && int.TryParse(hm.Groups[1].Value, out v) && v <= mx) page.YourHealth = v; public void GlobalBossAttack(GlobalBossPage page) if (page != null && page.Attack != null && Parse.Key(page.Attack.Text) == "ATTACK") Press(page.Attack, 250); internal static OcrLine GlobalBossClaimButton(List<OcrLine> lines, bool ready) var whole = lines.FirstOrDefault(l => Parse.Key(l.Text) == Parse.Key("CLAIM DROPS")); if (whole != null) return whole; string claim = Parse.Key("CLAIM"), drops = Parse.Key("DROPS"); foreach (var c in lines.Where(l => Parse.Key(l.Text) == claim)) int h = Math.Max(10, c.Box.Height); if (ready || lines.Any(d => Parse.Key(d.Text) == drops && d.Box.Y >= c.Box.Y && d.Box.Y - c.Box.Bottom < 2 * h && Math.Abs(d.CenterX - c.CenterX) < 4 * h)) return c; static readonly Regex BossDropCountRx = new Regex(@"^[xXĂ—]\s*([\dltIOo|]{1,6})$"); internal static List<string> GlobalBossReceived(List<OcrLine> lines) var got = new List<string>(); var head = lines.FirstOrDefault(l => Parse.Key(l.Text).Contains(Parse.Key("YOU RECEIVED"))); if (head == null) return got; int h = Math.Max(10, head.Box.Height); Func<OcrLine, bool> inBox = l => l.Box.Y > head.Box.Bottom + 3 * h && l.Box.Y < head.Box.Bottom + 16 * h && Math.Abs(l.CenterX - head.CenterX) < 12 * h; var counts = lines.Where(l => inBox(l) && BossDropCountRx.IsMatch(l.Text.Trim())).ToList(); var taken = new HashSet<OcrLine>(); foreach (var name in lines.Where(l => inBox(l) && !counts.Contains(l)).OrderBy(l => l.Box.Y / (2 * h)).ThenBy(l => l.Box.X)) if (!taken.Add(name)) continue; string words = name.Text.Trim(); foreach (var more in lines.Where(l => l != name && inBox(l) && !counts.Contains(l) && l.Box.Y > name.Box.Y && l.Box.Y - name.Box.Y < 2 * h && Math.Abs(l.CenterX - name.CenterX) < 2 * h)) if (taken.Add(more)) words += " " + more.Text.Trim(); var c = counts.Where(x => x.Box.Bottom <= name.Box.Y + h && name.Box.Y - x.Box.Bottom < 7 * h && Math.Abs(x.CenterX - name.CenterX) < 5 * h) .OrderBy(x => Math.Abs(x.CenterX - name.CenterX)).FirstOrDefault(); string n = c == null ? "" : Regex.Replace(BossDropCountRx.Match(c.Text.Trim()).Groups[1].Value, "[ltI|]", "1").Replace('O', '0').Replace('o', '0') + " "; public GlobalBossPage ClaimGlobalBossDrops(GlobalBossPage page) if (page == null || page.Claim == null) return null; Press("CLAIM DROPS", page.Claim.CenterX, page.Claim.CenterY, 1500); GlobalBossPage gone = null; for (int look = 0; look < 8; look++) using (var f = Capture()) if (!now.Page) ClearPopupIfAny(f); if (now.Page && (now.Claim != null || now.ClaimReady)) gone = null; if (now.Received.Count > 0) return now; public static readonly string[] StaminaItems = { "Bag of Peanuts", "Corned Beef Sandwich", "Tin Lunch Box" }; public static readonly string[] HealthItems = { "Bandages", "Small Medkit", "Large Medkit" }; public string UseConsumable(bool stamina, out bool none) if (!OpenTab(Tab.Inventory)) return null; using (var f = Capture()) tab = FindInventoryTab(f, "MISC"); if (tab == null) { log("Consumables: couldn't find the MISC tab in the inventory"); Snapshot("inventory without misc tab", 60); return null; } var names = stamina ? StaminaItems : HealthItems; int tabBottom = tab.Box.Bottom + tab.Box.Height; for (int look = 0; look < 5; look++) if (look > 0) { ScrollPage(0.6, -3); Wait(500); } using (var f = Capture()) lines = Ocr.Read(f, area, PageScale(f), Prep.None); int top = Math.Max(area.Top, tabBottom); cards = ItemPictures(f, new Rectangle(area.Left, top, area.Width, area.Bottom - top)); var card = ConsumableCard(lines, cards, names, out name); if (card == null) continue; Press("consumable card", card.Value.X + card.Value.Width / 2, card.Value.Y + card.Value.Height / 2, 900); for (int m = 0; m < 3; m++) using (var f = Capture()) use = ConsumableUse(Ocr.Read(f, PageArea(f), PageScale(f), Prep.None), name, card); if (use == null) use = ConsumableUse(Ocr.Read(f, PageArea(f), PageScale(f) + 1, Prep.None), name, card); Snapshot("consumable menu", 0, true); Snapshot("consumable after use", 0, true); for (int c = 0; c < 4; c++) using (var f = Capture()) yes = ConsumableConfirm(Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None), name, out asked); if (yes != null) { Press(yes, 900); continue; } log("Consumables: the game's \"USE " + name.ToUpperInvariant() + "\" window showed no USE"); Snapshot("consumable confirm without use", 60); using (var f = Capture()) ClearPopupIfAny(f); log("Consumables: " + name + " showed no USE"); Snapshot("consumable without use", 60); internal static bool ItemWorked(int before, int after, int max) if (before < 0 || after < 0) return true; if (max <= 0) return after > before; return after >= max || (after - before) * 10 >= max; internal static Rectangle? ConsumableCard(List<OcrLine> lines, List<Rectangle> cards, string[] names, out string name) if (Parse.Key(CardName(lines, c)).Contains(k)) { name = n; return c; } internal static OcrLine ConsumableUse(List<OcrLine> lines, string name, Rectangle? card = null) string k = Parse.Key(name); foreach (var u in lines.Where(l => Parse.Key(l.Text) == "USE")) int h = Math.Max(10, u.Box.Height); var title = lines.Where(t => t != u && t.Box.Bottom <= u.Box.Y + 2 && u.Box.Y - t.Box.Bottom < 5 * h && Math.Abs(t.CenterX - u.CenterX) < 8 * h) .OrderBy(t => t.Box.Y).ToList(); string all = Parse.Key(string.Join(" ", title.Select(t => t.Text))); if (title.Any(t => Parse.Key(t.Text).Contains(k)) || name.Split(' ').All(w => all.Contains(Parse.Key(w)))) return u; bool beside = u.CenterY > c.Top && u.CenterY < c.Bottom + c.Height / 2 && u.CenterX > c.Left && u.CenterX < c.Right + c.Width * 3 / 2; if (beside && name.Split(' ').Any(w => w.Length >= 5 && all.Contains(Parse.Key(w)))) return u; if (beside && lines.Any(d => Parse.Key(d.Text) == Parse.Key("DELETE") && d.Box.Y > u.Box.Bottom && d.Box.Y - u.Box.Bottom < 3 * h && Math.Abs(d.CenterX - u.CenterX) < 2 * h)) return u; internal static OcrLine ConsumableConfirm(List<OcrLine> lines, string name, out bool asked) string k = Parse.Key(name); string first = Parse.Key(name.Split(' ')[0]); var title = lines.FirstOrDefault(l => { string t = Parse.Key(l.Text); return t.StartsWith("USE") && t.Length > 3 && (t.Contains(k) || t.Substring(3).StartsWith(first)); }); if (title == null) return null; int h = Math.Max(10, title.Box.Height); return lines.Where(u => Parse.Key(u.Text) == "USE" && u.Box.Y > title.Box.Bottom && u.Box.Y - title.Box.Bottom < 12 * h && lines.Any(c => Parse.Key(c.Text) == Parse.Key("CANCEL") && c.Box.X > u.Box.Right && Math.Abs(c.CenterY - u.CenterY) < Math.Max(10, u.Box.Height))) .OrderBy(u => u.Box.Y).FirstOrDefault(); public OcrLine FindInventoryTab(Frame f, string word) var strip = TabStrip(PageArea(f)); string k = Parse.Key(word); var others = new[] { "WEAPONS", "ARMOR", "VEHICLES", "CRATES", "COSMETICS", "TROPHIES" }.Select(Parse.Key).ToList(); var row = Ocr.Read(f, strip, s, Prep.None).Where(l => others.Contains(Parse.Key(l.Text))).ToList(); Func<OcrLine, bool> onRow = l => row.Count == 0 || row.Any(r => Math.Abs(r.CenterY - l.CenterY) < Math.Max(8, r.Box.Height)); Func<List<OcrLine>, OcrLine> top = ls => ls.Where(l => Parse.Key(l.Text) == k && onRow(l)).OrderBy(l => l.Box.Y).FirstOrDefault(); return top(Ocr.Read(f, strip, s, Prep.None)) ?? top(Ocr.Read(f, strip, s, Prep.DarkText)) ?? top(Ocr.ReadWords(f, strip, s, Prep.DarkText)) ?? top(Ocr.ReadWords(f, strip, s, Prep.None)); public bool OpenGlobalBoss() return OpenEventPage(IsGlobalBossEntry, f => ReadGlobalBoss(f).Page);