using System.Collections.Generic;using System.Text.RegularExpressions; sealed partial class Game internal Point TerritoryToastX(Frame f) { string text; return TerritoryToastX(f, out text); } internal Point TerritoryToastX(Frame f, out string text) { Rectangle box; return TerritoryToastX(f, out text, out box); } internal Point TerritoryToastX(Frame f, out string text, out Rectangle box) { return TerritoryToastX(f, out text, out box, IsAttackWords); } static bool IsAttackWords(string key) { return key.Contains("TTACKED") || key.Contains("GARRISON"); } internal string ShipmentToast(Frame f, out Point x) x = TerritoryToastX(f, out text, out box, IsShipmentWords); if (!x.IsEmpty && text != null) return text.Trim(); string toast = ToastIn(f); if (toast == null || !IsShipmentWords(Parse.Key(toast))) return null; if (v.Problem != null) return toast.Trim(); double row = 34.5 * v.HeaderScale; int left = ToastProbeX(v) - 8, top = Math.Max(0, (int)(v.Energy.CenterY - row / 2) - (int)row); var area = Rectangle.Intersect(new Rectangle(left, top, v.Width - left, (int)(3.5 * row)), new Rectangle(0, 0, f.Width, f.Height)); if (area.Width < 20 || area.Height < 8) return toast.Trim(); var lines = Ocr.Read(f, area, View.ScaleFor(20 * v.HeaderScale) + 1, Prep.None).OrderBy(l => l.Box.Y).ThenBy(l => l.Box.X).ToList(); var title = lines.FirstOrDefault(l => IsShipmentWords(Parse.Key(l.Text))); if (title == null) return toast.Trim(); string all = string.Join(" / ", lines.Where(l => l.Box.Y >= title.Box.Y - 2 && l.Box.X >= title.Box.X - 3 * title.Box.Height).Select(l => l.Text.Trim()) .Where(t => t.Length > 1)); return all.Length > toast.Trim().Length ? all : toast.Trim(); internal static bool IsShipmentWords(string key) { return key.Contains("SHIPMENT") && !key.Contains("FENCE") && !key.Contains("HOTSHIPMENT"); } internal Point TerritoryToastX(Frame f, out string text, out Rectangle box, Func<string, bool> accept) text = null; box = Rectangle.Empty; if (v.Problem == null) return TerritoryToastAt(f, v.Energy.CenterY, 34.5 * v.HeaderScale, v.ProbeX, out text, out box, accept); double hs = Math.Max(0.6, f.Height / 1009.0), rowH = 34.5 * hs; var tried = new List<Rectangle>(); for (int y = (int)(8 * hs); y < Math.Min(f.Height, (int)(230 * hs)); y += Math.Max(2, (int)(6 * hs))) var x = TerritoryToastAt(f, y, rowH, f.Width / 2, out text, out box, accept, tried); if (!x.IsEmpty) return x; text = null; box = Rectangle.Empty; Point TerritoryToastAt(Frame f, int y, double row, int probeX, out string text, out Rectangle box, Func<string, bool> accept, List<Rectangle> tried = null) text = null; box = Rectangle.Empty; for (int x = f.Width - 1; x > probeX; x--) if (IsToastFrame(f.Pixel(x, y))) { right = x; break; } if (right < 0 || f.Width - right > 2 * row) return Point.Empty; int inX = right - (int)row, top = y, bottom = y; if (IsToastGold(f.Pixel(inX, y))) return FilledToastAt(f, y, row, inX, right, out text, out box, accept, tried); Func<int, bool> edge = yy => IsToastFrame(f.Pixel(inX, yy)) && IsToastFrame(f.Pixel(inX - 6, yy)) && IsToastFrame(f.Pixel(Math.Min(f.Width - 1, inX + 6), yy)); while (top > 0 && y - top < 4 * row && !edge(top)) top--; while (bottom < f.Height - 1 && bottom - y < 4 * row && !edge(bottom)) bottom++; if (bottom - top < 0.8 * row || bottom - top >= 4 * row) return Point.Empty; for (int dy = 0; dy <= 2; dy++) int x = inX, miss = 0, end = x; for (; x > 0 && miss <= 2; x--) if (IsToastFrame(f.Pixel(x, top + dy))) { end = x; miss = 0; } else miss++; left = Math.Min(left, end); if (right - left < 6 * row) return Point.Empty; var inside = Rectangle.FromLTRB(left + 3, top + 3, right - 2, bottom - 2); if (tried != null && tried.Contains(inside)) return Point.Empty; string words = Ocr.ReadText(f, inside, View.ScaleFor(20 * row / 34.5), Prep.None), k = Parse.Key(words); if (!accept(k)) { if (tried != null) tried.Add(inside); return Point.Empty; } var mark = CloseMarkAt(f, left, right - 2, top + 3, Math.Min(bottom - 2, top + (int)(1.2 * row)), row, c => Math.Min(c.R, Math.Min(c.G, c.B)) >= 180 || Math.Min(c.R, Math.Min(c.G, c.B)) >= 140 && Math.Max(c.R, Math.Max(c.G, c.B)) - Math.Min(c.R, Math.Min(c.G, c.B)) <= 40); if (mark.IsEmpty) return Point.Empty; box = Rectangle.FromLTRB(left, top, right, bottom); static Point CloseMarkAt(Frame f, int left, int right, int top, int bottom, double row, Func<Color, bool> ink) int from = Math.Max(Math.Max(0, left), right - (int)(2.3 * row)); right = Math.Min(right, f.Width); bottom = Math.Min(bottom, f.Height); if (from >= right || top >= bottom) return Point.Empty; Func<int, bool> inked = x => { for (int yy = top; yy < bottom; yy++) if (ink(f.Pixel(x, yy))) return true; return false; }; for (int x = right - 1; x >= from; x--) if (inked(x)) { x1 = x; break; } if (x1 < 0) return Point.Empty; for (int x = x1 - 1; x >= from && gap <= 1; x--) if (inked(x)) { x0 = x; gap = 0; } else gap++; int y0 = int.MaxValue, y1 = -1; for (int yy = top; yy < bottom; yy++) for (int x = x0; x <= x1; x++) if (ink(f.Pixel(x, yy))) { y0 = Math.Min(y0, yy); y1 = Math.Max(y1, yy); } if (y1 < 0 || x1 - x0 > row || y1 - y0 > row) return Point.Empty; return new Point((x0 + x1) / 2, (y0 + y1) / 2); static bool IsToastFrame(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)); || c.R >= 180 && c.G >= 70 && c.G <= 120 && c.B >= 60 && c.B <= 110 && c.R - c.G >= 80 || c.R >= 120 && c.G >= 90 && c.B <= 95 && c.R - c.B >= 50 && c.R - c.G >= 15 && c.R - c.G <= 60 || c.G >= 85 && c.R <= 170 && c.G - c.R >= 10 && c.G - c.B >= 15 || min >= 100 && max <= 175 && max - min <= 16 || Math.Abs(c.R - 162) <= 14 && Math.Abs(c.G - 152) <= 14 && Math.Abs(c.B - 178) <= 14; Point FilledToastAt(Frame f, int y, double row, int inX, int right, out string text, out Rectangle box, Func<string, bool> accept, List<Rectangle> tried) text = null; box = Rectangle.Empty; while (top > 0 && y - top < 3 * row && IsToastGold(f.Pixel(inX, top - 1))) top--; while (bottom < f.Height - 1 && bottom - y < 3 * row && IsToastGold(f.Pixel(inX, bottom + 1))) bottom++; if (bottom - top < 0.8 * row || bottom - top >= 3 * row) return Point.Empty; while (right + 1 < f.Width && IsToastGold(f.Pixel(right + 1, ry))) right++; int left = inX, miss = 0; for (int x = inX; x > 0 && miss <= 2; x--) if (IsToastGold(f.Pixel(x, ry))) { left = x; miss = 0; } else miss++; if (right - left < 6 * row) return Point.Empty; var inside = Rectangle.FromLTRB(left + 2, top + 2, right - 2, bottom - 2); if (tried != null && tried.Contains(inside)) return Point.Empty; string words = Ocr.ReadText(f, inside, View.ScaleFor(20 * row / 34.5), Prep.None), k = Parse.Key(words); if (!accept(k)) { if (tried != null) tried.Add(inside); return Point.Empty; } var mark = CloseMarkAt(f, left, right - 1, top + 2, Math.Min(bottom - 1, top + (int)(1.2 * row)), row, c => Math.Max(c.R, Math.Max(c.G, c.B)) <= 80); if (mark.IsEmpty) return Point.Empty; box = Rectangle.FromLTRB(left, top, right, bottom); public bool OpenDistrictFromToast(string name) using (var f = Capture()) if (TerritoryToastX(f, out text, out box).IsEmpty) return false; Click(box.Left + box.Width / 3, box.Top + box.Height / 2, 900); for (int look = 0; look < 8; look++) using (var f = Capture()) var w = ReadDistrictWindow(f); if (w != null && w.Is(name)) return true; if (w != null && look >= 1 && (w.Title.Length > 0 || w.TitleAgain.Length > 0)) { CloseDistrict(w); return false; } internal Point RulingFamilyX(Frame f, out string text) { return RulingFamilyX(f, IsRulingWords, out text); } static bool IsRulingWords(string key) { return key.Contains(Parse.Key("RULING FAMILY")) || key.Contains(Parse.Key("CHANGED HANDS")) || key.Contains(Parse.Key("RULES THE CITY")); } internal Point RulingFamilyX(Frame f, Func<string, bool> isTitle, out string text) double row = 34.5 * (v.Problem == null ? v.HeaderScale : f.Height / 989.0); var area = Rectangle.Intersect(new Rectangle(f.Width / 5, 0, f.Width * 3 / 5, f.Height / 3), new Rectangle(0, 0, f.Width, f.Height)); var lines = Ocr.Read(f, area, PopupScale(f), Prep.None); var title = lines.FirstOrDefault(l => isTitle(Parse.Key(l.Text))); if (title == null) return Point.Empty; Func<Color, bool> gold = c => IsToastGold(c) || EventOrange(c) || c.R >= 150 && c.G >= 120 && c.B <= 130 && c.R - c.B >= 60 && c.R - c.G <= 60; int y = title.CenterY, right = -1, half = (int)(row / 2); for (int x = title.Box.Right; x < f.Width - 1 && x - title.Box.Right < 20 * row; x++) if (y - half >= 0 && y + half < f.Height && gold(f.Pixel(x, y)) && gold(f.Pixel(x, y - half)) && gold(f.Pixel(x, y + half))) { right = x; break; } if (right < 0) return Point.Empty; int inX = right - (int)row, top = y; while (top > 0 && y - top < 3 * row && !gold(f.Pixel(inX, top))) top--; if (y - top >= 3 * row) return Point.Empty; while (bottom < f.Height - 1 && bottom - y < 4 * row && !gold(f.Pixel(inX, bottom))) bottom++; int x0 = int.MaxValue, x1 = -1, y0 = int.MaxValue, y1 = -1; for (int yy = top + 3; yy < Math.Min(f.Height, top + (int)(1.2 * row)); yy++) for (int x = right - (int)(1.2 * row); x < right - 2; x++) if (Math.Min(c.R, Math.Min(c.G, c.B)) < 130 || Math.Max(c.R, Math.Max(c.G, c.B)) - Math.Min(c.R, Math.Min(c.G, c.B)) > 40) continue; x0 = Math.Min(x0, x); x1 = Math.Max(x1, x); y0 = Math.Min(y0, yy); y1 = Math.Max(y1, yy); if (x1 < 0 || x1 - x0 > row || y1 - y0 > row) return Point.Empty; text = string.Join(" ", lines.Where(l => l.Box.Y >= top && l.Box.Bottom <= bottom && l.Box.Right <= right && l.Box.X >= right - 20 * row).OrderBy(l => l.Box.Y).ThenBy(l => l.Box.X).Select(l => l.Text.Trim())); return new Point((x0 + x1) / 2, (y0 + y1) / 2); public bool CloseRulingFamily(Frame f) var x = RulingFamilyX(f, out text); if (x.IsEmpty) return false; log("Closing the game's \"ruling family has changed hands\" window with its x (\"" + text + "\")"); public void CloseShipmentToast(Point x) log("Closing the game's shipment notification with its x - it covered the bars"); internal Point AnyToastX(Frame f, out string text) { Rectangle box; return TerritoryToastX(f, out text, out box, k => k.Length > 0); } internal static Point GreenToastX(Frame f) { return FilledToastX(f, IsToastGreen); } internal static Point RedToastX(Frame f) { return FilledToastX(f, IsToastRed); } internal static Point OrangeToastX(Frame f) { return FilledToastX(f, IsToastOrange); } static Point FilledToastX(Frame f, Func<Color, bool> fill) double hs = Math.Max(0.6, f.Height / 1009.0), row = 34.5 * hs; int xs = Math.Max(0, f.Width - (int)(2.5 * row)), yEnd = Math.Min(f.Height - 1, (int)(260 * hs)); int left = (int)(0.6 * row), right = (int)(0.35 * row); for (int y = 1; y < yEnd; y++) for (int x = xs; x < f.Width - right - 1; x++) if (!IsToastWhite(f.Pixel(x, y)) || !fill(f.Pixel(x - left, y)) || !fill(f.Pixel(x + right, y))) continue; int x0 = x, x1 = x, y0 = y, y1 = y; for (int yy = y; yy < Math.Min(f.Height, y + (int)row); yy++) for (int xx = Math.Max(0, x - (int)(row / 2)); xx < Math.Min(f.Width, x + (int)(row / 2)); xx++) if (IsToastWhite(f.Pixel(xx, yy))) { x0 = Math.Min(x0, xx); x1 = Math.Max(x1, xx); y1 = Math.Max(y1, yy); } if (x1 - x0 < 3 || y1 - y0 < 3 || x1 - x0 > row / 2 || y1 - y0 > row / 2) continue; if (!fill(f.Pixel(Math.Max(0, x0 - 3), (y0 + y1) / 2)) || !fill(f.Pixel(Math.Min(f.Width - 1, x1 + 3), (y0 + y1) / 2))) continue; return new Point((x0 + x1) / 2, (y0 + y1) / 2); static bool IsToastGreen(Color c) { return c.G >= 75 && c.G <= 140 && c.G - c.R >= 30 && c.G - c.B >= 30 && c.R < 80; } static bool IsToastRed(Color c) { return c.R >= 150 && c.R <= 220 && c.G >= 50 && c.G <= 100 && c.B >= 40 && c.B <= 95 && c.R - c.G >= 80; } static bool IsToastOrange(Color c) { return c.R >= 150 && c.R <= 215 && c.G >= 80 && c.G <= 130 && c.B <= 60 && c.R - c.G >= 50 && c.G - c.B >= 40; } static bool IsToastWhite(Color c) { return Math.Min(c.R, Math.Min(c.G, c.B)) >= 200; } static string Short(string words) words = Regex.Replace(Regex.Replace(words ?? "", @"\s+", " ").Trim(), @"^[xX×]\s+|\s+[xX×]$", ""); return words.Length > 60 ? words.Substring(0, 60) + "..." : words; readonly List<KeyValuePair<DateTime, string>> toastsClosed = new List<KeyValuePair<DateTime, string>>(); void NoteToastClosed(string text) toastsClosed.Add(new KeyValuePair<DateTime, string>(DateTime.UtcNow, text ?? "")); if (toastsClosed.Count > 10) toastsClosed.RemoveAt(0); public bool ToastClosedSince(DateTime sinceUtc, string phrase) { return toastsClosed.Any(t => t.Key >= sinceUtc && Parse.Has(t.Value, phrase)); } public bool CloseTerritoryToast(Frame f) var x = AnyToastX(f, out text); if (x.IsEmpty) { x = GreenToastX(f); text = "a green one"; } if (x.IsEmpty) { x = RedToastX(f); text = "a red one"; } if (x.IsEmpty) { x = OrangeToastX(f); text = "an orange one"; } if (x.IsEmpty || !ToastStillOver(x)) return false; log("Closing the game's notification (\"" + Short(text) + "\") with its x - it covered the bars"); bool ToastStillOver(Point x) using (var f = Capture()) return !OnMenuButton(x, FindMenuButton(f), f.Height); internal static bool OnMenuButton(Point x, Point menu, int height) double half = 32 * Math.Max(0.6, height / 1009.0); return !menu.IsEmpty && Math.Abs(x.X - menu.X) <= half && Math.Abs(x.Y - menu.Y) <= half; internal bool ToastOver(Frame f, Point p) Point x = AnyToastX(f, out text); if (x.IsEmpty) x = GreenToastX(f); if (x.IsEmpty) x = OrangeToastX(f); if (x.IsEmpty) x = RedToastX(f); return !x.IsEmpty && p.X >= x.X - 0.37 * f.Width && p.Y <= f.Height / 2; public bool CloseTerritoryToasts() for (int i = 0; i < 6; i++) using (var f = Capture()) x = AnyToastX(f, out text); if (x.IsEmpty) { x = GreenToastX(f); text = "a green one"; } if (x.IsEmpty) { x = OrangeToastX(f); text = "an orange one"; } if (x.IsEmpty || !ToastStillOver(x)) break; log("Closing the game's notification (\"" + Short(text) + "\") with its x - it covered the page");