AFK Macro

Idle Mafia Macro/Source/src/Vision.cs

Vision.cs

Pictures, the OCR and reading numbers from text · 667 lines · 28 KB

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
#if !LINUX
using System.Runtime.InteropServices.WindowsRuntime;
#endif
using System.Text;
using System.Text.RegularExpressions;
#if !LINUX
using Windows.Graphics.Imaging;
using Windows.Media.Ocr;
#endif
namespace IdleMafiaBot
{
sealed class Pixels
{
public byte[] Bytes;
GCHandle pin;
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)
{
lock (spare)
{
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);
return p;
}
public void Back()
{
lock (spare)
{
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; }
spare.Remove(least);
least.Free();
spare.Add(this);
}
}
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;
readonly byte[] px;
readonly int stride;
Pixels held;
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);
stride = data.Stride;
int size = stride * bmp.Height;
if (capture && size >= 4 * 1024 * 1024)
{
held = Pixels.Rent(size);
px = held.Bytes;
}
else px = new byte[size];
Marshal.Copy(data.Scan0, px, 0, size);
bmp.UnlockBits(data);
if (held == null) { Bitmap = bmp; GC.SuppressFinalize(this); return; }
Bitmap = new Bitmap(bmp.Width, bmp.Height, stride, PixelFormat.Format32bppArgb, held.At) { Tag = bmp.Tag };
bmp.Dispose();
}
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;
return copy;
}
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)
{
var c = Pixel(x, y);
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)
{
int hit = 0, n = 0;
for (int x = x1; x <= x2; x += 2) { n++; if (Near(x, y, rgb, tol)) hit++; }
return n == 0 ? 0 : (double)hit / n;
}
public bool LooksEmpty()
{
int white = 0, n = 0;
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;
n++;
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;
n++;
if (Math.Abs(px[k + 2] - r0) + Math.Abs(px[k + 1] - g0) + Math.Abs(px[k] - b0) > 30) detail++;
}
return detail * 200 >= n;
}
public void Dispose()
{
Bitmap.Dispose();
GC.SuppressFinalize(this);
var h = System.Threading.Interlocked.Exchange(ref held, null);
if (h != null) h.Back();
}
~Frame()
{
try { Bitmap.Dispose(); } catch (Exception) { }
var h = System.Threading.Interlocked.Exchange(ref held, null);
if (h != null) h.Free();
}
}
public sealed class OcrLine
{
public string Text;
public Rectangle Box;
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 }
public static class Ocr
{
#if !LINUX
static OcrEngine engine;
#endif
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; } }
#if LINUX
const int MaxSide = 10000;
public static string Check()
{
lock (gate)
{
if (engineChecked) return problem;
engineChecked = true;
problem = Bridge.Check();
if (problem == null) language = "the Linux reader";
return problem;
}
}
public static string Warning { get { return warning; } }
#else
static int MaxSide { get { return (int)OcrEngine.MaxImageDimension; } }
public static string Check()
{
lock (gate)
{
if (engineChecked) return problem;
engineChecked = true;
try
{
var e = Create();
problem = Unusable(e == null ? null : e.RecognizerLanguage);
if (problem == null) { engine = e; language = e.RecognizerLanguage.DisplayName; warning = NotEnglish(e.RecognizerLanguage); }
}
catch (Exception ex)
{
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;
}
return problem;
}
}
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;
return null;
}
static OcrEngine Create()
{
var e = OcrEngine.TryCreateFromLanguage(new Windows.Globalization.Language("en-US"));
if (e != null) return e;
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");
}
static string Have()
{
try
{
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)) + ".";
}
catch { return ""; }
}
static OcrEngine Engine
{
get
{
string p = Check();
if (p != null) throw new InvalidOperationException(p);
return engine;
}
}
#endif
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);
if (Bridge.On)
{
using (var pic = Prepare(f, area, zoom, prep))
foreach (var it in Bridge.Ask(Drawn(pic), pic.Picture.Width, pic.Picture.Height))
lines.Add(new OcrLine
{
Text = it.Text,
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))
});
return lines;
}
#if !LINUX
OcrResult result;
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)
{
var r = w.BoundingRect;
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);
}
lines.Add(new OcrLine
{
Text = line.Text,
Box = new Rectangle(area.X + (int)(x1 / zoom), area.Y + (int)(y1 / zoom),
(int)((x2 - x1) / zoom), (int)((y2 - y1) / zoom))
});
}
#endif
return lines;
}
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); }
return sb.ToString();
}
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)
{
#if LINUX
var b = pic.Picture;
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);
b.UnlockBits(d);
#endif
return pic.Held.Bytes;
}
internal sealed class Prepared : IDisposable
{
public Bitmap Picture;
public Pixels Held;
public void Dispose() { Picture.Dispose(); Held.Back(); }
}
#if !LINUX
static OcrResult Recognize(Prepared pic)
{
lock (gate)
{
var bmp = pic.Picture;
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;
}
}
#endif
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.Clear(Color.White);
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++)
{
int i = x * 4;
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)
: (max <= 95));
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);
}
big.UnlockBits(data);
return pic;
}
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]++; }
}
int face = 0;
for (int v = 1; v < 256; v++) if (count[v] > count[face]) face = v;
int far = 0, beyond = 0;
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];
far = 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++)
{
int i = x * 4;
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);
if (Bridge.On)
{
using (var pic = Prepare(f, area, zoom, prep))
foreach (var it in Bridge.Ask(Drawn(pic), pic.Picture.Width, pic.Picture.Height))
{
string text = it.Text;
int from = 0;
foreach (var word in text.Split(' '))
{
if (word.Length > 0)
words.Add(new OcrLine
{
Text = word,
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))
});
from += word.Length + 1;
}
}
return words;
}
#if !LINUX
OcrResult result;
using (var pic = Prepare(f, area, zoom, prep)) result = Recognize(pic);
foreach (var line in result.Lines)
foreach (var w in line.Words)
{
var r = w.BoundingRect;
words.Add(new OcrLine
{
Text = w.Text,
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)),
});
}
#endif
return words;
}
}
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 ?? "")
{
char c = ch;
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);
}
return sb.ToString();
}
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;
int v;
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)
{
cur = max = 0;
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)
{
value = 0;
if (s == null) return false;
const string D = "[0-9OoIlS]";
string num, suffix = "";
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; }
else
{
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()));
double v;
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;
}
value = v;
return true;
}
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");
bool hours = false;
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])"))
{
int n;
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;
else total += n;
any = true;
}
if (!hours)
foreach (Match p in Regex.Matches(u0, @"(?<![A-Z0-9\$.,])([IOL]{1,2})\s*(HRS?|HOURS?)(?![A-Z])"))
{
int n;
if (!int.TryParse(Digits(p.Groups[1].Value.Replace('L', 'I')), out n) || n == 0) continue;
total += n * 3600;
any = true;
break;
}
return any ? total : -1;
}
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])"))
{
int n;
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; }
any = true;
}
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+");
int v;
return m.Success && int.TryParse(m.Value, out v) ? v : -1;
}
}
}