using System.Collections.Generic;using System.Text.RegularExpressions; sealed class MarketListing public int Left = -1, Total = -1, Price = -1; public OcrLine Remove, Each; public bool Animal { get { return Regex.IsMatch(Name ?? "", @"\(\s*[LI1]evel\s*[0-9OIl]+\s*\)?\s*$", RegexOptions.IgnoreCase); } } public override string ToString() { return Name + " (" + (Left >= 0 ? Left.ToString() : "?") + " at " + (Price > 0 ? Price.ToString() : "?") + " each)"; } public int Used = -1, Slots = -1; public readonly List<MarketListing> Rows = new List<MarketListing>(); public bool ActivityInSight; public string Signature = ""; public bool Seen { get { return Used >= 0 || Rows.Count > 0; } } sealed class SellRow { public string Name = ""; public int Count = -1; public OcrLine Line, Sell; } public OcrLine Title, NewListing; public Point SearchAt = Point.Empty; public string SearchText = ""; public readonly List<SellRow> Items = new List<SellRow>(); public int FormCount = -1; public OcrLine Quantity, All, Button; public Point QuantityAt = Point.Empty, PriceAt = Point.Empty; public int PayEach = -1, PayAll = -1, PayFor = -1; public int SlotsUsed = -1, Slots = -1; public string ButtonText = ""; sealed partial class Game static readonly Regex ListingsRx = new Regex(@"LISTINGS\s*\(\s*([0-9OoIlS]+?)\s*[O0]F\s*([0-9OoIlS]+)\s*\)", RegexOptions.IgnoreCase); static readonly Regex MarketLeftRx = new Regex(@"([0-9Il|][0-9Il|,]*)\s*[o0][fn]\s*([0-9Il|][0-9Il|,]*)\s*l[eo]ft", RegexOptions.IgnoreCase); static readonly Regex ItemRx = new Regex(@"^(.*?[A-Za-z]{2}.*?)\s+[xX×]\s*([0-9lI|!][0-9,lI|!]*)\s*$"); static readonly Regex PayRx = new Regex(@"receive\s+([0-9][0-9,]*)\s+[GC]old\s+Bars?\s+per\s+item\s+sold\W*([0-9][0-9,]*)\s+[GC]old\s+Bars?\s+for\s+all\s+([0-9][0-9,]*)", RegexOptions.IgnoreCase); static readonly Regex PayOneRx = new Regex(@"receive\s+([0-9][0-9,]*)\s+[GC]old\s+Bars?\s+per\s+item\s+sold(?!\W*[0-9][0-9,]*\s+[GC]old\s+Bars?\s+for\s+all)", RegexOptions.IgnoreCase); static readonly Regex SlotsRx = new Regex(@"([0-9]+)\s*of\s*([0-9]+)\s*shop\s*slots", RegexOptions.IgnoreCase); internal static Action<string> Trace = null; static int Num(string s) { int n; return int.TryParse(Parse.Digits(Regex.Replace((s ?? "").Replace(",", ""), "[lI|!]", "1")), out n) ? n : -1; } internal static string TidyItem(string s) var words = (s ?? "").Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).ToList(); while (words.Count > 1 && words[0].Count(char.IsLetter) < 2) words.RemoveAt(0); return string.Join(" ", words).Trim(); internal static bool SameItem(string a, string b) string ka = Regex.Replace(Parse.Key(a), "[^A-Z]", ""), kb = Regex.Replace(Parse.Key(b), "[^A-Z]", ""); if (ka.Length < 3 || kb.Length < 3) return false; if (ka == kb) return true; return View.Distance(ka, kb) <= Math.Max(1, Math.Min(ka.Length, kb.Length) / 8); internal static List<string> SearchWords(string name) return Regex.Split(name ?? "", "[^A-Za-z]+").Where(w => w.Length >= 3).Distinct(StringComparer.OrdinalIgnoreCase) .OrderByDescending(w => w.Length).Take(2).Select(w => w.ToLowerInvariant()).ToList(); internal List<OcrLine> MarketLines(Frame f) { return Ocr.Read(f, PageArea(f), PageScale(f), Prep.None); } internal OcrLine MarketTab(Frame f, List<OcrLine> lines, string name) var browse = lines.FirstOrDefault(l => Parse.Key(l.Text).EndsWith("BROWSE") && l.CenterY < area.Top + area.Height * 0.3); if (browse == null) return null; var tab = lines.FirstOrDefault(l => Parse.Key(l.Text) == Parse.Key(name) && Math.Abs(l.CenterY - browse.CenterY) <= browse.Box.Height && l.Box.Left > browse.Box.Right); if (tab != null) return tab; int h = Math.Max(8, browse.Box.Height); var strip = Rectangle.Intersect(Rectangle.FromLTRB(browse.Box.Right + 2, browse.Box.Top - h / 2, browse.Box.Right + 25 * h, browse.Box.Bottom + h / 2), new Rectangle(0, 0, f.Width, f.Height)); if (strip.Width <= 0 || strip.Height <= 0) return null; foreach (var t in new[] { Tuple.Create(2, Prep.None), Tuple.Create(2, Prep.Contrast), Tuple.Create(3, Prep.None) }) tab = Ocr.Read(f, strip, t.Item1, t.Item2).FirstOrDefault(l => Parse.Key(l.Text) == Parse.Key(name)); if (tab != null) return tab; internal enum Prices { None, Top, Last, All } public MyShopPage ReadMyShop(Frame f, Prices prices = Prices.None) { return ReadMyShop(f, MarketLines(f), prices); } internal MyShopPage ReadMyShop(Frame f, List<OcrLine> lines, Prices prices = Prices.None) var p = new MyShopPage { Signature = string.Join(",", lines.Select(l => l.Text + "@" + l.Box.Y)) }; var m = ListingsRx.Match(l.Text); if (m.Success && Parse.Key(l.Text).Contains("SHOPIISTINGS")) int a = Num(m.Groups[1].Value), b = Num(m.Groups[2].Value); if (a >= 0 && b > 0 && a <= b) { p.Used = a; p.Slots = b; } if (Parse.Key(l.Text).StartsWith("RECENTACTIVITY")) p.ActivityInSight = true; var eaches = lines.Where(l => Parse.Key(l.Text) == "EACH").ToList(); foreach (var r in lines.Where(l => Parse.Key(l.Text) == "REMOVE").OrderBy(l => l.CenterY)) int h = Math.Max(8, r.Box.Height); var each = eaches.Where(e => Math.Abs(e.CenterY - r.CenterY) <= h && e.Box.Right <= r.Box.Left + h && r.Box.Left - e.Box.Right < 6 * h) .OrderBy(e => r.Box.Left - e.Box.Right).FirstOrDefault(); if (each == null) continue; var row = new MarketListing { Remove = r, Y = r.CenterY }; var name = lines.Where(l => l.CenterX < area.Left + area.Width / 2 && l.CenterY < r.CenterY - h / 5 && l.CenterY > r.CenterY - 2.6 * h && TidyItem(l.Text).Count(char.IsLetter) >= 3) .OrderBy(l => r.CenterY - l.CenterY).FirstOrDefault(); if (name != null) row.Name = TidyItem(name.Text); var forSale = lines.FirstOrDefault(l => ForSaleWord(l.Text) && l.CenterY <= r.CenterY + h / 3 && l.CenterY > r.CenterY - 2.6 * h); row.ForSale = forSale != null; var left = lines.Where(l => l.CenterY > r.CenterY - h / 3 && l.CenterY < r.CenterY + 2.6 * h && l.Box.Right < each.Box.Left).Select(l => MarketLeftRx.Match(l.Text)).FirstOrDefault(m => m.Success); int fh = forSale != null ? Math.Max(6, forSale.Box.Height) : h; var corner = forSale != null ? Rectangle.FromLTRB(forSale.Box.Right - 14 * fh, forSale.Box.Bottom, forSale.Box.Right + fh, forSale.Box.Bottom + 3 * fh) : Rectangle.FromLTRB(each.Box.Left - 20 * h, r.CenterY + h / 3, each.Box.Left - 9 * h, r.CenterY + 2 * h); foreach (var t in new[] { Tuple.Create(3, Prep.None), Tuple.Create(2, Prep.WhiteSoft), Tuple.Create(4, Prep.None) }) var m = MarketLeftRx.Match(Ocr.ReadText(f, corner, t.Item1, t.Item2)); if (m.Success) { left = m; break; } row.Left = Num(left.Groups[1].Value); row.Total = Num(left.Groups[2].Value); if (row.Left > row.Total) row.Left = row.Total = -1; for (int i = 0; i < p.Rows.Count; i++) if (prices == Prices.All || (prices == Prices.Top && i == 0) || (prices == Prices.Last && i == p.Rows.Count - 1)) p.Rows[i].Price = MarketPrice(f, p.Rows[i].Each, Math.Max(8, p.Rows[i].Remove.Box.Height)); internal static bool ForSaleWord(string text) string k = Parse.Key(text); return k == "FORSAIE" || (k.Length >= 6 && k.Length <= 8 && (k.EndsWith("SAIE") || k.StartsWith("FORSA"))); internal static int MarketPrice(Frame f, OcrLine each, int h) var band = Rectangle.Intersect(Rectangle.FromLTRB(each.Box.Left - 12 * h, each.Box.Top - h / 2, each.Box.Left - 2, each.Box.Bottom + h / 2), new Rectangle(0, 0, f.Width, f.Height)); if (band.Width <= 0 || band.Height <= 0) return -1; Func<int, int, bool> gold = (x, y) => return Math.Abs(c.R - 212) <= 18 && Math.Abs(c.G - 175) <= 18 && Math.Abs(c.B - 55) <= 22 || EventOrange(c); var cols = new int[band.Width]; for (int x = band.Left; x < band.Right; x++) for (int y = band.Top; y < band.Bottom; y++) if (gold(x, y)) cols[x - band.Left]++; int end = Array.FindLastIndex(cols, n => n > 0); int start = end, gap = 0, maxGap = Math.Max(3, h * 6 / 10); for (int i = end - 1; i >= 0; i--) if (cols[i] > 0) { start = i; gap = 0; } else if (++gap > maxGap) break; int x0 = band.Left + start, x1 = band.Left + end, y0 = int.MaxValue, y1 = -1; for (int y = band.Top; y < band.Bottom; y++) for (int x = x0; x <= x1; x++) if (gold(x, y)) { y0 = Math.Min(y0, y); y1 = Math.Max(y1, y); break; } if (y1 - y0 < h / 2 || (x1 - x0) > 6 * h) return -1; var ink = Rectangle.FromLTRB(x0, y0, x1 + 1, y1 + 1); var reads = new List<int>(); var around = Rectangle.Inflate(ink, h, h / 2); Func<int, bool> add = n => if (n <= 0) return false; return reads.Count(x => x == n) >= 2 && reads.All(x => x == n); foreach (var t in new[] { Tuple.Create(3, Prep.None), Tuple.Create(4, Prep.None), Tuple.Create(5, Prep.None), Tuple.Create(2, Prep.None), Tuple.Create(3, Prep.Contrast), Tuple.Create(4, Prep.Contrast) }) string text = Ocr.ReadText(f, around, t.Item1, t.Item2); if (Trace != null) Trace("price text at " + t.Item1 + "/" + t.Item2 + ": '" + text + "'"); var m = Regex.Match(text, @"(?<![A-Za-z])[0-9IlS|][0-9IlSO,|]*(?![A-Za-z])"); string digits = m.Success && m.Value.Any(char.IsDigit) ? m.Value.Replace('I', '1').Replace('l', '1').Replace('|', '1').Replace('S', '5').Replace('O', '0') : ""; if (add(digits.Length > 0 ? Num(digits) : -1)) { sure = true; break; } foreach (var crop in new[] { ink, Rectangle.Inflate(ink, 1, 1), Rectangle.Inflate(ink, 2, 1) }) foreach (int copies in new[] { 2, 3, 4 }) if (add(ReadLoneNumber(f, crop, new[] { copies }))) { sure = true; break; } foreach (int copies in new[] { 2, 3 }) foreach (int zoom in new[] { 3, 4 }) if (!sure && add(InkNumber(f, ink, gold, copies, zoom))) sure = true; if (Trace != null) Trace("price ink " + ink + " reads " + string.Join(",", reads)); var groups = reads.GroupBy(n => n).OrderByDescending(g => g.Count()).ToList(); if (groups.Count == 0 || groups[0].Count() < 2 || (groups.Count > 1 && groups[1].Count() * 2 >= groups[0].Count())) return -1; return groups[0].Key <= 10000000 ? groups[0].Key : -1; static int InkNumber(Frame f, Rectangle ink, Func<int, int, bool> isInk, int copies, int zoom) int pad = Math.Max(4, ink.Height) * zoom, w = ink.Width * zoom, h = ink.Height * zoom; var bmp = new Bitmap(2 * pad + copies * w + (copies - 1) * pad, h + 2 * pad, System.Drawing.Imaging.PixelFormat.Format32bppArgb); using (var g = Graphics.FromImage(bmp)) g.Clear(Color.White); for (int y = 0; y < ink.Height; y++) for (int x = 0; x < ink.Width; x++) if (!isInk(ink.X + x, ink.Y + y)) continue; for (int k = 0; k < copies; k++) for (int dy = 0; dy < zoom; dy++) for (int dx = 0; dx < zoom; dx++) bmp.SetPixel(pad + k * (w + pad) + x * zoom + dx, pad + y * zoom + dy, Color.Black); using (var copy = new Frame(bmp)) string text = Ocr.ReadText(copy, new Rectangle(0, 0, copy.Width, copy.Height), 1, Prep.None); if (Trace != null) Trace("ink number x" + copies + " zoom " + zoom + ": '" + text + "'"); var words = Regex.Split(text, @"\s+").Select(s => Regex.Replace(s, "[^0-9IlSO|]", "")).Where(s => s.Length > 0).ToList(); if (words.Count != copies) return -1; var nums = words.Select(s => s.Replace('I', '1').Replace('l', '1').Replace('|', '1').Replace('S', '5').Replace('O', '0')).ToList(); return nums.All(s => s == nums[0]) && nums[0].Length <= 7 && words.Any(s => s.Any(char.IsDigit)) && int.TryParse(nums[0], out n) ? n : -1; internal sealed class RemoveWindow { public string Name; public OcrLine Remove, Cancel; } internal RemoveWindow ReadRemoveWindow(Frame f) var lines = MarketLines(f); var w = ReadRemoveWindow(lines); if (w == null || (w.Remove != null && w.Cancel != null)) return w; var title = lines.First(l => Parse.Key(l.Text).Contains("REMOVEIISTING")); int th = Math.Max(8, title.Box.Height); var below = Rectangle.Intersect(Rectangle.FromLTRB(title.Box.Left - 3 * th, title.Box.Bottom + 3 * th, title.Box.Left + 30 * th, title.Box.Bottom + 16 * th), new Rectangle(0, 0, f.Width, f.Height)); foreach (var t in new[] { Tuple.Create(1, Prep.None), Tuple.Create(1, Prep.Contrast), Tuple.Create(2, Prep.Contrast) }) var more = lines.Concat(Ocr.Read(f, below, t.Item1, t.Item2)).ToList(); var again = ReadRemoveWindow(more); if (again != null && again.Remove != null && again.Cancel != null) return again; internal static RemoveWindow ReadRemoveWindow(List<OcrLine> lines) var title = lines.FirstOrDefault(l => Parse.Key(l.Text).Contains("REMOVEIISTING")); if (title == null) return null; var w = new RemoveWindow(); w.Cancel = lines.FirstOrDefault(l => Parse.Key(l.Text) == "CANCEI" && l.CenterY > title.CenterY); w.Remove = lines.FirstOrDefault(l => Parse.Key(l.Text) == "REMOVE" && Math.Abs(l.CenterY - w.Cancel.CenterY) <= w.Cancel.Box.Height && l.CenterX < w.Cancel.CenterX && w.Cancel.Box.Left - l.Box.Right < 12 * w.Cancel.Box.Height); int th = Math.Max(8, title.Box.Height); int bottom = w.Remove != null ? w.Remove.Box.Top : title.CenterY + 12 * th; int right = title.Box.Left + 30 * th; string q = string.Join(" ", lines.Where(l => l.CenterY > title.Box.Bottom && l.CenterY < bottom && l.Box.Left >= title.Box.Left - 2 * th && l.Box.Right <= right) .OrderBy(l => l.CenterY).ThenBy(l => l.CenterX).Select(l => l.Text)); var m = Regex.Match(q, @"Take\s+(.+?)\s+out\s+of\s+your", RegexOptions.IgnoreCase); if (m.Success) w.Name = m.Groups[1].Value.Trim(); internal SellPage ReadSellPage(Frame f) { return ReadSellPage(MarketLines(f)); } internal static SellPage ReadSellPage(List<OcrLine> lines) p.Title = lines.FirstOrDefault(l => Parse.Key(l.Text).StartsWith("YOURTRADABIE")); p.NewListing = lines.FirstOrDefault(l => Parse.Key(l.Text).StartsWith("NEWIISTING")); if (p.Title == null || p.NewListing == null || p.NewListing.Box.Left <= p.Title.Box.Right) return null; int h = Math.Max(8, p.Title.Box.Height); int split = p.NewListing.Box.Left - 2 * h; var left = lines.Where(l => l.CenterX < split && l.Box.Left >= p.Title.Box.Left - 4 * h && l.CenterY > p.Title.Box.Bottom).OrderBy(l => l.CenterY).ToList(); var right = lines.Where(l => l.Box.Left >= split && l.CenterY > p.NewListing.Box.Bottom).OrderBy(l => l.CenterY).ToList(); foreach (var sell in left.Where(l => Parse.Key(l.Text) == "SEII")) var name = left.Where(l => l.Box.Right < sell.Box.Left && l.CenterY <= sell.CenterY + h / 3 && l.CenterY > sell.CenterY - 2 * h && TidyItem(l.Text).Count(char.IsLetter) >= 3) .OrderBy(l => l.CenterY).FirstOrDefault(); if (name == null) continue; var row = new SellRow { Line = name, Sell = sell, Name = TidyItem(name.Text) }; var m = ItemRx.Match(name.Text); if (m.Success) { row.Name = TidyItem(m.Groups[1].Value); row.Count = Num(m.Groups[2].Value); } var count = left.FirstOrDefault(l => l.Box.Left > name.Box.Right && l.Box.Right < sell.Box.Left && Math.Abs(l.CenterY - name.CenterY) <= h / 2 && Regex.IsMatch(l.Text.Trim(), @"^[xX×]\s*[0-9][0-9,]*$")); if (count != null) row.Count = Num(count.Text); int firstItem = p.Items.Count > 0 ? p.Items.Min(i => i.Line.Box.Top) : int.MaxValue; var search = left.FirstOrDefault(l => Parse.Key(l.Text).Contains("SEARCHYOURITEMS")); search = left.FirstOrDefault(l => l.CenterY < p.Title.CenterY + 7 * h && l.Box.Bottom < firstItem && !ItemRx.IsMatch(l.Text) && Parse.Key(l.Text) != "SEII"); if (!Parse.Key(search.Text).Contains("SEARCHYOURITEMS")) p.SearchText = search.Text.Trim(); p.SearchAt = new Point(p.Title.Box.Left + (split - p.Title.Box.Left) * 85 / 100, search.CenterY); var item = right.FirstOrDefault(l => ItemRx.IsMatch(l.Text)); if (item != null && !right.Any(l => l.CenterY < item.CenterY && Parse.Key(l.Text).StartsWith("PICKANITEM"))) var m = ItemRx.Match(item.Text); p.FormItem = TidyItem(m.Groups[1].Value); p.FormCount = Num(m.Groups[2].Value); var qty = p.Quantity = right.FirstOrDefault(l => Parse.Key(l.Text).StartsWith("QUANTIT")); var price = right.FirstOrDefault(l => Parse.Key(l.Text).StartsWith("PRICEPERITEM")); if (qty != null) p.All = right.FirstOrDefault(l => Parse.Key(l.Text) == "AII" && l.CenterY > qty.CenterY && l.CenterY < qty.CenterY + 6 * h); p.FormOpen = qty != null && p.All != null && !right.Any(l => Parse.Key(l.Text).StartsWith("PICKANITEM")); if (qty != null && p.All != null) p.QuantityAt = new Point((p.NewListing.Box.Left + p.All.Box.Left) / 2, p.All.CenterY); if (price != null && price.CenterY > p.All.CenterY) p.PriceAt = new Point(p.QuantityAt.X, price.CenterY + (p.All.CenterY - qty.CenterY)); var view = right.FirstOrDefault(l => Parse.Key(l.Text).StartsWith("VIEWACTIVE")); int payTop = price != null ? price.Box.Bottom : p.NewListing.Box.Bottom, payBottom = view != null ? view.Box.Top : int.MaxValue; string pay = string.Join(" ", right.Where(l => l.CenterY > payTop && l.CenterY < payBottom).Select(l => l.Text)); var pm = PayRx.Match(pay); if (pm.Success) { p.PayEach = Num(pm.Groups[1].Value); p.PayAll = Num(pm.Groups[2].Value); p.PayFor = Num(pm.Groups[3].Value); } var one = PayOneRx.Match(pay); if (one.Success) { p.PayEach = Num(one.Groups[1].Value); p.PayAll = p.PayEach; p.PayFor = 1; } p.Button = right.FirstOrDefault(l => l.Box.Top > view.Box.Bottom && l.CenterY < view.CenterY + 10 * h); if (p.Button != null) p.ButtonText = p.Button.Text.Trim(); var slots = right.Select(l => SlotsRx.Match(l.Text)).FirstOrDefault(m => m.Success); if (slots != null) { p.SlotsUsed = Num(slots.Groups[1].Value); p.Slots = Num(slots.Groups[2].Value); } void PressMarket(OcrLine b, string want, int waitMs) if (b == null || Parse.Key(b.Text) != Parse.Key(want)) throw new NeverPressException(b == null ? want : b.Text, "not the Black Market's " + want); if (OnlyNavigate) RefuseInCheck(b.Text); Click(b.CenterX, b.CenterY, waitMs); public MyShopPage OpenMyShop(Prices prices = Prices.None) if (!OpenTab(Tab.BlackMarket, true)) return null; for (int look = 0; look < 5; look++) using (var f = Capture()) var lines = MarketLines(f); var shop = ReadMyShop(f, lines, prices); if (shop.Seen) return shop; var tab = MarketTab(f, lines, "MY SHOP"); if (tab != null) { PressMarket(tab, "MY SHOP", 1200); continue; } log("Black Market: MY SHOP didn't open"); Snapshot("market my shop not open", 60); public MyShopPage MyShopToEnd(MyShopPage top) for (int roll = 0; roll < 12 && !shop.ActivityInSight; roll++) using (var f = Capture()) next = ReadMyShop(f); if (next.Signature == shop.Signature) break; if (next.Used < 0) { next.Used = top.Used; next.Slots = top.Slots; } public bool? RemoveListing(MarketListing l, int usedBefore) PressMarket(l.Remove, "REMOVE", 1200); for (int look = 0; look < 4 && (w == null || w.Remove == null); look++) using (var f = Capture()) w = ReadRemoveWindow(f); if (w == null || w.Remove == null) log("Black Market: REMOVE on " + l.Name + " didn't bring up its question - nothing taken out"); Snapshot("market remove question unread", 60); if (w != null && w.Cancel != null) PressMarket(w.Cancel, "CANCEL", 800); if (w.Name == null || !SameItem(w.Name, l.Name)) log("Black Market: the question asked about \"" + (w.Name ?? "?") + "\", not " + l.Name + " - cancelled"); Snapshot("market remove question other item", 60); if (w.Cancel != null) PressMarket(w.Cancel, "CANCEL", 800); PressMarket(w.Remove, "REMOVE", 1500); for (int look = 0; look < 5; look++) using (var f = Capture()) var lines = MarketLines(f); if (ReadRemoveWindow(lines) != null) continue; var shop = ReadMyShop(f, lines); if (usedBefore > 0 && shop.Used == usedBefore - 1) return true; var again = OpenMyShop(); if (again == null) return null; if (usedBefore > 0 && again.Used == usedBefore - 1) return true; again = MyShopToEnd(again); return again.Seen && !again.Rows.Any(r => SameItem(r.Name, l.Name) && r.Left == l.Left); for (int look = 0; look < 5; look++) using (var f = Capture()) var lines = MarketLines(f); var p = ReadSellPage(lines); var tab = MarketTab(f, lines, "SELL"); if (tab != null) { PressMarket(tab, "SELL", 1200); continue; } if (!OnTab(f, Tab.BlackMarket) && !OpenTab(Tab.BlackMarket, true)) return null; internal void FormItemAgain(Frame f, SellPage p, string name, bool onlyRow) if (p == null || p.Quantity == null || (p.FormItem != null && SameItem(p.FormItem, name))) return; int h = Math.Max(8, p.NewListing.Box.Height), s = PageScale(f); var card = Rectangle.Intersect(Rectangle.FromLTRB(p.NewListing.Box.Left - h, p.NewListing.Box.Bottom, PageArea(f).Right, p.Quantity.Box.Top), new Rectangle(0, 0, f.Width, f.Height)); if (card.Width > 0 && card.Height > 0) foreach (var t in new[] { Tuple.Create(s + 1, Prep.None), Tuple.Create(s + 1, Prep.Contrast), Tuple.Create(s + 1, Prep.RedText), Tuple.Create(s + 2, Prep.None), Tuple.Create(s + 2, Prep.RedText) }) foreach (var l in Ocr.Read(f, card, t.Item1, t.Item2)) var m = ItemRx.Match(l.Text); string read = TidyItem(m.Success ? m.Groups[1].Value : l.Text); if (!SameItem(read, name)) continue; if (m.Success) p.FormCount = Num(m.Groups[2].Value); if (onlyRow && p.FormOpen && p.FormItem == null) log("Black Market: the listing form's name didn't read - going by the search's only row, " + name); Snapshot("market form name unread", 0, true, false); static readonly Regex PlusRx = new Regex(@"\+\s*([0-9]+)\s*$"); static string Plus(string name) { return PlusRx.Match(name ?? "").Groups[1].Value; } public int KeptSpotMinutes = -1; static readonly Regex KeptSpotRx = new Regex(@"old\s*spot.*?in\s+([0-9]{1,3})\s*m", RegexOptions.IgnoreCase | RegexOptions.Singleline); internal static int KeptSpot(string toast) var m = KeptSpotRx.Match(toast ?? ""); return m.Success ? int.Parse(m.Groups[1].Value) : -1; public const string NotTradable = "it isn't among your tradable items"; public string ListItem(string name, int qty, int price, out int listed) if (p == null) return "the SELL tab didn't open"; foreach (var word in SearchWords(name)) if (p.SearchAt.IsEmpty) { Snapshot("market search box not found", 60); return "its search box wasn't found"; } Click(p.SearchAt.X, p.SearchAt.Y, 300); Type(new string('\b', (p.SearchText ?? "").Length + 8) + word); for (int look = 0; look < 3 && row == null; look++) Wait(look == 0 ? 900 : 700); using (var f = Capture()) p = ReadSellPage(f) ?? p; row = p.Items.FirstOrDefault(i => SameItem(i.Name, name) && Plus(i.Name) == Plus(name)) ?? p.Items.FirstOrDefault(i => SameItem(i.Name, name)); if (row == null) return NotTradable; if (row.Sell == null) return "its SELL didn't read"; bool onlyRow = p.Items.Count == 1; PressMarket(row.Sell, "SELL", 900); for (int go = 0; go < 2; go++) for (int look = 0; look < 4; look++) using (var f = Capture()) { p = ReadSellPage(f); FormItemAgain(f, p, name, onlyRow && look == 3); } if (p != null && p.FormItem != null && SameItem(p.FormItem, name)) break; if (p == null || p.FormItem == null || !SameItem(p.FormItem, name)) log("Black Market: the listing form for " + name + " shows " + (p == null ? "no page" : p.FormItem == null ? "no item" : "\"" + p.FormItem + "\"")); Snapshot("market form not its item", 60); return "its listing form didn't show it"; if (p.QuantityAt.IsEmpty || p.PriceAt.IsEmpty) return "its listing form didn't read"; int have = p.FormCount > 0 ? p.FormCount : row.Count; if (have <= 0 && qty == 1) have = 1; if (have <= 0) return "how many you have didn't read"; want = Math.Min(qty, have); if (want == p.FormCount && p.All != null) Press(p.All, 500); else { Click(p.QuantityAt.X, p.QuantityAt.Y, 300); Type(new string('\b', 8) + want); } Click(p.PriceAt.X, p.PriceAt.Y, 300); Type(new string('\b', 8) + price + "\r"); using (var f = Capture()) FormItemAgain(f, p, name, onlyRow); ready = p != null && FormSays(p, name, want, price) && GoldIsh(f, p.Button.Box.Left - p.Button.Box.Height, p.Button.CenterY); log("Black Market: the listing form for " + name + " didn't read back right (" + (p == null ? "no form" : "\"" + p.ButtonText + "\", " + (p.PayFor >= 0 ? p.PayEach + " per item, for all " + p.PayFor : "no payout line")) + ") - not put up"); Snapshot("market form unread", 60); return "its listing form didn't read back right"; PressMarket(p.Button, "PUT IN YOUR SHOP", 1500); KeptSpotMinutes = KeptSpot(ReadToast(2500, t => Parse.Has(t, "LISTED"))); for (int look = 0; look < 8; look++) using (var f = Capture()) var shop = ReadMyShop(f, Prices.Top); var top = shop.Rows.FirstOrDefault(); if (top != null && SameItem(top.Name, name) && top.Left == want && top.Price == price) { listed = want; return null; } using (var f = Capture()) check = Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None).Any(l => Parse.Key(l.Text).StartsWith("BOTCHECK")); if (check) Snapshot("market bot check", 0, true, false); else Snapshot("market listing not seen", 60); public const string NotSeenAfterPut = "it didn't show in MY SHOP after PUT IN YOUR SHOP"; public bool OnTop(Frame f, string name, int qty, int price) var top = ReadMyShop(f, Prices.Top).Rows.FirstOrDefault(); return top != null && SameItem(top.Name, name) && top.Left == qty && top.Price == price; internal static bool FormSays(SellPage p, string name, int qty, int price) if (p.FormItem == null || !SameItem(p.FormItem, name) || p.Button == null || Parse.Key(p.ButtonText) != "PUTINYOURSHOP") return false; if (p.PayFor != qty || p.PayEach <= 0 || p.PayEach >= price || p.PayEach < price - Math.Max(1, (int)Math.Ceiling(price * 0.07)) - 1) return false; return p.PayAll < 0 || (long)p.PayEach * qty == p.PayAll;