using System.Collections.Generic;using System.Drawing.Drawing2D;using System.Drawing.Imaging;using System.Runtime.InteropServices;using System.Runtime.InteropServices.WindowsRuntime;using System.Text.RegularExpressions;using Windows.Graphics.Imaging; static readonly List<Pixels> spare = new List<Pixels>(); public IntPtr At { get { return pin.AddrOfPinnedObject(); } } public static int Waiting { get { lock (spare) return spare.Count; } } public static Pixels Rent(int size) var fit = spare.Where(s => s.Bytes.Length >= size).OrderBy(s => s.Bytes.Length).FirstOrDefault(); if (fit != null) { spare.Remove(fit); return fit; } var p = new Pixels { Bytes = new byte[size] }; p.pin = GCHandle.Alloc(p.Bytes, GCHandleType.Pinned); if (spare.Count < 2) { spare.Add(this); return; } var least = spare.OrderBy(s => s.Bytes.Length).First(); if (least.Bytes.Length >= Bytes.Length) { Free(); return; } public void Free() { if (pin.IsAllocated) pin.Free(); Bytes = null; } public static void Release() { lock (spare) { foreach (var s in spare) s.Free(); spare.Clear(); } } public sealed class Frame : IDisposable public readonly Bitmap Bitmap; public Frame(Bitmap bmp) : this(bmp, false) { } internal Frame(Bitmap bmp, bool capture) var data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); int size = stride * bmp.Height; if (capture && size >= 4 * 1024 * 1024) held = Pixels.Rent(size); else px = new byte[size]; Marshal.Copy(data.Scan0, px, 0, size); if (held == null) { Bitmap = bmp; GC.SuppressFinalize(this); return; } Bitmap = new Bitmap(bmp.Width, bmp.Height, stride, PixelFormat.Format32bppArgb, held.At) { Tag = bmp.Tag }; public int Width { get { return Bitmap.Width; } } public int Height { get { return Bitmap.Height; } } public static readonly object NotSendable = new object(); public bool Sendable { get { return Bitmap.Tag != NotSendable; } } public static Bitmap Marked(Bitmap from, Bitmap copy) if (from != null && copy != null && from.Tag == NotSendable) copy.Tag = NotSendable; public Color Pixel(int x, int y) if (x < 0 || y < 0 || x >= Width || y >= Height) return Color.Black; int i = y * stride + x * 4; return Color.FromArgb(px[i + 2], px[i + 1], px[i]); public bool Near(int x, int y, int rgb, int tol) return Math.Abs(c.R - ((rgb >> 16) & 0xFF)) <= tol && Math.Abs(c.G - ((rgb >> 8) & 0xFF)) <= tol && Math.Abs(c.B - (rgb & 0xFF)) <= tol; public double FillRatio(int x1, int x2, int y, int rgb, int tol) for (int x = x1; x <= x2; x += 2) { n++; if (Near(x, y, rgb, tol)) hit++; } return n == 0 ? 0 : (double)hit / n; for (int i = 0; i < 48; i++) for (int j = 0; j < 27; j++) int k = (int)((j + 0.5) * Height / 27) * stride + (int)((i + 0.5) * Width / 48) * 4; if (px[k] >= 250 && px[k + 1] >= 250 && px[k + 2] >= 250) white++; return white * 10 >= n * 3; public bool TopBarDrawn() int band = Math.Max(1, Height * 11 / 100), n = 0, detail = 0; int k0 = 2 * stride + 2 * 4; int r0 = px[k0 + 2], g0 = px[k0 + 1], b0 = px[k0]; for (int y = 2; y < band; y += 3) for (int x = 2; x < Width; x += 3) int k = y * stride + x * 4; if (Math.Abs(px[k + 2] - r0) + Math.Abs(px[k + 1] - g0) + Math.Abs(px[k] - b0) > 30) detail++; return detail * 200 >= n; GC.SuppressFinalize(this); var h = System.Threading.Interlocked.Exchange(ref held, null); try { Bitmap.Dispose(); } catch (Exception) { } var h = System.Threading.Interlocked.Exchange(ref held, null); public sealed class OcrLine public int CenterX { get { return Box.X + Box.Width / 2; } } public int CenterY { get { return Box.Y + Box.Height / 2; } } public override string ToString() { return string.Format("{0},{1} {2}", Box.X, Box.Y, Text); } public enum Prep { None, WhiteText, DarkText, WhiteBright, WhiteSoft, GreyText, Contrast, RedText } static string problem, language, warning; static bool engineChecked; static readonly object gate = new object(); public static int Pad = 16; public const string HowToFix = "To fix it, add English text recognition, then restart the bot (it checks once, when it starts; changing Windows' display " + "language or Roblox's language doesn't add it): in Windows Settings > Time & language > Language & region > Add a language, pick English (United States) " + "with its optional features ticked - or, more sure, run this in PowerShell as administrator (it needs internet and takes a few minutes): " + "Add-WindowsCapability -Online -Name \"Language.OCR~~~en-US~0.0.1.0\""; public static string Language { get { return language; } } const int MaxSide = 10000; public static string Check() if (engineChecked) return problem; problem = Bridge.Check(); if (problem == null) language = "the Linux reader"; public static string Warning { get { return warning; } } static int MaxSide { get { return (int)OcrEngine.MaxImageDimension; } } public static string Check() if (engineChecked) return problem; problem = Unusable(e == null ? null : e.RecognizerLanguage); if (problem == null) { engine = e; language = e.RecognizerLanguage.DisplayName; warning = NotEnglish(e.RecognizerLanguage); } problem = "Windows text recognition (OCR) doesn't work on this PC (" + ex.Message.Replace("\r", " ").Replace("\n", " ").Trim() + "), so the bot can't read the game. " + HowToFix; public static string Warning { get { return warning; } } internal static string NotEnglish(Windows.Globalization.Language lang) if (lang == null || lang.LanguageTag.StartsWith("en", StringComparison.OrdinalIgnoreCase)) return null; return "Windows reads text in " + lang.DisplayName + " on this PC, not English, so the bot can miss some words in the game." + Have() + " " + HowToFix; internal static string Unusable(Windows.Globalization.Language lang) if (lang == null) return "Windows has no English text recognition (OCR) on this PC, so the bot can't read the game." + Have() + " " + HowToFix; if (lang.Script != "Latn") return "Windows only has text recognition (OCR) for " + lang.DisplayName + " on this PC, which can't read the game's English letters. " + HowToFix; static OcrEngine Create() var e = OcrEngine.TryCreateFromLanguage(new Windows.Globalization.Language("en-US")); foreach (var lang in OcrEngine.AvailableRecognizerLanguages) if (lang.LanguageTag.StartsWith("en-", StringComparison.OrdinalIgnoreCase) && (e = OcrEngine.TryCreateFromLanguage(lang)) != null) return e; var profile = OcrEngine.TryCreateFromUserProfileLanguages(); var other = ChooseOther(OcrEngine.AvailableRecognizerLanguages.ToList(), profile == null ? null : profile.RecognizerLanguage); return (other == null ? null : OcrEngine.TryCreateFromLanguage(other)) ?? profile; internal static Windows.Globalization.Language ChooseOther(IList<Windows.Globalization.Language> installed, Windows.Globalization.Language profile) if (profile != null && profile.Script == "Latn") return profile; return installed.FirstOrDefault(l => l.Script == "Latn"); var l = OcrEngine.AvailableRecognizerLanguages; return l.Count == 0 ? " It has no text recognition installed at all." : " It only has: " + string.Join(", ", l.Select(x => x.DisplayName)) + "."; if (p != null) throw new InvalidOperationException(p); public static List<OcrLine> Read(Frame f, Rectangle area, int scale, Prep prep, bool asIs = false) area.Intersect(new Rectangle(0, 0, f.Width, f.Height)); var lines = new List<OcrLine>(); if (area.Width < 4 || area.Height < 4) return lines; int pad = prep == Prep.None ? 0 : Pad; int fit = (MaxSide - 2 * pad) / Math.Max(area.Width, area.Height); if (fit < 1) return lines; double zoom = Math.Min(ZoomFor(f, scale, asIs), fit); using (var pic = Prepare(f, area, zoom, prep)) foreach (var it in Bridge.Ask(Drawn(pic), pic.Picture.Width, pic.Picture.Height)) Box = new Rectangle(area.X + (int)((it.X - pad) / zoom), area.Y + (int)((it.Y - pad) / zoom), (int)(it.W / zoom), (int)(it.H / zoom)) using (var pic = Prepare(f, area, zoom, prep)) result = Recognize(pic); foreach (var line in result.Lines) double x1 = double.MaxValue, y1 = double.MaxValue, x2 = 0, y2 = 0; foreach (var w in line.Words) x1 = Math.Min(x1, r.X - pad); y1 = Math.Min(y1, r.Y - pad); x2 = Math.Max(x2, r.X - pad + r.Width); y2 = Math.Max(y2, r.Y - pad + r.Height); Box = new Rectangle(area.X + (int)(x1 / zoom), area.Y + (int)(y1 / zoom), (int)((x2 - x1) / zoom), (int)((y2 - y1) / zoom)) public static string ReadText(Frame f, Rectangle area, int scale, Prep prep, bool asIs = false) var sb = new StringBuilder(); foreach (var l in Read(f, area, scale, prep, asIs)) { if (sb.Length > 0) sb.Append(' '); sb.Append(l.Text); } public const double BigWindow = 1.75; internal static bool FullZoom = false; internal static double ZoomFor(Frame f, int scale, bool asIs = false) if (asIs || scale <= 1 || !Big(f)) return scale; return Math.Max(1, scale / Bigger(f)); static double Bigger(Frame f) { return Math.Min(f.Height / 1009.0, f.Width / 1790.0); } public static bool Big(Frame f) { return !FullZoom && Bigger(f) >= BigWindow; } public static void Release() { Pixels.Release(); } static byte[] Drawn(Prepared pic) var d = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); Marshal.Copy(d.Scan0, pic.Held.Bytes, 0, d.Stride * b.Height); internal sealed class Prepared : IDisposable public void Dispose() { Picture.Dispose(); Held.Back(); } static OcrResult Recognize(Prepared pic) using (var sb = SoftwareBitmap.CreateCopyFromBuffer(pic.Held.Bytes.AsBuffer(0, bmp.Width * 4 * bmp.Height), BitmapPixelFormat.Bgra8, bmp.Width, bmp.Height, BitmapAlphaMode.Premultiplied)) return Engine.RecognizeAsync(sb).AsTask().Result; internal static Prepared Prepare(Frame f, Rectangle area, double zoom, Prep prep) int pad = prep == Prep.None ? 0 : Pad; int w = (int)Math.Round(area.Width * zoom), h = (int)Math.Round(area.Height * zoom); var held = Pixels.Rent((w + 2 * pad) * 4 * (h + 2 * pad)); var big = new Bitmap(w + 2 * pad, h + 2 * pad, (w + 2 * pad) * 4, PixelFormat.Format32bppArgb, held.At); var pic = new Prepared { Picture = big, Held = held }; using (var g = Graphics.FromImage(big)) g.InterpolationMode = zoom == 1 ? InterpolationMode.NearestNeighbor : InterpolationMode.HighQualityBicubic; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.DrawImage(f.Bitmap, new Rectangle(pad, pad, w, h), area, GraphicsUnit.Pixel); if (prep == Prep.None) return pic; var data = big.LockBits(new Rectangle(0, 0, big.Width, big.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); int stride = data.Stride; var row = new byte[stride]; if (prep == Prep.Contrast) { Stretch(data, row, pad, w, h); big.UnlockBits(data); return pic; } for (int y = 0; y < big.Height; y++) var at = data.Scan0 + y * stride; Marshal.Copy(at, row, 0, stride); for (int x = 0; x < big.Width; x++) bool inside = x >= pad && y >= pad && x < pad + w && y < pad + h; byte b = row[i], gr = row[i + 1], r = row[i + 2]; int max = Math.Max(r, Math.Max(gr, b)), min = Math.Min(r, Math.Min(gr, b)); bool ink = inside && (prep == Prep.WhiteText ? (min >= 165 && max - min <= 70) : prep == Prep.WhiteBright ? (min >= 185 && max - min <= 70) : prep == Prep.WhiteSoft ? (min >= 130 && max - min <= 90) : prep == Prep.GreyText ? (min >= 70 && max <= 200 && max - min <= 30) : prep == Prep.RedText ? (r >= 120 && r - Math.Max(gr, b) >= 45) byte v = ink ? (byte)0 : (byte)255; row[i] = v; row[i + 1] = v; row[i + 2] = v; row[i + 3] = 255; Marshal.Copy(row, 0, at, stride); static void Stretch(BitmapData data, byte[] row, int pad, int w, int h) int stride = data.Stride; var count = new int[256]; for (int y = pad; y < pad + h; y++) Marshal.Copy(data.Scan0 + y * stride, row, 0, stride); for (int x = pad; x < pad + w; x++) { int i = x * 4; count[(row[i] + row[i + 1] + row[i + 2]) / 3]++; } for (int v = 1; v < 256; v++) if (count[v] > count[face]) face = v; for (int d = 255; d > 0 && beyond * 200 < w * h; d--) if (face + d < 256) beyond += count[face + d]; if (face - d >= 0) beyond += count[face - d]; for (int y = 0; y < h + 2 * pad; y++) var at = data.Scan0 + y * stride; Marshal.Copy(at, row, 0, stride); for (int x = 0; x < w + 2 * pad; x++) bool inside = x >= pad && y >= pad && x < pad + w && y < pad + h; int off = Math.Abs((row[i] + row[i + 1] + row[i + 2]) / 3 - face); byte v = inside && far > 0 ? (byte)(255 - Math.Min(255, off * 255 / far)) : (byte)255; row[i] = v; row[i + 1] = v; row[i + 2] = v; row[i + 3] = 255; Marshal.Copy(row, 0, at, stride); public static List<OcrLine> ReadWords(Frame f, Rectangle area, int scale, Prep prep, bool asIs = false) area.Intersect(new Rectangle(0, 0, f.Width, f.Height)); var words = new List<OcrLine>(); if (area.Width < 4 || area.Height < 4) return words; int pad = prep == Prep.None ? 0 : Pad; int fit = (MaxSide - 2 * pad) / Math.Max(area.Width, area.Height); if (fit < 1) return words; double zoom = Math.Min(ZoomFor(f, scale, asIs), fit); using (var pic = Prepare(f, area, zoom, prep)) foreach (var it in Bridge.Ask(Drawn(pic), pic.Picture.Width, pic.Picture.Height)) foreach (var word in text.Split(' ')) Box = new Rectangle(area.X + (int)((it.X + it.W * from / text.Length - pad) / zoom), area.Y + (int)((it.Y - pad) / zoom), (int)(it.W * word.Length / text.Length / zoom), (int)(it.H / zoom)) using (var pic = Prepare(f, area, zoom, prep)) result = Recognize(pic); foreach (var line in result.Lines) foreach (var w in line.Words) Box = new Rectangle(area.X + (int)((r.X - pad) / zoom), area.Y + (int)((r.Y - pad) / zoom), (int)(r.Width / zoom), (int)(r.Height / zoom)), public static class Parse public static string Key(string s) var sb = new StringBuilder(); foreach (char ch in (s ?? "").ToUpperInvariant()) if (char.IsLetterOrDigit(ch)) sb.Append(ch); return sb.ToString().Replace("VV", "W").Replace("0", "O").Replace("1", "I").Replace("L", "I").Replace("5", "S"); public static bool Has(string text, string phrase) { return Key(text).Contains(Key(phrase)); } public static string Digits(string s) var sb = new StringBuilder(); foreach (char ch in s ?? "") if (c == 'O' || c == 'o' || c == 'D') c = '0'; else if (c == 'I' || c == 'l' || c == '|' || c == 'i') c = '1'; else if (c == 'S' || c == 's') c = '5'; else if (c == 'B') c = '8'; if (char.IsDigit(c)) sb.Append(c); public static int Signed(string s, string before) var m = Regex.Match(s ?? "", @"([+-]?)\s?([0-9OoIlSB][0-9OoIlSB,.]*)\s*" + Regex.Escape(before ?? ""), RegexOptions.IgnoreCase); if (!m.Success) return 0; if (!int.TryParse(Digits(m.Groups[2].Value), out v)) return 0; return m.Groups[1].Value == "-" ? -v : v; public static bool Fraction(string s, out int cur, out int max, bool whole = false) if (s == null) return false; var m = Regex.Match(s, whole ? @"(?<![A-Za-z0-9])([0-9OoIlSB]{1,7})\s*[/|]\s*([0-9OoIlSB]{1,7})(?![0-9])" : @"([0-9OoIlSB]{1,7})\s*[/|]\s*([0-9OoIlSB]{1,7})"); if (!m.Success) return false; return int.TryParse(Digits(m.Groups[1].Value), out cur) && int.TryParse(Digits(m.Groups[2].Value), out max); public static bool Money(string s, out double value) if (s == null) return false; const string D = "[0-9OoIlS]"; var m = Regex.Match(s, @"\$\s?(" + D + "{1,3}(?:[.,]" + D + @"{1,2})?)\s?([KMBT])(?![A-Za-z])", RegexOptions.IgnoreCase); if (m.Success) { num = m.Groups[1].Value.Replace(',', '.'); suffix = m.Groups[2].Value; } m = Regex.Match(s, @"\$\s?(" + D + "{1,3}(?:," + D + @"{3})*)(?![0-9OoIlS.,])", RegexOptions.IgnoreCase); if (!m.Success) return false; num = m.Groups[1].Value.Replace(",", ""); var sb = new StringBuilder(); foreach (char ch in num) sb.Append(ch == '.' ? "." : Digits(ch.ToString())); if (!double.TryParse(sb.ToString(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v)) return false; switch (suffix.ToUpperInvariant()) case "K": v *= 1e3; break; case "M": v *= 1e6; break; case "B": v *= 1e9; break; case "T": v *= 1e12; break; public static int Duration(string s) if (string.IsNullOrEmpty(s)) return -1; var m = Regex.Match(s, @"(\d{1,2}):(\d{2}):(\d{2})"); if (m.Success) return int.Parse(m.Groups[1].Value) * 3600 + int.Parse(m.Groups[2].Value) * 60 + int.Parse(m.Groups[3].Value); m = Regex.Match(s, @"(?<![\d:.])(\d{1,3}):(\d{2})(?![\d:.])"); if (m.Success) return int.Parse(m.Groups[1].Value) * 60 + int.Parse(m.Groups[2].Value); int total = 0; bool any = false; string u0 = s.ToUpperInvariant().Replace("MLN", "MIN").Replace("M1N", "MIN").Replace("HRZ", "HR") .Replace("1-IP", "HR").Replace("1-IR", "HR").Replace("HQ", "HR").Replace("HÄ", "HR"); foreach (Match p in Regex.Matches(u0, @"(?<![\$\d.,])(?=[0-9OISB]{0,2}[0-9])([0-9OISB]{1,3})\s*(DAYS?|HRS?|HOURS?|MINS?|MINUTES?|SECS?|SECONDS?)(?![A-Z])")) if (!int.TryParse(Digits(p.Groups[1].Value), out n)) continue; string u = p.Groups[2].Value; if (u.StartsWith("D")) total += n * 86400; else if (u.StartsWith("H")) { total += n * 3600; hours = true; } else if (u.StartsWith("M")) total += n * 60; foreach (Match p in Regex.Matches(u0, @"(?<![A-Z0-9\$.,])([IOL]{1,2})\s*(HRS?|HOURS?)(?![A-Z])")) if (!int.TryParse(Digits(p.Groups[1].Value.Replace('L', 'I')), out n) || n == 0) continue; public static int Countdown(string s) if (string.IsNullOrEmpty(s)) return -1; if (Regex.IsMatch(s, @"\d:\d{2}")) return Duration(s); int total = 0; bool any = false; foreach (Match p in Regex.Matches(s, @"(?<![A-Za-z0-9])([0-9lIO]{1,3})\s*([dhms])(?![a-z])")) if (!int.TryParse(Digits(p.Groups[1].Value), out n)) continue; switch (p.Groups[2].Value) { case "d": total += n * 86400; break; case "h": total += n * 3600; break; case "m": total += n * 60; break; default: total += n; break; } return any ? total : Duration(s); public static string Short(int seconds) if (seconds < 60) return Math.Max(0, seconds) + " s"; if (seconds < 3600) return (seconds / 60) + " min"; if (seconds < 86400) return (seconds / 3600) + " h" + (seconds % 3600 >= 60 ? " " + (seconds % 3600 / 60) + " min" : ""); return (seconds / 86400) + " d" + (seconds % 86400 >= 3600 ? " " + (seconds % 86400 / 3600) + " h" : ""); public static int RegenTimer(string s) var m = Regex.Match(s ?? "", @"\+\s*(\d)\s*(\d{1,2})\s*[:.]\s*(\d{2})"); return m.Success ? int.Parse(m.Groups[2].Value) * 60 + int.Parse(m.Groups[3].Value) : -1; public static int FirstInt(string s) var m = Regex.Match(s ?? "", @"\d+"); return m.Success && int.TryParse(m.Value, out v) ? v : -1;