using System.Collections.Generic;using System.Text.RegularExpressions; public int PlaytimeSecs = -1; public string PlaytimeText = ""; public long Respect = -1; public long FightWins = -1, FightLosses = -1, Defenses = -1, BossesBeaten = -1; public long JobsDone = -1, JobsMastered = -1, OpsDone = -1, Properties = -1; public double IncomePerHour = -1, CashBanked = -1; public long HeistsDone = -1, HeistAssists = -1, HeistSabotages = -1, CleanRuns = -1; public long UpgradeTries = -1, UpgradeWins = -1; public List<string> Lines() var lines = new List<string>(); if (Level > 0 || Respect >= 0 || PlaytimeSecs >= 0) lines.Add("Player: level " + (Level > 0 ? Level.ToString() : "?") + (Respect >= 0 ? ", respect " + Respect.ToString("N0") : "") + (PlaytimeSecs >= 0 ? ", playtime " + PlaytimeText : "")); if (FightWins >= 0 || FightLosses >= 0 || Defenses >= 0 || BossesBeaten >= 0) lines.Add("Combat: " + FightWins + " fight wins" + (FightLosses >= 0 ? ", " + FightLosses + " fight losses" : "") + ", " + Defenses + " defenses, " + BossesBeaten + " bosses beaten"); if (JobsDone >= 0 || OpsDone >= 0 || Properties >= 0) lines.Add("Empire: " + JobsDone + " jobs (" + JobsMastered + " mastered), " + OpsDone + " operations, " + Properties + " properties" + (IncomePerHour >= 0 ? ", " + SafehouseInfo.Money(IncomePerHour) + "/hr" : "") + (CashBanked >= 0 ? ", " + SafehouseInfo.Money(CashBanked) + " banked" : "")); if (HeistsDone >= 0 || HeistAssists >= 0) lines.Add("Heists: " + HeistsDone + " done, " + HeistAssists + " assists, " + HeistSabotages + " sabotages, " + CleanRuns + " clean runs"); lines.Add("Upgrades: " + UpgradeWins + " of " + UpgradeTries + " tries worked"); sealed partial class Game public PlayerStats ReadPlayerStats() var s = new PlayerStats(); for (int attempt = 0; attempt < 3; attempt++) bool slow = attempt > 0 || DateTime.UtcNow < slowUntil; using (var f = Capture()) if (StatsTitle(f) != null) break; if (DropdownLines(f) != null) { CloseMenu(f); continue; } var at = FindMenuButton(f); if (at.IsEmpty) { CloseTerritoryToasts(); Wait(1500); continue; } if (slow) { Check(); NoteLateFrame(); Win.SlowClick(at.X, at.Y); Wait(1200 + (FrameLag() > 0 ? 800 : 0)); } else Click(at.X, at.Y, 900); using (var f = Capture()) var stats = DropdownStats(f); if (stats == null) continue; if (slow) PressSlow(stats.Text, stats.CenterX, stats.CenterY, 1200); using (var f = Capture()) title = StatsTitle(f); NoteFailure("the STATS panel didn't open"); Snapshot("stats not opened", 60); var seen = new HashSet<string>(); for (int pass = 0; pass < 5; pass++) using (var f = Capture()) ReadStatsRows(f, s, seen, title); if (s.Found == before) break; using (var f = Capture()) var p = v != null && v.Problem == null ? v.PagePoint(0.5, 0.5) : new Point(f.Width / 2, f.Height / 2); for (int attempt = 0; attempt < 3; attempt++) using (var f = Capture()) if (title == null) break; var close = StatsClose(f, title); if (close.IsEmpty && attempt < 2 && CloseTerritoryToasts()) { Wait(1200); continue; } if (close.IsEmpty) { Snapshot("stats no close mark", 60); break; } Click(close.X, close.Y, 900); using (var f = Capture()) if (StatsTitle(f) != null) { NoteFailure("the STATS panel didn't close"); Snapshot("stats not closed", 60); } if (s.Found > 0) s.At = DateTime.UtcNow; double hs = v != null && v.Problem == null ? v.HeaderScale : Math.Max(0.6, f.Height / 1009.0); var at = FindMenuButton(f); if (!at.IsEmpty) Click(at.X, at.Y, 700); internal static bool MenuOpen(Frame f) { return DropdownLines(f) != null; } internal bool CloseOpenMenu(Frame f) if (!MenuOpen(f)) return false; var at = FindMenuButton(f); if (at.IsEmpty) return false; log("Closing the game's menu - it lay over the page"); static List<OcrLine> DropdownLines(Frame f) var area = new Rectangle(f.Width / 2, 0, f.Width - f.Width / 2, f.Height); int scale = Math.Max(2, Math.Min(4, (int)Math.Ceiling(1100.0 / Math.Max(300, area.Height / 4)))); var lines = Ocr.Read(f, area, scale, Prep.None); var want = new[] { "MAIL", "SETTINGS", "PLAYERS", "STATS", "UPDATES" }; var found = lines.Where(l => want.Any(w => Parse.Key(l.Text) == Parse.Key(w))).ToList(); return found.Count >= 3 ? found : null; static OcrLine DropdownStats(Frame f) var lines = DropdownLines(f); if (lines == null) return null; return lines.FirstOrDefault(l => Parse.Key(l.Text) == Parse.Key("STATS")) ?? lines.FirstOrDefault(l => Parse.Key(l.Text).Contains(Parse.Key("STAT"))); static OcrLine StatsTitle(Frame f) int scale = Math.Max(2, Math.Min(4, (int)Math.Ceiling(1000.0 / Math.Max(300, f.Height / 3)))); var lines = Ocr.Read(f, new Rectangle(0, 0, f.Width, f.Height), scale, Prep.None); var upgrades = lines.Where(l => Parse.Has(l.Text, "YOUR UPGRADES")).ToList(); return lines.OrderByDescending(l => l.Box.Height).FirstOrDefault(l => Parse.Has(l.Text, "YOUR STATS") && !upgrades.Any(u => Math.Abs(u.CenterY - l.CenterY) <= Math.Max(8, l.Box.Height) && u.Box.X > l.Box.Right)); internal Point FindMenuButton(Frame f) double hs = v != null && v.Problem == null ? v.HeaderScale : Math.Max(0.6, f.Height / 1009.0); int top = 0, bottom = v != null && v.Problem == null ? v.Header.Bottom : (int)(100 * hs); int x0 = Math.Max(0, f.Width - (int)(100 * hs)), x1 = f.Width - 2; if (bottom <= top + 4 || x1 <= x0 + 4) return Point.Empty; var glyph = MenuGlyph(f, top, bottom, hs); static Point MenuGlyph(Frame f, int top, int bottom, double hs) int x0 = Math.Max(0, f.Width - (int)(110 * hs)), x1 = f.Width - 2; int minRun = Math.Max(6, (int)(14 * hs)), maxRun = Math.Max(minRun + 4, (int)(40 * hs)); var bands = new List<int[]>(); for (int y = Math.Max(0, top); y < Math.Min(bottom, f.Height); y++) int best = 0, bestAt = -1, run = 0; for (int x = x0; x <= x1; x++) bool grey = c.R >= 150 && c.G >= 150 && c.B >= 150 && Math.Max(c.R, Math.Max(c.G, c.B)) - Math.Min(c.R, Math.Min(c.G, c.B)) <= 40; bool gold = c.R >= 160 && c.G >= 120 && c.B <= 130 && c.R - c.B >= 70 && c.R >= c.G || EventOrange(c); if (grey || gold) { if (++run > best) { best = run; bestAt = x; } } bool bar = best >= minRun && best <= maxRun; var last = bands.Count > 0 ? bands[bands.Count - 1] : null; if (bar && last != null && last[1] == y - 1 && Math.Abs(last[3] - bestAt) <= 3) last[1] = y; else if (bar) bands.Add(new[] { y, y, bestAt - best + 1, bestAt }); int maxThick = Math.Max(3, (int)(9 * hs)); var bars = bands.Where(b => b[1] - b[0] + 1 <= maxThick).ToList(); for (int i = 0; i + 2 < bars.Count; i++) var row = bars.Skip(i).Where(b => Math.Abs(b[2] - a[2]) <= 3 && Math.Abs(b[3] - a[3]) <= 3).ToList(); if (row.Count < 3) continue; var m = row[1]; var z = row[2]; int g1 = (m[0] + m[1]) / 2 - (a[0] + a[1]) / 2, g2 = (z[0] + z[1]) / 2 - (m[0] + m[1]) / 2; if (g1 < Math.Max(4, (int)(6 * hs)) || g1 > (int)(15 * hs) + 2 || Math.Abs(g1 - g2) > 2) continue; int t1 = a[1] - a[0], t2 = m[1] - m[0], t3 = z[1] - z[0]; if (Math.Max(t1, Math.Max(t2, t3)) - Math.Min(t1, Math.Min(t2, t3)) > Math.Max(2, (int)hs)) continue; if (!DarkBetween(f, a, m) || !DarkBetween(f, m, z)) continue; if (Math.Abs(a[2] - m[2]) > 3 || Math.Abs(m[2] - z[2]) > 3 || Math.Abs(a[3] - z[3]) > 3) continue; return new Point((m[2] + m[3]) / 2, (m[0] + m[1]) / 2); static bool DarkBetween(Frame f, int[] upper, int[] lower) for (int y = upper[1] + 1; y < lower[0]; y++) for (int x = upper[2]; x <= upper[3]; x++) bool grey = c.R >= 150 && c.G >= 150 && c.B >= 150 && Math.Max(c.R, Math.Max(c.G, c.B)) - Math.Min(c.R, Math.Min(c.G, c.B)) <= 40; bool gold = c.R >= 160 && c.G >= 120 && c.B <= 130 && c.R - c.B >= 70 && c.R >= c.G || EventOrange(c); if (!grey && !gold) dark++; return all > 0 && dark * 4 >= all * 3; static Point StatsClose(Frame f, OcrLine title) int th = Math.Max(8, title.Box.Height); int x0 = title.Box.Right + 2 * th, y0 = Math.Max(0, title.Box.Y - th), y1 = Math.Min(f.Height, title.Box.Bottom + th); if (Bridge.On) { y0 = Math.Max(0, title.CenterY - th * 4 / 5); y1 = Math.Min(f.Height, title.CenterY + th * 4 / 5); } Func<Color, bool> light = c => c.R >= 130 && c.G >= 130 && c.B >= 130 && Math.Max(c.R, Math.Max(c.G, c.B)) - Math.Min(c.R, Math.Min(c.G, c.B)) <= 34; int minX = -1, maxX = -1; for (int x = Math.Max(x0, f.Width / 2); x < f.Width; x++) for (int y = y0; y < y1 && !any; y++) any = light(f.Pixel(x, y)); if (any) { if (minX < 0) minX = x; maxX = x; } else if (minX >= 0 && x - maxX > th / 2) if (maxX - minX < th / 3) { minX = maxX = -1; continue; } if (maxX < 0 || maxX - minX > 4 * th || maxX - minX < th / 3) return Point.Empty; int minY = int.MaxValue, maxY = -1; for (int y = y0; y < y1; y++) for (int x = minX; x <= maxX; x++) if (light(f.Pixel(x, y))) { minY = Math.Min(minY, y); maxY = Math.Max(maxY, y); } if (maxY < 0) return Point.Empty; return new Point((minX + maxX) / 2, (minY + maxY) / 2); static readonly Tuple<string, string>[] StatLabels = Tuple.Create("Level", "int"), Tuple.Create("Total playtime", "time"), Tuple.Create("Current respect", "int"), Tuple.Create("Fight wins", "int"), Tuple.Create("Fight losses", "int"), Tuple.Create("Successful defenses", "int"), Tuple.Create("Bosses defeated", "int"), Tuple.Create("Jobs completed", "int"), Tuple.Create("Jobs fully mastered", "int"), Tuple.Create("Operations completed", "int"), Tuple.Create("Properties owned", "int"), Tuple.Create("Property income per hour", "money"), Tuple.Create("Total cash banked", "money"), Tuple.Create("Heists completed", "int"), Tuple.Create("Heist assists", "int"), Tuple.Create("Heist sabotages", "int"), Tuple.Create("Clean runs", "int"), Tuple.Create("Upgrade attempts", "int"), Tuple.Create("Successful upgrades", "int"), void ReadStatsRows(Frame f, PlayerStats s, HashSet<string> seen, OcrLine title) int th = Math.Max(8, title.Box.Height); var close = StatsClose(f, title); int left = Math.Max(0, title.Box.X - 2 * th), edge = close.IsEmpty ? f.Width : Math.Min(f.Width, close.X + th); var area = new Rectangle(left, title.Box.Y, edge - left, f.Height - title.Box.Y); var lines = new List<OcrLine>(); foreach (var t in new[] { Tuple.Create(2, Prep.None), Tuple.Create(3, Prep.None), Tuple.Create(2, Prep.WhiteText) }) lines.AddRange(Ocr.Read(f, area, t.Item1, t.Item2)); foreach (var kv in StatLabels) string key = Parse.Key(kv.Item1); if (!seen.Add(key)) continue; var label = lines.Where(l => l.Box.X < left + (edge - left) * 62 / 100) .FirstOrDefault(l => Parse.Key(l.Text) == key || Parse.Key(l.Text).StartsWith(key) || (key.Length > 8 && Parse.Key(l.Text).Contains(key))); if (label == null) { seen.Remove(key); continue; } int h = Math.Max(8, label.Box.Height); var right = lines.Where(l => l != label && Math.Abs(l.CenterY - label.CenterY) <= h && l.Box.X > label.Box.X) .OrderByDescending(l => l.CenterX).FirstOrDefault(); if (right != null) row += " " + right.Text; if (SetStat(s, kv.Item1, kv.Item2, row)) s.Found++; static bool SetStat(PlayerStats s, string label, string type, string row) if (!Parse.Money(row, out v)) return false; if (label == "Property income per hour") s.IncomePerHour = v; int secs = PlaytimeSecs(row); if (secs < 0) return false; s.PlaytimeText = ShortPlaytime(secs); var m = Regex.Match(row, @"([0-9][0-9,\.]*)\s*[^0-9]*$"); if (!m.Success) m = Regex.Match(row, @"([0-9][0-9,\.]*)"); if (!m.Success) return false; if (!long.TryParse(m.Groups[1].Value.Replace(",", "").Replace(".", ""), out n)) return false; case "Level": if (n <= 0 || n > 10000) return false; s.Level = (int)n; return true; case "Current respect": s.Respect = n; return true; case "Fight wins": s.FightWins = n; return true; case "Fight losses": s.FightLosses = n; return true; case "Successful defenses": s.Defenses = n; return true; case "Bosses defeated": s.BossesBeaten = n; return true; case "Jobs completed": s.JobsDone = n; return true; case "Jobs fully mastered": s.JobsMastered = n; return true; case "Operations completed": s.OpsDone = n; return true; case "Properties owned": s.Properties = n; return true; case "Heists completed": s.HeistsDone = n; return true; case "Heist assists": s.HeistAssists = n; return true; case "Heist sabotages": s.HeistSabotages = n; return true; case "Clean runs": s.CleanRuns = n; return true; case "Upgrade attempts": s.UpgradeTries = n; return true; case "Successful upgrades": s.UpgradeWins = n; return true; static int PlaytimeSecs(string row) foreach (Match m in Regex.Matches(row ?? "", @"([0-9OoIlS]{1,4})\s*([dhmsDHMS])\b")) if (!int.TryParse(Parse.Digits(m.Groups[1].Value), out n)) continue; switch (m.Groups[2].Value.ToLowerInvariant()) case "d": total += n * 86400; break; case "h": total += n * 3600; break; case "m": total += n * 60; break; default: total += n; break; static string ShortPlaytime(int secs) int d = secs / 86400, h = secs % 86400 / 3600, m = secs % 3600 / 60; return (d > 0 ? d + "d " : "") + (h > 0 || d > 0 ? h + "h " : "") + m + "m";