AFK Macro

Idle Mafia Macro/Source/src/Bot.cs

Bot.cs

The work rounds: jobs, heists, fights, bosses, bank and more · 9,825 lines · 482 KB

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
namespace IdleMafiaBot
{
sealed class Counters
{
public int Jobs, Helps, Fights, Wins, Losses, Deposits, OpsCollected, OpsStarted, BossHits, Rewards, Crates;
public int TakedownAttacks, StaminaGiven, Bought, Collected, Hired, Rerolls;
public int BossesBeaten, Properties;
public int HeistsStarted;
public double CrewCash;
public double FightCash;
public Counters Copy() { return (Counters)MemberwiseClone(); }
public string Text()
{
return string.Join(";", typeof(Counters).GetFields().Select(f => f.Name + "=" + Convert.ToString(f.GetValue(this), CultureInfo.InvariantCulture)));
}
public static Counters Parse(string text)
{
var c = new Counters();
foreach (var part in (text ?? "").Split(';'))
{
int eq = part.IndexOf('=');
if (eq <= 0) continue;
var f = typeof(Counters).GetField(part.Substring(0, eq).Trim());
double v;
if (f == null || !double.TryParse(part.Substring(eq + 1), NumberStyles.Float, CultureInfo.InvariantCulture, out v) || v < 0) continue;
if (f.FieldType == typeof(int)) f.SetValue(c, (int)Math.Min(int.MaxValue, v));
else if (f.FieldType == typeof(double)) f.SetValue(c, v);
}
return c;
}
public void Add(Counters now, Counters since)
{
foreach (var f in typeof(Counters).GetFields())
{
if (f.FieldType == typeof(int)) f.SetValue(this, (int)f.GetValue(this) + Math.Max(0, (int)f.GetValue(now) - (int)f.GetValue(since)));
else if (f.FieldType == typeof(double)) f.SetValue(this, (double)f.GetValue(this) + Math.Max(0, (double)f.GetValue(now) - (double)f.GetValue(since)));
}
}
public override string ToString()
{
return string.Format("jobs {0} | heist helps {1} | fights {2} ({3} won, {4} lost) | boss hits {5} | deposits {6} | operations {7} collected, {8} started | rewards {9} | crates {10}",
Jobs, Helps, Fights, Wins, Losses, BossHits, Deposits, OpsCollected, OpsStarted, Rewards, Crates)
+ (Bought > 0 || Collected > 0 ? string.Format(" | bought {0} | collected {1}", Bought, Collected) : "")
+ (Hired > 0 || Rerolls > 0 ? string.Format(" | crew hires {0} | rerolls {1}", Hired, Rerolls) : "")
+ (HeistsStarted > 0 ? " | heists started " + HeistsStarted : "")
+ (TakedownAttacks > 0 || StaminaGiven > 0 ? string.Format(" | Takedown attacks {0} | stamina given {1}", TakedownAttacks, StaminaGiven) : "");
}
}
sealed class RegenGuess
{
public int Period, Max = -1;
int prevTick = -1, value = -1, tick = -1;
DateTime at;
public bool HasValue { get { return value >= 0; } }
public void Note(int v, int max, int t, DateTime now)
{
if (v < 0 || max <= 0) return;
if (v < max && t > 0)
{
if (prevTick > 0 && t > prevTick + 2) Period = Math.Min(900, Math.Max(Period, t));
prevTick = t;
}
else prevTick = -1;
value = v; Max = max; tick = v >= max ? 0 : t; at = now;
}
public void Forget() { value = -1; prevTick = -1; }
public int Guess(DateTime now)
{
if (value < 0) return -1;
if (value >= Max || Period <= 0) return value;
int next = tick >= 0 ? tick : Period;
double s = (now - at).TotalSeconds;
int gained = s < next ? 0 : 1 + (int)((s - next) / Period);
return Math.Min(Max, value + gained);
}
}
sealed partial class Bot
{
public readonly Settings S;
public readonly Counters Count = new Counters();
public readonly Discord Discord;
public readonly StatsCard Stats;
public readonly RemoteControl Remote;
public List<JobInfo> Catalog;
public volatile List<JobInfo> CatalogView;
readonly string dir, logFile;
string catalogFile;
static readonly object logLock = new object();
Game game;
Thread thread;
volatile bool stop;
volatile bool scanRequested, checkRequested;
volatile bool finishing, keepRoll, stopAgain;
volatile int finishEnd;
bool Halted { get { return HaltedNow(stop, finishing && !stopAgain, Environment.TickCount, finishEnd); } }
internal static bool HaltedNow(bool stop, bool finishing, int now, int end) { return stop && !(finishing && end - now > 0); }
public event Action<string> Logged;
public event Action<string> StateChanged;
public event Action<Header> HeaderRead;
public event Action<string, string> InfoChanged;
public event Action<List<OpSlot>> OpsRead;
public event Action CatalogChanged;
DateTime nextOps, nextBosses, nextPlaytime, nextContracts, nextCrates, nextHeists, nextFights, nextBank, nextPoints, nextJobs, nextLearn, nextGameStats, lastBank;
int level = -1, repeats, frontFails, logWrites;
int xpNextLast = -1, xpNextSeen = -1;
string lastError;
bool learnedThisRun, waitingForUser, logBroken, catalogBuiltIn;
volatile bool levelledStopped;
readonly HashSet<string> firstSeen = new HashSet<string>();
void SnapshotOnce(string what)
{
if (firstSeen.Add(what)) game.Snapshot(what, 0, true);
}
Header last;
public Bot(Settings s, string dir)
{
S = s;
this.dir = dir;
catalogFile = Path.Combine(dir, "jobs.txt");
logFile = Path.Combine(dir, "IdleMafiaBot.log");
memory = new FightMemory(MemoryFile(s.Account));
catalogBuiltIn = !File.Exists(catalogFile);
Catalog = GameData.LoadCatalog(catalogFile);
CatalogView = Catalog.Select(j => j.Copy()).ToList();
level = s.LastLevel;
crewUnequipped = s.CrewGearOff;
ownOpenBefore = s.CrewOpenBefore;
if (s.ScreenFirst) RobloxWindow.ScreenFirst = true;
Discord = new Discord(s, Log);
HeaderRead += NoteTicks;
Stats = new StatsCard(this, s, Log, SaveSettings);
Remote = new RemoteControl(s, Log, SaveSettings, Stats);
}
public bool Running { get { return thread != null && thread.IsAlive; } }
public bool Once;
public void Start() { if (Program.Reroller) StartRerollOnly(); else Begin(false); }
public void RequestScan() { Begin(true); }
public void RequestCheck() { Begin(false, true); }
public bool CheckOrScan { get { return scanRequested || checkRequested; } }
void Begin(bool scan, bool check = false)
{
if (Running) return;
scanRequested = scan;
checkRequested = check;
stop = false;
finishing = keepRoll = stopAgain = false;
thread = new Thread(Run) { IsBackground = true, Name = "bot" };
thread.SetApartmentState(ApartmentState.MTA);
thread.Start();
}
public void Stop() { Stop(false); }
public void Stop(bool keep)
{
if (keep) keepRoll = true;
if (stop) { stopAgain = true; finishing = false; }
stop = true; stopWhy = null;
List<Bot> others;
lock (partners) others = partners.ToList();
foreach (var p in others) p.Stop(keep);
}
public bool StopPressed { get { return stop; } }
public bool OwnRollRuns
{
get
{
var r = autoRun;
if (r != null && r.Running) return true;
List<Bot> others;
lock (partners) others = partners.ToList();
return others.Any(p => p.OwnRollRuns);
}
}
volatile Turns turns;
readonly List<Bot> partners = new List<Bot>();
Bot lead;
volatile string who;
DateTime idleUntil;
bool Rejoins { get { return S.Rejoin && turns == null; } }
static readonly TimeSpan TurnLength = TimeSpan.FromMinutes(3);
DateTime turnStarted = DateTime.MaxValue;
bool TurnUp(string task)
{
return turns != null && task != "bank" && DateTime.UtcNow - turnStarted >= TurnLength;
}
void SayTurnUp(List<string> left)
{
if (left.Count == 0) return;
Log("Turn: its " + TurnLength.TotalMinutes + " minutes are up - " + string.Join(", ", left) + " wait for " + S.Account + "'s next turn");
}
public bool InTurns { get { return turns != null; } }
public void StartAll(List<Instance> others)
{
if (Running) return;
List<Bot> made;
lock (partners)
{
partners.Clear();
foreach (var i in others.Where(i => i.Name != null && i.Handle != pinned)) partners.Add(Partner(i));
made = partners.ToList();
}
if (made.Count == 0) { Start(); return; }
who = S.Account;
var t = new Turns(new[] { this }.Concat(made));
foreach (var p in made) p.turns = t;
turns = t;
Log("Playing " + (made.Count + 1) + " accounts in turns: " + string.Join(", ", new[] { S.Account }.Concat(made.Select(p => p.S.Account)))
+ " - one round each, the fullest bars first. Rejoining is off while it does.");
foreach (var p in made) p.Begin(false);
Begin(false);
}
public void StartAllWhenSeen()
{
new Thread(() =>
{
for (var until = DateTime.UtcNow.AddMinutes(2); !Running && DateTime.UtcNow < until; Thread.Sleep(1000))
{
var seen = InstancesSeen;
if (seen != null && seen.Count(i => i.Name != null) > 1 && seen.Any(i => i.Name != null && i.Handle == pinned)) { StartAll(seen); return; }
}
if (Running) return;
Log("Turns: the other accounts' Roblox windows weren't read again - " + (S.Account.Length > 0 ? S.Account : "this account") + " plays alone");
Start();
}) { IsBackground = true, Name = "turns again" }.Start();
}
Bot Partner(Instance i)
{
string ini = Accounts.Ini(dir, i.Name);
bool copy = !File.Exists(ini) && S.Account.Length > 0;
var theirs = new Settings();
theirs.TakeAccountFields(copy ? Accounts.NewFrom(S) : Settings.Load(ini));
theirs.Account = i.Name;
var p = new Bot(theirs, dir) { lead = this, who = i.Name, pinned = i.Handle };
p.Discord.Pc = S;
p.accountIniAt = IniTime(ini);
p.FromLead();
p.UseAccountFiles();
p.Logged += line => { var h = Logged; if (h != null) h(line); };
if (copy)
{
p.Log("Account: " + i.Name + " is new here: it starts with " + S.Account + "'s settings and the game's job list.");
p.SaveSettings();
}
return p;
}
DateTime accountIniAt;
static DateTime IniTime(string ini)
{
try { return File.Exists(ini) ? File.GetLastWriteTimeUtc(ini) : DateTime.MinValue; }
catch (Exception) { return DateTime.MinValue; }
}
void SavedOwn() { if (lead != null) accountIniAt = IniTime(Accounts.Ini(dir, S.Account)); }
void FromLead()
{
var l = lead;
if (l == null) return;
string ini = Accounts.Ini(dir, S.Account), name = S.Account;
var at = IniTime(ini);
if (at != DateTime.MinValue && at != accountIniAt)
{
var theirs = Settings.Load(ini);
if (theirs.Unreadable == null)
{
S.TakeAccountFields(theirs);
S.Account = name;
Log("Settings: " + name + "'s own settings were changed - they're used from this turn on");
}
accountIniAt = at;
}
bool screenFirst = S.ScreenFirst;
S.TakePcFields(l.S);
S.ScreenFirst |= screenFirst;
S.StatsLink = "";
S.RemoteControl = false;
}
void UseAccountFiles()
{
catalogFile = Accounts.Jobs(dir, S.Account);
catalogBuiltIn = !File.Exists(catalogFile);
Catalog = GameData.LoadCatalog(catalogFile);
CatalogView = Catalog.Select(j => j.Copy()).ToList();
level = S.LastLevel;
}
int BarsNeed()
{
var h = Instances.HeaderIn(pinned, S);
if (h == null || (h.Energy < 0 && h.Stamina < 0)) h = last;
return h == null ? 0 : FullestBar(h);
}
internal static int FullestBar(Header h)
{
int e = h.EnergyMax > 0 && h.Energy >= 0 ? h.Energy * 100 / h.EnergyMax : 0;
int s = h.StaminaMax > 0 && h.Stamina >= 0 ? h.Stamina * 100 / h.StaminaMax : 0;
return Math.Min(100, Math.Max(e, s));
}
void Turn()
{
var t = turns;
if (t == null) { Tick(); return; }
if (DateTime.UtcNow < idleUntil) { Sleep(500); return; }
if (WaitingInTurns()) return;
int need = BarsNeed();
if (!t.Take(this, need, () => stop, 0))
{
State("Another account is playing - " + (S.Account.Length > 0 ? S.Account : "this one") + " waits for its turn");
if (!t.Take(this, need, () => stop, 15000)) return;
}
FromLead();
turnStarted = DateTime.UtcNow;
try
{
if (WaitingInTurns()) return;
if (game.Win.Find() && !game.Win.Minimized && !game.Win.IsForeground)
{
State("Bringing " + S.Account + "'s Roblox window to the front for its turn");
game.Win.Front();
Sleep(400);
}
Tick();
}
finally { t.Pass(this); }
}
bool WaitingInTurns()
{
if (!S.WaitWhileBusy || !game.Win.UserActiveWithin(S.IdleSeconds * 1000)) return false;
State("Waiting until you stop using the mouse/keyboard (several accounts in turns)");
Sleep(1500);
return true;
}
public event Action<List<Instance>> InstancesChanged;
public List<Instance> InstancesSeen { get; private set; }
volatile IntPtr pinned;
public IntPtr Pinned { get { return pinned; } }
public bool PlayOn(Instance which)
{
if (Running || which == null || which.Name == null) return false;
pinned = which.Handle;
var seen = InstancesSeen;
RobloxWindow.Taken = seen == null ? new IntPtr[0] : seen.Where(i => i.Handle != which.Handle).Select(i => i.Handle).ToArray();
lock (accountLock)
{
if (S.Account.Length == 0) Adopt(which.Name);
else if (!Accounts.Same(which.Name, S.Account) && !string.Equals(Profile.Key(which.Name), Profile.Key(S.Account), StringComparison.OrdinalIgnoreCase)) SwitchAccount(which.Name);
}
var kept = KeptHeader(which.Name);
if (kept != null)
{
last = kept;
var hr = HeaderRead;
if (hr != null) hr(kept);
}
lookNow.Set();
return true;
}
public bool ReadWindow(Instance which)
{
if (Running || which == null) return false;
if (which.Name != null) return true;
var g = new Game(msg => { }, () => false);
g.Win.Pinned = which.Handle;
g.Win.NoFallback = true;
Header head = null;
try
{
if (!g.Win.Find() || g.Win.Minimized || !g.Win.Front()) { Log("Account: couldn't bring that Roblox window to the front - is it minimized?"); return false; }
for (int tries = 0; tries < 3 && head == null; tries++)
{
Thread.Sleep(tries == 0 ? 400 : 800);
using (var f = g.Win.Capture())
{
var v = g.ViewOf(f);
if (v.Problem != null) continue;
g.SeedMax(S.BarMax);
var h = g.ReadHeader(f, v);
if (h != null && Accounts.Plausible(h.Name)) head = h;
}
}
}
catch (Exception) { }
finally { g.Win.EndAct(true, false); }
if (head == null) { Log("Account: couldn't read the name in that Roblox window - is Idle Mafia Game open in it, with its top bar showing?"); return false; }
string name = Accounts.Tidy(head.Name.Trim());
foreach (var i in (InstancesSeen ?? new List<Instance>()).Where(i => i.Handle == which.Handle).Concat(new[] { which }))
{
i.Name = name;
i.Head = head;
}
KeepProfile(name);
KeepHeader(head);
var changed = InstancesChanged;
if (changed != null && InstancesSeen != null) changed(InstancesSeen);
return true;
}
readonly Dictionary<string, Header> headers = new Dictionary<string, Header>(StringComparer.OrdinalIgnoreCase);
readonly AutoResetEvent lookNow = new AutoResetEvent(false);
void KeepHeader(Header h)
{
if (h == null || !Accounts.Plausible(h.Name)) return;
lock (headers) headers[Profile.Key(h.Name.Trim())] = h;
}
Header KeptHeader(string name)
{
Header h;
lock (headers) return headers.TryGetValue(Profile.Key(name.Trim()), out h) ? h : null;
}
void LookForInstances(Game reader)
{
var before = InstancesSeen;
var now = Instances.Read(before, S, dir);
if (Instances.SameWindows(before, now)) return;
InstancesSeen = now;
if (now.Count > 1)
{
foreach (var i in now.Where(i => i.Name != null)) { KeepProfile(i.Name); KeepHeader(i.Head); }
if (pinned == IntPtr.Zero || !now.Any(i => i.Handle == pinned))
{
var own = Instances.Pick(now, S.Account, pinned != IntPtr.Zero);
if (own == null || !PlayOn(own))
if (pinned != IntPtr.Zero && !now.Any(i => i.Handle == pinned)) { pinned = IntPtr.Zero; RobloxWindow.Taken = new IntPtr[0]; }
}
else RobloxWindow.Taken = now.Where(i => i.Handle != pinned).Select(i => i.Handle).ToArray();
}
else { pinned = IntPtr.Zero; RobloxWindow.Taken = new IntPtr[0]; }
reader.Win.Pinned = pinned;
var h = InstancesChanged;
if (h != null) h(now);
}
void KeepProfile(string name)
{
if (Directory.Exists(Accounts.Folder(dir, name))) return;
try
{
Directory.CreateDirectory(Accounts.Folder(dir, name));
Log("Account: " + name + " is open in a Roblox window (accounts\\" + Profile.Key(name) + ")");
}
catch (Exception e) { Log("Couldn't make " + name + "'s folder: " + e.Message); }
}
public void StartWatching()
{
var watcher = new Thread(() =>
{
Native.RealPixels();
var reader = new Game(msg => { }, () => false);
var win = reader.Win;
win.ScreenOnly = true;
string seen = null;
while (true)
{
try
{
if (!Running)
{
LookForInstances(reader);
win.Pinned = pinned;
string state = !win.Find() ? "not running" : win.Minimized ? "minimized"
: RobloxWindow.ScreenFirst && !win.InSight() ? "covered" : null;
Header h = null;
if (state == null)
using (var f = win.Capture())
{
var v = reader.ViewOf(f);
if (v.Problem != null) state = "unreadable";
else { reader.SeedMax(S.BarMax); h = reader.ReadHeader(f, v); }
}
NoteScreenFirst();
if ((state ?? "found") != seen)
{
seen = state ?? "found";
Info("roblox", seen);
}
if (h != null)
{
KeepHeader(h);
bool other = !string.IsNullOrWhiteSpace(h.Name) && OtherAccount(h.Name);
if (h.Level > 0 && !other && TakeLevel(h.Level)) { if (level > 0 && h.Level > level) levelledStopped = true; level = h.Level; S.LastLevel = level; }
var before = last;
last = h;
var hr = HeaderRead;
if (hr != null) hr(h);
if (!GuessBar(energyRegen, DateTime.UtcNow, ref h.Energy, ref h.EnergyMax) && before != null) { h.Energy = before.Energy; h.EnergyMax = before.EnergyMax; }
}
}
}
catch (Exception e)
{
if (MemoryShort(e)) { SayMemory(); Thread.Sleep(60000); }
}
lookNow.WaitOne(Running ? 5000 : 3000);
}
}) { IsBackground = true, Name = "watch" };
watcher.Start();
}
public void Log(string msg)
{
var w = who;
string line = DateTime.Now.ToString("HH:mm:ss") + " " + (w != null ? "[" + w + "] " : "") + msg, problem = null;
lock (logLock)
{
if (logWrites++ % 200 == 0)
try
{
var fi = new FileInfo(logFile);
if (fi.Exists && fi.Length > 5 * 1024 * 1024)
{
string old = Path.Combine(dir, "IdleMafiaBot.1.log");
File.Delete(old);
File.Move(logFile, old);
}
}
catch (Exception) { }
try { File.AppendAllText(logFile, DateTime.Now.ToString("yyyy-MM-dd ") + line + Environment.NewLine); }
catch (Exception e) { if (!logBroken) problem = DateTime.Now.ToString("HH:mm:ss") + " Can't write the log file: " + e.Message; logBroken = true; }
}
var h = Logged;
if (h != null) { h(line); if (problem != null) h(problem); }
}
public void LogDetails(string text)
{
lock (logLock)
try { File.AppendAllText(logFile, text + Environment.NewLine); }
catch (Exception) { }
}
void State(string s) { var h = StateChanged; if (h != null) h(s); }
void Info(string key, string value) { var h = InfoChanged; if (h != null) h(key, value); }
void Run()
{
Native.RealPixels();
game = new Game(Log, () => Halted) { ProblemsDir = Path.Combine(dir, "problems"), CollectFights = CollectFights };
game.Win.Pinned = pinned;
game.Win.NoFallback = turns != null;
game.Tag = turns != null ? who : null;
idleUntil = DateTime.MinValue;
game.SeedMax(S.BarMax);
game.UserBack = () => acting && S.WaitWhileBusy && game.Win.UserInputSinceBot();
game.Saved = (reason, file) => Discord.Problem("picture " + reason, "⚠️ Problem: " + reason,
file != null ? "The bot carries on. The picture is in its problems folder too." : "The bot carries on. The picture is in its problems folder (not sent: another window was over Roblox).",
file != null ? (Func<byte[]>)(() => Discord.JpgOf(file)) : null, true, 180);
game.Failed = TaskFailed;
ShowStuck();
DiscordStart();
firstSeen.Clear();
redone.Clear();
oneJobSaid = null;
DateTime now = DateTime.UtcNow;
nextOps = nextBosses = nextPlaytime = nextContracts = nextCrates = nextHeists = nextFights = nextBank = nextPoints = nextJobs = nextLearn = nextGameStats = now;
nextBosses = BossesDoneUntil(now);
ResetExpState();
ResetMenuNumbers();
fullClock.Reset();
ResetFamily(now);
LoadDefence();
nextEquip = nextShop = nextSafehouse = nextReroll = nextMarket = now;
nextUpgrade = nextCrew = nextCrewSlot = now;
if (levelledStopped) { levelledStopped = false; PropertiesLevelUp(); }
propRecheck = true;
nextBriefcases = nextEvents = nextGlobalBoss = nextKnock = now; ResetGlobalBoss(); badgeSig = eventsSig = null; eventsOn.Clear(); briefcasesSaid = null;
rerollCrew = null; rerollSaid = null;
lastBank = DateTime.MinValue;
learnedThisRun = false; foldDue = true;
lastError = null;
frontFails = 0;
rejoiner = new Rejoiner();
#if LINUX
rejoiner.Playing(now);
#endif
dialogSeen = null;
dialogUp = disconnectHint = robloxUsed = false;
robloxUsedAt = DateTime.MinValue;
focusBack = IntPtr.Zero;
gameInput = now;
tutorialSince = now;
string ocr = Ocr.Check();
Log((scanRequested ? "Scanning the game screens" : checkRequested ? "Checking your game" : "Bot started") + " - version " + Program.Version
+ (ocr == null ? ", reading text in " + Ocr.Language : ""));
bool play = !scanRequested && !checkRequested, needUser = false;
if (play && autoRun == null) RestoreRun();
try
{
if (ocr != null) { Log("Problem: " + ocr); Info("ocr", ocr); stopWhy = ocr; return; }
while (!stop)
{
try { Turn(); lastError = null; }
catch (StopException) { break; }
catch (NeedUserException e) { Log(e.Message); stop = true; stopWhy = e.Message; needUser = true; }
catch (UserBusyException)
{
if (!waitingForUser) Log(S.WaitWhileBusy ? "You started using the PC - stopped for now, waiting until you've been idle for " + S.IdleSeconds + " s"
: "The mouse moved during a click - trying again in a moment");
waitingForUser = true;
State("Waiting until you stop using the mouse/keyboard");
if (S.WaitWhileBusy) Sleep(S.IdleSeconds * 1000);
else
{
Sleep(1500);
Native.POINT a, b;
Native.GetCursorPos(out a);
int since = Environment.TickCount, start = since;
while (!stop && Environment.TickCount - since < 1500 && Environment.TickCount - start < 30000)
{
Thread.Sleep(250);
Native.GetCursorPos(out b);
if (Math.Abs(b.X - a.X) > 3 || Math.Abs(b.Y - a.Y) > 3) { a = b; since = Environment.TickCount; }
}
}
}
catch (WindowChangedException e)
{
repeats = e.Message == lastError ? repeats + 1 : 0;
lastError = e.Message;
if (repeats == 0) Log("Stopped this round: " + e.Message);
State("Stopped this round: " + e.Message + " - looking again shortly");
if (e.Message == Game.Disconnected) disconnectHint = true;
game.ForgetView();
Sleep(repeats > 2 ? 60000 : 10000);
}
catch (OutOfMemoryPause e)
{
MemoryShort(e.InnerException);
WaitForMemory(SayMemory());
}
catch (Exception e)
{
bool memory = MemoryShort(e);
repeats = e.Message == lastError ? repeats + 1 : 0;
lastError = e.Message;
if (repeats % 20 == 0) Log("Problem: " + e.Message + (repeats > 0 ? " (again, " + (repeats + 1) + " times)" : ""));
if (!memory) SnapshotOnce("error " + e.Message);
if (repeats == 3) Discord.Problem("error " + e.Message, "⚠️ The same problem keeps coming back", ProblemReport.NoUser(e.Message), Shot, true, 180);
if (memory) WaitForMemory(SayMemory());
else Sleep(repeats > 5 ? 15000 : 3000);
}
}
if (play && !needUser && !keepRoll) StopRollAtEnd();
}
finally
{
RerollOnly = false;
FightOnly = false;
try { FoldTotals(); } catch (Exception) { }
var t = turns;
if (t != null)
{
t.Leave(this);
if (lead == null)
{
List<Bot> others;
lock (partners) others = partners.ToList();
foreach (var p in others) if (!p.stop) p.Stop(keepRoll || needUser);
foreach (var p in others) { var th = p.thread; if (th != null) th.Join((RollStopSeconds + 5) * 1000); }
lock (partners) partners.Clear();
}
turns = null;
}
Log("Bot stopped");
if (lead == null) who = null;
if (stopWhy != null) { var said = StoppedMessage(stopWhy); Discord.Problem("stopped", said[0], said[1], Shot, false, 0); }
stopWhy = null;
if (lead != null) Discord.Close();
State("Stopped");
}
}
internal string[] StoppedMessage(string why)
{
if (lead == null) return new[] { "\U0001F6D1 The bot stopped", why + "\nPress Start in the bot's window to go on." };
return new[] { "\U0001F6D1 " + S.Account + " stopped", why + "\nThe other accounts play on. To play " + S.Account + " again: Stop, then Start in the bot's window." };
}
void Sleep(int ms)
{
int end = Environment.TickCount + ms;
while (!stop && end - Environment.TickCount > 0) Thread.Sleep(100);
}
readonly object memoryLock = new object();
DateTime memoryAt = DateTime.MinValue, memorySaid = DateTime.MinValue;
static Exception MemoryCause(Exception e)
{
for (int depth = 0; e != null && depth < 8; depth++)
{
if (e is OutOfMemoryException || e.HResult == unchecked((int)0x8007000E) || e.HResult == unchecked((int)0x80070008)) return e;
var all = e as AggregateException;
if (all != null && all.InnerExceptions.Count > 1) return all.InnerExceptions.Select(MemoryCause).FirstOrDefault(x => x != null);
e = e.InnerException;
}
return null;
}
static bool PictureFailed(Exception e)
{
for (int depth = 0; e != null && depth < 8; depth++, e = e.InnerException)
if (e is ArgumentException && (e.StackTrace ?? "").Contains("System.Drawing.")) return true;
return false;
}
internal bool MemoryShort(Exception e)
{
lock (memoryLock)
{
if (MemoryCause(e) == null && !(PictureFailed(e) && DateTime.UtcNow < memoryAt.AddMinutes(5))) return false;
memoryAt = DateTime.UtcNow;
return true;
}
}
bool SayMemory()
{
lock (memoryLock)
{
if (DateTime.UtcNow < memorySaid.AddMinutes(10)) return false;
memorySaid = DateTime.UtcNow;
}
string text;
try { text = Native.MemoryText(); }
catch (Exception) { text = "not known"; }
Log("Memory: " + text);
return true;
}
public void NoteMemory(Exception e)
{
try { if (MemoryShort(e)) SayMemory(); }
catch (Exception) { }
}
void WaitForMemory(bool say)
{
try
{
Ocr.Release();
System.Runtime.GCSettings.LargeObjectHeapCompactionMode = System.Runtime.GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
catch (Exception) { }
if (say) Log("The PC ran out of memory - the bot waits a minute before going on");
State("The PC ran out of memory - waiting a minute before going on");
Sleep(60000);
}
void Tick()
{
DiscordSummary();
if (!game.Win.Find())
{
dialogUp = false;
bool touched = rejoiner.Active ? Native.LastInputTick() != stepInput : game.Win.UserInputSinceBot();
var step = rejoiner.OnClosed(DateTime.UtcNow, touched, Rejoins, robloxUsed);
Away("closed", "⚠️ Roblox isn't open", Rejoins && (step != Rejoiner.Step.Wait || rejoiner.Active) ? "The bot starts the game again." : "The bot waits until Roblox is open again.");
if (TakeRejoinStep(step)) return;
State(rejoiner.Status ?? (RobloxWindow.All().Count > 0 ? S.Account + "'s Roblox window is closed - open it again, or press Stop and pick another account" : "Waiting for Roblox to be opened..."));
Sleep(4000);
return;
}
if (game.Win.IsForeground && game.Win.UserActiveWithin(5000)) { robloxUsed = true; robloxUsedAt = DateTime.UtcNow; }
Header h = null;
if (!game.Win.Minimized)
{
string unreadable = null;
bool leftAnimals = false;
Game.Disconnect dialog = null;
bool looked;
using (var f = game.Capture())
{
var v = game.ViewOf(f);
string other = RobloxWindow.OtherWindow();
robloxCheckUp = other != null;
if (other != null)
unreadable = "Roblox opened its own \"" + other + "\" window beside the game - a Roblox check only you can do: "
+ "join the game once with this account on a phone or another PC, then restart Roblox here";
else if (v.Problem != null)
{
unreadable = v.Language != null ? v.Problem : string.Format("Can't read the game in this {0}x{1} window: {2}", f.Width, f.Height, v.Problem);
NoteLoading(f);
if (DateTime.UtcNow >= nextRulingLook && !(S.WaitWhileBusy && game.Win.UserActiveWithin(15000)))
{
nextRulingLook = DateTime.UtcNow.AddMinutes(1);
if (game.CloseRulingFamily(f)) game.ForgetView();
else if (game.ReadAnimalsWindow(f) != null) leftAnimals = true;
}
}
else
{
h = game.ReadHeader(f, v);
NoteRobloxBar(game.RobloxBar);
if (S.Events && !game.RobloxBar) NoteBadges(f);
NoteMenuNumbers(f);
}
looked = LookForDisconnect(f, h == null || (h.Energy < 0 && h.Stamina < 0 && h.Health < 0), ref dialog);
}
if (leftAnimals)
{
Log("Closing the Animals window left open over the game's menu");
if (game.CloseAnimals()) { game.ForgetView(); return; }
}
NoteEmptyPictures();
if (looked) dialogUp = dialog != null;
if (unreadable != null || dialogUp) readOk = false;
if (unreadable != null && !dialogUp && !rejoiner.Active) Away("unreadable", "⚠️ The bot can't read the game", unreadable);
if (unreadable != null && !dialogUp && !robloxUsed && game.Win.Handle == readHandle) rejoiner.Playing(DateTime.UtcNow);
if (unreadable != null || dialogUp || (rejoiner.Active && !looked))
{
if (!looked || !Offline(dialog)) { State(rejoiner.Status ?? unreadable ?? Game.Disconnected); WatchRoblox(4000); }
return;
}
GameReadable();
if (awaySince != DateTime.MinValue) BackIn();
if (!string.IsNullOrWhiteSpace(h.Name) && NoteAccount(h.Name))
{
State("Another account is in Roblox: taking its own settings");
Sleep(2500);
return;
}
var before = last;
bool xpUp = h.Level <= 0 && h.XpNext > 0 && h.XpNext == xpNextLast && xpNextSeen > 0 && h.XpNext > xpNextSeen;
if (h.XpNext > 0 && h.XpNext == xpNextLast) xpNextSeen = h.XpNext;
xpNextLast = h.XpNext;
if (xpUp) { game.MenuChanged(); PropertiesLevelUp(); BossesLevelUp(level > 0 ? level + 1 : 0); }
if (h.Level > 0 && TakeLevel(h.Level))
{
if (h.Level > level) { game.MenuChanged(); PropertiesLevelUp(); if (level > 0) BossesLevelUp(h.Level); }
level = h.Level;
S.LastLevel = level;
LevelPing(h.Level, h.XpNext);
}
else if (Game.LevelFitsXp(level, h.XpNext) == false && h.XpNext == xpNextSeen)
{
Log("The level kept (" + level + ") doesn't fit the XP bar (" + h.XpNext + " to the next) - forgotten until it reads again");
level = -1;
S.LastLevel = -1;
}
S.BarMax = game.KnownMax;
last = h;
NoteBarsMoving(h, DateTime.UtcNow);
NoteHealth(h.Health, h.HealthMax, DateTime.UtcNow);
var hr = HeaderRead;
if (hr != null) hr(h);
GuessBar(staminaRegen, DateTime.UtcNow, ref h.Stamina, ref h.StaminaMax);
GuessBar(energyRegen, DateTime.UtcNow, ref h.Energy, ref h.EnergyMax);
NoteFullBars(h);
if (h.Energy < 0 && before != null) { h.Energy = before.Energy; h.EnergyMax = before.EnergyMax; }
}
if (scanRequested)
{
if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000))
{
State("Waiting until you stop using the mouse/keyboard (scan ready)");
Sleep(1500);
return;
}
if (!Act(delegate { try { Scan(); } catch (NeverPressException) { Log("--- Scan stopped: a press was refused ---"); } })) return;
scanRequested = false;
stop = true;
return;
}
if (checkRequested)
{
if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000))
{
State("Waiting until you stop using the mouse/keyboard (check ready)");
Sleep(1500);
return;
}
if (!Act(delegate { CheckGame(); })) return;
checkRequested = false;
stop = true;
return;
}
if (h == null)
{
if (game.Win.Handle == readHandle && readOk) rejoiner.Playing(DateTime.UtcNow);
if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000))
{
State("Roblox is minimized - restoring it once you stop using the mouse/keyboard");
Sleep(1500);
return;
}
if (frontFails == 0) Log("Roblox is minimized - restoring it");
Act(delegate { });
return;
}
if (SweepRobloxBar()) return;
if (RerollOnly)
{
if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000))
{
State("Waiting until you stop using the mouse/keyboard (only rerolling)");
Sleep(1500);
return;
}
bool goDue = DateTime.UtcNow >= nextReroll;
if (!goDue && !RollStopDue(DateTime.UtcNow))
{
State("Only rerolling: the next go at " + nextReroll.ToLocalTime().ToString("HH:mm"));
Sleep(2000);
return;
}
if (DisconnectBeforeRound()) return;
State("Working: only rerolling the crew");
Act(delegate
{
if (goDue) Log("Doing: only rerolling the crew" + (h.Cash >= 0 ? " (" + M(h.Cash) + " on hand)" : ""));
CloseEventWindow();
if (GearBackDue(DateTime.UtcNow)) RunTask("gear back", h);
if (RollStopDue(DateTime.UtcNow)) RunTask(RollStopTask, h);
if (!goDue) return;
var brk = rerollerBreak;
rerollerBreak = null;
if (Program.Reroller && brk != null)
foreach (var t in brk.Split(new[] { ", " }, StringSplitOptions.RemoveEmptyEntries))
if ((t == "jobs" && S.RerollerJobs) || (t == "give stamina" && S.RerollerGifts)) { Log("Doing: " + t + " (a full bar)"); RunTask(t, last ?? h); }
taskNow = "rerolls";
failedNow = neutralNow = false;
try { DoRerolls(true); if (!failedNow && !neutralNow) TaskWorked("rerolls"); }
finally { taskNow = null; if (failedNow) StreakCheck("rerolls"); }
});
return;
}
if (FightOnly)
{
if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000))
{
State("Waiting until you stop using the mouse/keyboard (only fighting)");
Sleep(1500);
return;
}
bool goDue = DateTime.UtcNow >= nextFights;
if (!goDue && !RollStopDue(DateTime.UtcNow))
{
State("Only fighting: the next go at " + nextFights.ToLocalTime().ToString("HH:mm"));
Sleep(2000);
return;
}
if (DisconnectBeforeRound()) return;
State("Working: only fighting players");
Act(delegate
{
if (goDue) Log(string.Format("Doing: only fighting players (stamina {0}/{1}, health {2}/{3})", h.Stamina, h.StaminaMax, h.Health, h.HealthMax));
CloseEventWindow();
if (GearBackDue(DateTime.UtcNow)) RunTask("gear back", h);
if (RollStopDue(DateTime.UtcNow)) RunTask(RollStopTask, h);
if (!goDue) return;
taskNow = "fights";
failedNow = neutralNow = false;
int fought = 0;
try { fought = DoFights(100, S.OnlyFightsSpendAll ? 0 : AboveKept); if (!failedNow && !neutralNow) TaskWorked("fights"); }
finally { taskNow = null; if (failedNow) StreakCheck("fights"); }
AfterFightOnly(fought);
});
return;
}
LookForTutorial();
var due = WhatIsDue(h);
if (due.Contains("learn jobs")) due = new List<string> { "learn jobs" };
if (due.Count == 0)
{
if (Once)
{
Log(string.Format("Test round finished: nothing was due (energy {0}/{1}, stamina {2}/{3}, health {4}/{5})",
h.Energy, h.EnergyMax, h.Stamina, h.StaminaMax, h.Health, h.HealthMax));
stop = true;
return;
}
State("Running - nothing to do right now");
if (turns != null) { idleUntil = DateTime.UtcNow.AddSeconds(60); return; }
Sleep(2500);
return;
}
if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000))
{
if (!waitingForUser) Log("You're using the PC - waiting until you've been idle for " + S.IdleSeconds + " s (" + string.Join(", ", due) + " ready)");
waitingForUser = true;
State("Waiting until you stop using the mouse/keyboard (" + string.Join(", ", due) + " ready)");
Sleep(1500);
return;
}
if (S.WaitWhileBusy) waitingForUser = false;
if (DisconnectBeforeRound()) return;
State("Working: " + string.Join(", ", due));
bool acted = Act(delegate
{
Log(string.Format("Doing: {0} (energy {1}/{2}, stamina {3}/{4}, health {5}/{6})",
string.Join(", ", due), h.Energy, h.EnergyMax, h.Stamina, h.StaminaMax, h.Health, h.HealthMax));
CloseEventWindow();
var left = new List<string>();
for (int i = 0; i < due.Count && !RerollOnly && !FightOnly; i++)
{
if (i > 0 && GlobalBossCuts(due[i])) break;
if (TurnUp(due[i])) { left.Add(due[i]); continue; }
State("Working: " + Doing(due, i));
game.Check();
if (i > 0) TerritoryCheck();
RunTask(due[i], h);
BossesAfter(due, i);
BankNext(due, i);
}
SayTurnUp(left);
if (!due.Contains("learn jobs") || RerollOnly || FightOnly) return;
Header h2;
using (var f = game.Capture()) h2 = game.ReadHeader(f);
if (h2.Energy < 0 && last != null) { h2.Energy = last.Energy; h2.EnergyMax = last.EnergyMax; }
last = h2;
var more = WhatIsDue(h2);
more.Remove("learn jobs");
if (more.Count == 0) return;
Log(string.Format("Doing: {0} (energy {1}/{2}, stamina {3}/{4})", string.Join(", ", more), h2.Energy, h2.EnergyMax, h2.Stamina, h2.StaminaMax));
left.Clear();
for (int i = 0; i < more.Count; i++)
{
if (GlobalBossCuts(more[i])) break;
if (TurnUp(more[i])) { left.Add(more[i]); continue; }
State("Working: " + Doing(more, i));
game.Check();
RunTask(more[i], h2);
BossesAfter(more, i);
BankNext(more, i);
}
SayTurnUp(left);
});
if (acted) waitingForUser = false;
if (Once && acted) { Log("Test round finished: " + Count); stop = true; }
}
int loadingLookAt = Environment.TickCount - 60000;
string versionSaid;
void NoteLoading(Frame f)
{
if (Environment.TickCount - loadingLookAt < 30000) return;
loadingLookAt = Environment.TickCount;
bool updated;
if (!Game.GameLoading(f, out updated)) return;
string v = Game.LoadingVersion(f);
if (v == null || v == versionSaid) return;
versionSaid = v;
Log("The game is loading" + (updated ? " its new version " : ", version ") + v);
}
static string Doing(List<string> tasks, int i)
{
var rest = tasks.Skip(i + 1).ToList();
return tasks[i] + (rest.Count == 0 ? "" : ". Next: " + string.Join(", ", rest.Take(3)) + (rest.Count > 3 ? " and " + (rest.Count - 3) + " more" : ""));
}
bool acting;
bool Act(Action work)
{
if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) throw new UserBusyException();
if (!game.Win.BeginAct())
{
if (frontFails++ == 0) Log("Couldn't bring Roblox to the front (PC locked, or an admin window in front?) - will keep trying");
State("Can't bring Roblox to the front - is the PC locked or an admin window in front?");
Sleep(Math.Min(3000 << Math.Min(frontFails - 1, 4), 30000));
return false;
}
if (frontFails > 0) { Log("Roblox is in front again (after " + frontFails + " tries)"); frontFails = 0; }
bool userBack = false;
acting = true;
try { work(); }
catch (UserBusyException) { userBack = true; throw; }
finally
{
acting = false;
rejoiner.Playing(DateTime.UtcNow);
game.Win.EndAct(S.GiveFocusBack && !stop, !stop && !userBack);
}
return true;
}
bool Can(Tab t) { return game == null || !game.NotInMenu(t); }
internal static void FullBarsFirst(List<string> due, Header h)
{
bool energyFull = h.EnergyMax > 0 && h.Energy >= h.EnergyMax;
bool staminaFull = h.StaminaMax > 0 && h.Stamina >= h.StaminaMax;
if (!energyFull && !staminaFull) return;
int at = due.TakeWhile(t => t == RollStopTask || t == "gear back" || t == "events" || t == "tutorial" || t == "operations").Count();
var first = due.Skip(at).Where(t => (energyFull && (t == "heists" || t == "jobs"))
|| (staminaFull && (t == "takedown" || t == "give stamina" || t == "bosses" || t == "fights"))).ToList();
if (first.Count == 0) return;
foreach (var t in first) due.Remove(t);
due.InsertRange(at, first);
}
internal sealed class FullClock
{
readonly List<KeyValuePair<DateTime, double>>[] hour = { new List<KeyValuePair<DateTime, double>>(), new List<KeyValuePair<DateTime, double>>() };
readonly double[] total = new double[2];
readonly bool[] was = new bool[2];
DateTime lastRead = DateTime.MinValue;
public DateTime NextLog = DateTime.MinValue;
public void Reset() { foreach (var l in hour) l.Clear(); total[0] = total[1] = 0; was[0] = was[1] = false; lastRead = NextLog = DateTime.MinValue; }
public void Note(DateTime now, bool?[] full)
{
double gap = lastRead == DateTime.MinValue ? 0 : (now - lastRead).TotalSeconds;
lastRead = now;
for (int i = 0; i < 2; i++)
{
bool isFull = full[i] == true;
if (isFull && was[i] && gap > 0 && gap <= 300) { hour[i].Add(new KeyValuePair<DateTime, double>(now, gap)); total[i] += gap; }
was[i] = isFull;
hour[i].RemoveAll(p => (now - p.Key).TotalMinutes > 60);
}
}
public double HourMinutes(int bar) { return hour[bar].Sum(p => p.Value) / 60; }
public double TotalMinutes(int bar) { return total[bar] / 60; }
}
readonly FullClock fullClock = new FullClock();
DateTime fullShown = DateTime.MinValue;
void NoteFullBars(Header h)
{
var now = DateTime.UtcNow;
fullClock.Note(now, new bool?[] { h.EnergyMax > 0 && h.Energy >= 0 ? (bool?)(h.Energy >= h.EnergyMax) : null,
h.StaminaMax > 0 && h.Stamina >= 0 ? (bool?)(h.Stamina >= h.StaminaMax) : null });
if ((now - fullShown).TotalSeconds >= 20)
{
fullShown = now;
Info("fullbars", string.Format("Bars sat full in the last hour: energy {0}, stamina {1}", FullMin(fullClock.HourMinutes(0)), FullMin(fullClock.HourMinutes(1))));
}
if (now - totalsFoldedAt >= TimeSpan.FromMinutes(10)) FoldTotals();
if (fullClock.NextLog == DateTime.MinValue) fullClock.NextLog = now.AddHours(1);
else if (now >= fullClock.NextLog)
{
fullClock.NextLog = now.AddHours(1);
Log(string.Format("Bars: energy sat full {0} and stamina {1} in the last hour ({2} and {3} since Start)",
FullMin(fullClock.HourMinutes(0)), FullMin(fullClock.HourMinutes(1)), FullMin(fullClock.TotalMinutes(0)), FullMin(fullClock.TotalMinutes(1))));
}
}
static string FullMin(double minutes) { return minutes < 0.5 ? "0 min" : Math.Round(minutes) + " min"; }
bool GearBackDue(DateTime now) { return crewUnequipped && now >= gearBackHold && Can(Tab.Crew); }
List<string> WhatIsDue(Header h, bool rerolls = true)
{
if (Program.Whale) S.WhaleOnly(); else S.MacroOnly();
if (Program.Reroller) S.RerollerOnly();
var now = DateTime.UtcNow;
var due = new List<string>();
bool rollStop = RollStopDue(now);
if (rollStop) due.Add(RollStopTask);
if (GearBackDue(now)) due.Add("gear back");
if (S.GlobalBoss && globalBossUp)
{
if (ExpLastCall(now))
{
if (now >= expClaimAgain && ExpClaimDue(now, h)) due.Add("2x exp");
if (S.Operations && Can(Tab.Operations) && ByNumber(Tab.Operations, nextOps, now)) due.Add("operations");
}
if (now >= nextGlobalBoss) due.Add("global boss");
if (rerolls && S.KeepAlive && due.Count == 0 && now >= keepAliveHold && Rejoiner.KeepAliveDue(now, GameInput)) due.Add("stay in the game");
if (!globalBossWaits || due.Count > 0) return due;
}
bool bossKeeps = S.GlobalBoss && globalBossUp;
if (S.Events && !string.IsNullOrEmpty(badgeSig) && (badgeSig != eventsSig || now >= nextEvents)) due.Add("events");
if (tutorialAction != null && (tutorialAction != "fight" || (h.Stamina > 0 && !bossKeeps && Can(Tab.Fight)))) due.Add("tutorial");
if (S.GlobalBoss && now >= nextGlobalBoss) due.Add("global boss");
if (S.Operations && Can(Tab.Operations) && ByNumber(Tab.Operations, nextOps, now)) due.Add("operations");
bool bare = crewUnequipped;
if (GlobalBossHoldsExp(now)) { if (ExpPending(now)) ExpSay("gb", "2X EXP: waits for the Global Boss"); }
else
{
expSaid.Remove("gb");
if (now >= expClaimAgain && ExpClaimDue(now, h)) due.Add("2x exp");
}
bool expHolds = ExpHoldsHealth(now);
if (expHolds) ExpSay("hold", ExpRunning(now) ? "2X EXP: running - player fights and the Takedown wait while a boss is left"
: "2X EXP: saving health for it - player fights and the Takedown wait" + ExpWhen(now));
else expSaid.Remove("hold");
if (S.Takedown && !Lowering && !bare && !expHolds && !bossKeeps && Can(Tab.Family) && (now >= nextTakedown || (takedownWants > 0 && h.Stamina >= 0
&& h.Stamina - Kept(h.StaminaMax) >= Math.Min(5, takedownWants)))) due.Add("takedown");
if (S.GiveStamina && !bossKeeps && now >= nextGive && WhyNotGive(h) == null && Can(Tab.Family) && !BossDamageHold()) due.Add("give stamina");
NoteGiveWhy(h);
if (S.Bosses && !bare && !bossKeeps && now >= nextBosses && Can(Tab.Bosses)) due.Add("bosses");
if (S.Playtime && now >= nextPlaytime && Can(Tab.Safehouse)) due.Add("playtime rewards");
if (S.Contracts && Can(Tab.Contracts) && ByNumber(Tab.Contracts, nextContracts, now)) due.Add("contracts");
if (S.Briefcases && now >= nextBriefcases) due.Add("briefcases");
if (KnocksOn && S.TrickOrTreat && now >= nextKnock && Can(Tab.Territory)) due.Add("trick or treat");
double spend = Spendable(h);
if (S.Properties && now >= nextProperties && (propLotsOpen != 0 || propReplace || propRecheck) && Can(Tab.Properties)
&& (propRecheck || propPrice <= 0 || (spend >= 0 && spend >= propPrice * 1.01))) due.Add("properties");
if (S.SafehouseUpgrade && (now >= nextUpgrade || (upgradeNeedsLevel > 0 && h.Level >= upgradeNeedsLevel)) && Can(Tab.Safehouse)
&& (upgradePrice < 0 || (upgradePrice > 0 && spend >= 0 && spend >= upgradePrice + PropertyKeep()))) due.Add("safehouse upgrade");
if (!Lowering && ((ShopOn() && now >= nextShop && Can(Tab.Shop)) || (S.Crates && now >= nextCrates && Can(Tab.Inventory) && !CratesHeld(now))
|| (S.EquipBest && now >= nextEquip))) due.Add("gear");
if (!Program.Whale && now >= nextSafehouse && Can(Tab.Safehouse)) due.Add("read safehouse");
if (AnimalsDue(now)) due.Add("animals");
if (S.GameStats && now >= nextGameStats) due.Add("game stats");
if (S.SpendPoints && h.SkillPoints >= game.PointsCost && now >= nextPoints && pointsStopped != PointsStopKey()) due.Add("skill points");
if (S.Jobs && NeedLearn() && Can(Tab.Jobs)) due.Add("learn jobs");
bool startDue = S.StartHeists && now >= nextHeistStart;
bool startSaved = startDue && heistEnergy > 0 && h.Energy >= heistEnergy;
if (S.Heists && ((now >= nextHeists || startSaved) && (h.Energy >= helpEnergy || startDue)) && Can(Tab.Heists) && !(S.HeistsFamilyOnly && family.NotInFamily)) due.Add("heists");
if (S.Jobs && S.JobMode == JobMode.OneJob) NoteOneJob(h); else oneJobSaid = null;
if (S.Jobs && now >= nextJobs && JobReady(h) && Can(Tab.Jobs) && !HeistEnergyHeld(h)) due.Add("jobs");
if (crewQuiet && (S.CrewFill || S.CrewReroll || S.TrainCrew)) { crewQuiet = false; nextCrew = now; }
if (!Program.Whale && !Lowering && Can(Tab.Crew) && (now >= nextCrew || (BuySlots && crewSlotPrice > 0 && spend >= crewSlotPrice + SlotKeep() && now >= nextCrewSlot))) due.Add("crew");
if (S.Fights && !bare && !expHolds && !bossKeeps && now >= nextFights && (FightsMayUse(h) || FightItemsReady(now)) && Can(Tab.Fight)) due.Add("fights");
if (Lowering && !(S.RespectCycle && S.Fights) && now >= respectWait) due.Add("respect back");
if (S.FamilyWatch && now >= nextFamily && Can(Tab.Family)) due.Add("family");
if (Settings.MarketRelistShown && (S.MarketRelist || S.MarketPending.Length > 0) && now >= nextMarket && Can(Tab.BlackMarket)) due.Add("relist");
bool bankTime = S.BankAt > 0 && !cashOut ? h.Cash >= S.BankAt && now - lastBank >= TimeSpan.FromSeconds(30) : now >= nextBank || (due.Count > 0 && now >= lastBank.AddMinutes(1));
if (S.Bank && bankTime && Can(Tab.Bank) && (!AutoRollHoldsBank() || rollStop)) due.Add("bank");
bool lucky = RerollsLead();
if (rerolls && (due.Count == 0 || lucky) && S.CrewReroll && !RollUnwanted && now >= nextReroll && RerollCashOk(h, now) && Can(Tab.Crew))
{
due.Insert(lucky ? due.TakeWhile(t => t == RollStopTask || t == "gear back" || t == "events" || t == "tutorial").Count() : due.Count, "rerolls");
if (lucky && rerollsYield)
foreach (var t in new[] { "gear", "bank" })
if (due.Remove(t)) due.Insert(due.IndexOf("rerolls"), t);
}
else if (!Lowering && rerolls && autoRun != null && autoRun.Running && !RollUnwanted && now >= nextReroll && Can(Tab.Crew)) due.Add("rerolls");
if (rerolls && S.KeepAlive && due.Count == 0 && now >= keepAliveHold && Rejoiner.KeepAliveDue(now, GameInput)) due.Add("stay in the game");
FullBarsFirst(due, h);
if (cashOut && S.Bank && Can(Tab.Bank) && (!AutoRollHoldsBank() || rollStop))
{
due.Remove("bank");
int at = due.TakeWhile(t => t == RollStopTask || t == "gear back").Count();
if (BankFee() != 0) at = Math.Max(at, Math.Max(due.LastIndexOf("properties"), due.LastIndexOf("safehouse upgrade")) + 1);
due.Insert(at, "bank");
}
return due;
}
sealed class MenuNumber
{
public DateTime Red = DateTime.MinValue, Plain = DateTime.MinValue;
public int RedReads;
public DateTime Visited = DateTime.MinValue;
public bool Anyway = true;
public DateTime IgnoreUntil = DateTime.MinValue;
public DateTime Safety = DateTime.MinValue;
}
readonly Dictionary<Tab, MenuNumber> menuNumbers = new Dictionary<Tab, MenuNumber>();
void ResetMenuNumbers()
{
menuNumbers[Tab.Operations] = new MenuNumber();
menuNumbers[Tab.Contracts] = new MenuNumber();
}
int menuNumbersAt = Environment.TickCount - 60000;
void NoteMenuNumbers(Frame f)
{
if (menuNumbers.Count == 0 || Environment.TickCount - menuNumbersAt < 20000) return;
menuNumbersAt = Environment.TickCount;
var v = game.MenuNow(f);
if (v == null || v.Problem != null) return;
var now = DateTime.UtcNow;
foreach (var kv in menuNumbers)
{
var b = v.Badge(f, kv.Key);
if (b == true) { if (++kv.Value.RedReads >= 2) kv.Value.Red = now; }
else if (b == false) { kv.Value.RedReads = 0; kv.Value.Plain = now; }
}
}
bool ByNumber(Tab t, DateTime next, DateTime now)
{
MenuNumber n;
if (!menuNumbers.TryGetValue(t, out n)) return now >= next;
return NumberSays(n.Red, n.Plain, n.Visited, n.Anyway, n.IgnoreUntil, n.Safety, next, now);
}
internal static bool NumberSays(DateTime red, DateTime plain, DateTime visited, bool anyway, DateTime ignoreUntil, DateTime safety, DateTime next, DateTime now)
{
var seen = red > plain ? red : plain;
if (now - seen > TimeSpan.FromMinutes(2)) return now >= next;
if (red > plain && now >= ignoreUntil) return now >= visited.AddMinutes(1);
if (safety == DateTime.MinValue || safety > visited.AddHours(3)) safety = visited.AddHours(3);
return now >= next && (anyway || now >= safety);
}
void Visited(Tab t, bool? worked, bool anyway, DateTime safety = default(DateTime))
{
MenuNumber n;
if (!menuNumbers.TryGetValue(t, out n)) return;
var now = DateTime.UtcNow;
if (worked == false && n.Red > n.Plain && now - n.Red < TimeSpan.FromMinutes(2)) n.IgnoreUntil = now.AddMinutes(30);
n.Visited = now;
n.Anyway = anyway;
n.Safety = safety;
n.RedReads = 0;
n.Red = DateTime.MinValue;
}
sealed class Streak { public DateTime Since; public int Fails; public string Why = ""; public bool Said; }
readonly Dictionary<string, Streak> streaks = new Dictionary<string, Streak>();
string taskNow;
bool failedNow;
bool neutralNow;
void Neutral() { neutralNow = true; }
static string TaskTitle(string task)
{
switch (task)
{
case "give stamina": return "Stamina gifts";
case "gear back": return "Putting the crew's gear back";
case "gear": return "Shop, gear and crates";
case "read safehouse": return "Reading the Safehouse";
case "learn jobs": return "Reading the job list";
case "stay in the game": return "Staying in the game";
case "relist": return "Black Market relisting";
default: return char.ToUpperInvariant(task[0]) + task.Substring(1);
}
}
void TaskFailed(string why, bool own)
{
string task = taskNow;
if (task == null || string.IsNullOrWhiteSpace(why)) return;
Streak s;
if (!streaks.TryGetValue(task, out s)) streaks[task] = s = new Streak();
var now = DateTime.UtcNow;
if (failedNow)
{
if (own) { s.Why = why.Trim(); if (s.Said) ShowStuck(); }
return;
}
failedNow = true;
if (s.Fails == 0) s.Since = now;
s.Fails++;
s.Why = why.Trim();
}
void StreakCheck(string task)
{
Streak s;
if (!streaks.TryGetValue(task, out s) || s.Said || (s.Fails < 3 && DateTime.UtcNow - s.Since < TimeSpan.FromHours(1))) return;
s.Said = true;
string line = StuckLine(task, s);
Log(line);
ShowStuck();
Discord.Problem("stuck " + task, "⚠️ " + ProblemReport.NoUser(line), "The bot keeps trying. The pictures are in its problems folder.", Shot, false, 180);
}
void TaskWorked(string task)
{
Streak s;
if (!streaks.TryGetValue(task, out s)) return;
streaks.Remove(task);
if (!s.Said) return;
Log(TaskTitle(task) + ": working again (stuck from " + s.Since.ToLocalTime().ToString("HH:mm") + ")");
ShowStuck();
}
static string StuckLine(string task, Streak s)
{
return TaskTitle(task) + ": stuck since " + s.Since.ToLocalTime().ToString("HH:mm") + " - " + s.Why + (s.Fails > 1 ? " (" + s.Fails + " tries)" : "");
}
void ShowStuck()
{
Info("stuck", string.Join("\n", streaks.Where(kv => kv.Value.Said).Select(kv => StuckLine(kv.Key, kv.Value))));
}
void RunTask(string task, Header h)
{
taskNow = task;
failedNow = neutralNow = false;
bool spends = task == "fights" || task == "takedown" || task == "tutorial";
if (spends) HealthSpent();
try { RunTaskWatched(task, h); if (!failedNow && !neutralNow) TaskWorked(task); }
finally { taskNow = null; if (failedNow) StreakCheck(task); if (spends) HealthSpent(); }
}
void RunTaskWatched(string task, Header h)
{
try { RunTaskNow(task, h); redone.Remove(task); }
catch (UserBusyException) { if (redone.Add(task)) DueAgain(task); throw; }
catch (WindowChangedException e)
{
if (redone.Add(task)) DueAgain(task);
else if (e.Message != Game.Disconnected)
{
redone.Remove(task);
Later(task, 10);
Log(TaskTitle(task) + ": cut short twice in a row (" + e.Message + ") - it waits 10 minutes, the rest go on");
game.NoteFailure("cut short twice in a row: " + e.Message);
}
throw;
}
catch (NeverPressException) { redone.Remove(task); Later(task, 30); }
catch (StopException) { throw; }
catch (NeedUserException) { throw; }
catch (Exception e)
{
redone.Remove(task);
var memory = MemoryShort(e) ? MemoryCause(e) ?? e : null;
Later(task, memory != null ? 2 : 15);
if (firstSeen.Add("task error " + task + " " + e.Message))
{
if (memory != null) Log(TaskTitle(task) + ": the PC ran out of memory (" + memory.GetType().Name + ": " + memory.Message + ") - it waits 2 minutes");
else Log(TaskTitle(task) + ": an error (" + e.GetType().Name + ": " + e.Message + ") - it waits 15 minutes, the rest go on");
LogDetails(e.ToString());
if (memory == null) SnapshotOnce("error in " + task);
}
game.NoteFailure(memory != null ? "the PC ran out of memory" : "an error: " + e.Message);
if (memory != null) throw new OutOfMemoryPause(e);
}
finally
{
rejoiner.Playing(DateTime.UtcNow);
if (SpendsEnergy.Contains(task)) energyRegen.Forget();
if (SpendsStamina.Contains(task)) staminaRegen.Forget();
}
}
static readonly HashSet<string> SpendsEnergy = new HashSet<string> { "jobs", "heists", "tutorial", "skill points", "learn jobs", "animals" };
static readonly HashSet<string> SpendsStamina = new HashSet<string> { "bosses", "fights", "takedown", "give stamina", "tutorial", "skill points", "global boss" };
void Later(string task, int minutes)
{
var when = DateTime.UtcNow.AddMinutes(minutes);
switch (task)
{
case "operations": nextOps = when; break;
case "bosses": nextBosses = when; break;
case "playtime rewards": nextPlaytime = when; break;
case "contracts": nextContracts = when; break;
case "briefcases": nextBriefcases = when; break;
case "global boss": nextGlobalBoss = when; break;
case "trick or treat": nextKnock = when; break;
case "events": nextEvents = when; eventsSig = badgeSig; break;
case "properties": nextProperties = when; break;
case "safehouse upgrade": nextUpgrade = when; break;
case "crew": nextCrew = nextCrewSlot = when; break;
case "rerolls": nextReroll = when; break;
case RollStopTask: rollStopNext = when; break;
case "gear back": gearBackHold = when; break;
case "gear": nextShop = nextCrates = nextEquip = when; break;
case "read safehouse": nextSafehouse = when; break;
case "game stats": nextGameStats = when; break;
case "skill points": nextPoints = when; break;
case "learn jobs": nextLearn = when; break;
case "heists": nextHeists = when; break;
case "jobs": nextJobs = when; break;
case "fights": nextFights = when; break;
case "bank": nextBank = when; lastBank = when.AddMinutes(-1); break;
case "takedown": nextTakedown = when; break;
case "give stamina": nextGive = when; break;
case "family": nextFamily = when; break;
case "relist": nextMarket = when; break;
case "animals": nextAnimals = when; break;
case "stay in the game": keepAliveHold = when; break;
case "tutorial": nextTutorialLook = when; tutorialAction = null; break;
}
}
DateTime keepAliveHold = DateTime.MinValue;
readonly HashSet<string> redone = new HashSet<string>();
void DueAgain(string task)
{
var now = DateTime.UtcNow;
switch (task)
{
case "operations": nextOps = now; break;
case "bosses": nextBosses = now; break;
case "playtime rewards": nextPlaytime = now; break;
case "contracts": nextContracts = now; break;
case "briefcases": nextBriefcases = now; break;
case "global boss": nextGlobalBoss = now; break;
case "trick or treat": nextKnock = now; break;
case "events": nextEvents = now; break;
case "properties": nextProperties = now; break;
case "safehouse upgrade": nextUpgrade = now; break;
case "crew": nextCrew = now; break;
case "rerolls": nextReroll = now; break;
case RollStopTask: rollStopNext = now; break;
case "gear": nextCrates = nextEquip = now; break;
case "read safehouse": nextSafehouse = now; break;
case "game stats": nextGameStats = now; break;
case "skill points": nextPoints = now; break;
case "learn jobs": nextLearn = now.AddMinutes(5); break;
case "heists": nextHeists = now; break;
case "jobs": nextJobs = now; break;
case "fights": nextFights = now; break;
case "bank": nextBank = now; break;
case "takedown": nextTakedown = now; break;
case "give stamina": nextGive = now; break;
case "family": nextFamily = now; break;
case "relist": nextMarket = now; break;
case "animals": nextAnimals = now; break;
}
}
void RunTaskNow(string task, Header h)
{
switch (task)
{
case "operations": DoOperations(); break;
case "bosses": DoBosses(); break;
case "2x exp":
if (ClaimHeldExp(S.Bosses && Can(Tab.Bosses) ? Tab.Bosses : Tab.Safehouse, "the bosses")) bossesNow = true;
else { Neutral(); expClaimAgain = DateTime.UtcNow.AddMinutes(5); }
break;
case "playtime rewards":
{
int got = game.ClaimPlaytimeRewards();
Count.Rewards += Report(got, "Claimed {0} playtime reward(s)");
if (got > 0) playtimeClaimed = DateTime.UtcNow;
if (game.PlaytimeExpHeld && !expHeld) Log("Playtime rewards: the 2X EXP one is ready and kept - claimed with health almost full, for the finished operations and the bosses");
expHeld = game.PlaytimeExpHeld;
NoteExpCard(DateTime.UtcNow);
int wait = game.PlaytimeNextSeconds;
nextPlaytime = DateTime.UtcNow.AddSeconds(got > 0 || wait == 0 ? 600 : wait > 0 ? Math.Min(wait + 20, 3 * 3600) : wait == -2 ? 3 * 3600 : 1200);
break;
}
case "contracts":
{
int got = game.ClaimContracts();
Count.Rewards += Report(Math.Max(0, got), "Claimed {0} contract task(s)");
nextContracts = DateTime.UtcNow.AddMinutes(got > 0 ? 15 : 30);
Visited(Tab.Contracts, got < 0 ? (bool?)null : got > 0, got < 0);
break;
}
case "events": DoEvents(); break;
case "briefcases": DoBriefcases(); break;
case "global boss": DoGlobalBoss(); break;
case "trick or treat": DoKnocks(); break;
case "tutorial": DoTutorial(); break;
case "properties": DoProperties(); break;
case "safehouse upgrade": DoSafehouseUpgrade(); break;
case "crew": DoCrew(); break;
case "rerolls": DoRerolls(false); break;
case RollStopTask: DoRollStop(); break;
case "gear back":
{
game.OpenTab(Tab.Crew);
game.ScrollPage(0.62, 40);
if (DoEquip() || !crewUnequipped) break;
gearBackFails++;
int wait = gearBackFails == 1 ? 1 : gearBackFails == 2 ? 5 : 15;
gearBackHold = DateTime.UtcNow.AddMinutes(wait);
Log("Crew: the gear isn't back on yet - bosses, player fights and the Takedown wait; AUTO EQUIP BEST again in " + wait + " minute" + (wait == 1 ? "" : "s"));
break;
}
case "gear": DoGear(); break;
case "read safehouse": ReadSafehouse(); break;
case "animals": DoAnimals(h); break;
case "game stats": DoGameStats(); break;
case "skill points":
{
int ups = game.SpendPoints(S.PointsStat, h.SkillPoints, S.PointsStop(S.PointsStat));
int stat = game.SpentOn >= 0 ? game.SpentOn : S.PointsStat;
Report(ups, "Skill points: " + Game.StatNames[stat] + " +{0}");
if (game.PointsAtStop)
{
pointsStopped = PointsStopKey();
Log("Skill points: " + Game.StatNames[stat] + " is at your stop of " + S.PointsStop(stat) + " - the points stay saved until you change it");
}
if (ups > 0 && stat == 5) PropertiesLevelUp();
if (ups == 0 && game.SavingFor != null && pointsSaid != Game.StatNames[stat] + game.PointsCost)
{
pointsSaid = Game.StatNames[stat] + game.PointsCost;
Log("Skill points: saving up for " + game.SavingFor);
}
}
nextPoints = DateTime.UtcNow.AddMinutes(game.PointsFailed ? 60 : 10);
break;
case "learn jobs": LearnJobs(); break;
case "heists": DoHeists(); break;
case "jobs": DoJobs(); break;
case "fights": DoFights(); break;
case "respect back": RespectBack("the respect cycle was switched off"); break;
case "bank":
if (BankJustRan(DateTime.UtcNow))
{
if (nextBank < lastBank.AddSeconds(15)) nextBank = lastBank.AddSeconds(15);
Neutral();
break;
}
DoBank();
break;
case "stay in the game": game.KeepAlive(); break;
case "takedown": DoTakedown(); break;
case "give stamina": DoGiveStamina(); break;
case "family": DoFamilyWatch(); break;
case "relist": DoRelist(); break;
}
}
int Report(int n, string format)
{
if (n > 0) Log(string.Format(format, n));
return n;
}
DateTime nextMarket;
void DoRelist()
{
var now = DateTime.UtcNow;
nextMarket = now.AddMinutes(Math.Max(Settings.MarketMinutesLeast, Math.Min(60, S.MarketMinutes)));
if (!PendingBackUp()) return;
if (!S.MarketRelist) { Neutral(); return; }
var shop = game.OpenMyShop();
if (shop == null) return;
int used = shop.Used;
if (shop.Rows.Count == 0)
{
if (used == 0) SayOnce("market none", "Black Market: there are no listings in your shop - nothing to relist (the bot only puts your own back up)");
Neutral();
return;
}
var done = new List<string>();
var turned = new HashSet<string>();
for (int go = 0; go < Math.Max(1, used) && go < 40; go++)
{
if (go > 0) { shop = game.OpenMyShop(); if (shop == null) break; }
shop = game.MyShopToEnd(shop);
MarketListing last = null;
for (int look = 0; look < 3; look++)
{
if (look > 0) game.Wait(1000);
using (var f = game.Capture())
{
last = game.ReadMyShop(f, Game.Prices.Last).Rows.LastOrDefault();
if (last != null && last.Animal)
{
SayOnce("market animal", "Black Market: " + last.Name + " is an animal - the bot leaves it in your shop as it is and relists the others");
last = game.ReadMyShop(f, Game.Prices.All).Rows.LastOrDefault(r => !r.Animal);
}
}
if (last == null || (last.Price > 0 && last.Left > 0 && last.Name.Length >= 3)) break;
}
if (last == null) break;
if (!last.ForSale)
{
SayOnce("market closed", "Black Market: your listings don't say FOR SALE right now (is the shop closed?) - nothing relisted. The bot never pays rent");
game.Snapshot("market listing not for sale", 360);
break;
}
if (turned.Contains(Parse.Key(last.Name) + "|" + last.Left + "|" + last.Price)) break;
if (last.Price <= 0 || last.Left <= 0 || last.Name.Length < 3)
{
Log("Black Market: couldn't read \"" + last.Name + "\" (" + (last.Left > 0 ? last.Left + " left" : "how many are left unread") + ", "
+ (last.Price > 0 ? last.Price + " each" : "its price unread") + ") - left in your shop as it is");
game.Snapshot("market listing unread", 60);
break;
}
S.MarketPending = last.Name + "|" + last.Left + "|" + last.Price;
SaveSettings();
var removed = game.RemoveListing(last, used);
if (removed == false)
{
S.MarketPending = "";
SaveSettings();
game.NoteFailure("Black Market: " + last.Name + " wasn't taken out");
break;
}
if (removed == null)
{
Log("Black Market: couldn't tell whether " + last.Name + " was taken out - looking again in 2 minutes");
game.NoteFailure("Black Market: " + last.Name + " unclear");
nextMarket = DateTime.UtcNow.AddMinutes(2);
break;
}
int listed;
string why = game.ListItem(last.Name, last.Left, last.Price, out listed);
if (why == Game.NotSeenAfterPut && WaitForCheck(last.Name, listed, last.Price)) why = null;
else if (why == Game.NotSeenAfterPut) { CheckNotDone(last.Name); break; }
if (why != null)
{
Log("Black Market: " + last.Name + " is in your inventory again but didn't go back up (" + why + ") - trying again in " + MarketRetryMinutes + " minutes, no collection deposit meanwhile");
game.NoteFailure("Black Market: " + last.Name + " didn't go back up");
MarketPing(last.Name, why);
nextMarket = DateTime.UtcNow.AddMinutes(MarketRetryMinutes);
break;
}
S.MarketPending = "";
SaveSettings();
turned.Add(Parse.Key(last.Name) + "|" + listed + "|" + last.Price);
done.Add(last.Name + " (" + listed + (listed < last.Left ? " of " + last.Left : "") + " at " + last.Price + ")");
if (game.KeptSpotMinutes >= 0)
{
Log("Black Market: the game kept " + last.Name + " in its old spot - it moves to the top only in " + game.KeptSpotMinutes + " min, so the next relist waits for that");
nextMarket = DateTime.UtcNow.AddMinutes(Math.Max(game.KeptSpotMinutes + 1, Settings.MarketMinutesLeast));
break;
}
}
if (done.Count > 0) Log("Black Market: relisted " + string.Join(", ", done) + " - back on top");
}
bool PendingBackUp()
{
if (S.MarketPending.Length == 0) return true;
var p = S.MarketPending.Split('|');
int qty, price;
if (p.Length != 3 || !int.TryParse(p[1], out qty) || !int.TryParse(p[2], out price) || qty <= 0 || price <= 0 || p[0].Length < 3)
{
S.MarketPending = "";
SaveSettings();
return true;
}
string name = p[0];
var shop = game.OpenMyShop();
if (shop == null) { nextMarket = DateTime.UtcNow.AddMinutes(2); return false; }
var end = game.MyShopToEnd(shop);
if (shop.Rows.Concat(end.Rows).Any(r => Game.SameItem(r.Name, name) && r.Left == qty))
{
Log("Black Market: " + name + " is up in your shop already");
S.MarketPending = "";
checkUndoneAt = DateTime.MinValue;
SaveSettings();
return true;
}
if (DateTime.UtcNow - checkUndoneAt < TimeSpan.FromHours(3))
{
SayOnce("market check wait " + checkUndoneAt.Ticks, "Black Market: " + name + " isn't up yet - the bot puts it up again 3 hours after the bot check, and looks every 30 minutes meanwhile");
nextMarket = DateTime.UtcNow.AddMinutes(MarketRetryMinutes);
return false;
}
checkUndoneAt = DateTime.MinValue;
int listed;
string why = game.ListItem(name, qty, price, out listed);
if (why == Game.NotSeenAfterPut && WaitForCheck(name, listed, price)) why = null;
else if (why == Game.NotSeenAfterPut) { CheckNotDone(name); return false; }
if (why == null)
{
Log("Black Market: put " + name + " back up (" + listed + " at " + price + " each)");
S.MarketPending = "";
SaveSettings();
return true;
}
if (why == Game.NotTradable)
{
Log("Black Market: " + name + " isn't among your tradable items any more - nothing to put back up");
S.MarketPending = "";
SaveSettings();
return true;
}
Log("Black Market: " + name + " still didn't go back up (" + why + ") - again in " + MarketRetryMinutes + " minutes, no collection deposit meanwhile");
game.NoteFailure("Black Market: " + name + " didn't go back up");
MarketPing(name, why);
nextMarket = DateTime.UtcNow.AddMinutes(MarketRetryMinutes);
return false;
}
void MarketPing(string name, string why)
{
Discord.Problem("market relist", "⚠️ Black Market: " + name + " didn't go back up",
"(" + why + ")\nIf the game shows a bot check, do it yourself " + CheckWhere + ". The item is in your inventory; the bot tries again in "
+ MarketRetryMinutes + " minutes.", Shot, false, 30, mention: true);
}
const int CheckWaitMinutes = 2;
#if LINUX && !MAC
const string CheckWhere = "on the Live page with the mouse on (http://127.0.0.1:8090/?control)";
#else
const string CheckWhere = "in the game";
#endif
DateTime checkUndoneAt
{
get
{
DateTime at;
return DateTime.TryParse(S.MarketCheckUndone, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out at) ? at : DateTime.MinValue;
}
set { S.MarketCheckUndone = value == DateTime.MinValue ? "" : value.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture); }
}
bool WaitForCheck(string name, int qty, int price)
{
Log("Black Market: " + name + " didn't show in your shop after PUT IN YOUR SHOP - if the game asks a bot check, do it " + CheckWhere + ": the bot leaves the game alone up to " + CheckWaitMinutes + " minutes for it");
Discord.Problem("market check", "⚠️ Black Market: the game may want a bot check",
"The bot put " + name + " up, but it didn't show in your shop. If the game asks a bot check, do it yourself " + CheckWhere + ": the bot waits "
+ CheckWaitMinutes + " minutes without touching the game, then relists the rest. Not done by then: it plays on and puts the item up again in 3 hours.", Shot, false, 30, mention: true);
var until = DateTime.UtcNow.AddMinutes(CheckWaitMinutes);
while (!stop && DateTime.UtcNow < until)
{
Sleep(15000);
if (stop) break;
try { using (var f = game.Capture()) if (game.OnTop(f, name, qty, price)) { Log("Black Market: " + name + " is up - thanks, relisting goes on"); return true; } }
catch (Exception) { }
}
return false;
}
void CheckNotDone(string name)
{
Log("Black Market: " + name + " still isn't up after " + CheckWaitMinutes + " minutes - it's in your inventory; the bot plays on, looks every 30 minutes whether it's up and puts it up again in 3 hours, no collection deposit meanwhile");
game.NoteFailure("Black Market: " + name + " didn't go back up");
checkUndoneAt = DateTime.UtcNow;
SaveSettings();
nextMarket = DateTime.UtcNow.AddMinutes(MarketRetryMinutes);
}
const int MarketRetryMinutes = 30;
bool NeedLearn()
{
if (DateTime.UtcNow < nextLearn || level <= 0) return false;
int top = Catalog.Count == 0 ? -1 : Catalog.Max(j => GameData.CityIndex(j.City));
if (top + 1 < GameData.CityLevels.Length && level >= GameData.CityLevels[top + 1]) return true;
if (learnedThisRun) return false;
if (catalogBuiltIn) return true;
if (S.JobMode == JobMode.Mastery && !JobsReadRecently()) return true;
return Catalog.Count == 0;
}
bool JobsReadRecently()
{
DateTime at;
if (!DateTime.TryParse(S.JobsReadAt, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out at)) return false;
var age = DateTime.UtcNow - at;
return age >= TimeSpan.Zero && age < TimeSpan.FromHours(12);
}
void LearnJobs()
{
nextLearn = DateTime.UtcNow.AddMinutes(30);
if (!game.OpenTab(Tab.Jobs)) { nextLearn = DateTime.UtcNow.AddMinutes(5); return; }
Log("Reading every job and its mastery (takes a moment)...");
int before = Catalog.Count;
var fold = FoldCities();
var kept = Catalog.Select(j => j.Copy()).ToList();
string otherBefore = OtherOnTop();
int seen = otherBefore != null ? 0 : game.LearnJobs(Catalog, level, true, fold);
string other = otherBefore ?? OtherOnTop();
if (other != null)
{
Catalog = kept;
CatalogView = Catalog.Select(j => j.Copy()).ToList();
var cc = CatalogChanged;
if (cc != null) cc();
nextLearn = DateTime.UtcNow.AddMinutes(10);
Log("Jobs: the job read was left out - the game showed " + other + " (another Roblox window in front?); read again in 10 minutes");
return;
}
GameData.Renumber(Catalog);
SaveCatalog();
if (FoldCities().Any(c => !fold.Contains(c))) foldDue = true;
learnedThisRun = true;
if (seen > 0) { S.JobsReadAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); catalogBuiltIn = false; }
Log(string.Format("Read {0} jobs ({1} new).", seen, Catalog.Count - before));
ReportTarget();
}
string OtherOnTop()
{
Header h;
using (var f = game.Capture()) h = game.ReadHeader(f);
if (h == null) return null;
if (S.Account.Length > 0 && !string.IsNullOrEmpty(h.Name) && Accounts.Plausible(h.Name))
lock (accountLock) if (IsOther(Accounts.Known(dir, h.Name.Trim()), Whose())) return "\"" + h.Name.Trim() + "\"";
if (h.Level > 0 && level > 0 && Math.Abs(h.Level - level) > 5) return "level " + h.Level + " (this account is " + level + ")";
return null;
}
void SaveCatalog()
{
CatalogView = Catalog.Select(j => j.Copy()).ToList();
try
{
GameData.SaveCatalog(catalogFile, Catalog);
if (S.Account.Length > 0) { Directory.CreateDirectory(Accounts.Folder(dir, S.Account)); GameData.SaveCatalog(Accounts.Jobs(dir, S.Account), Catalog); }
}
catch (Exception e) { if (firstSeen.Add("jobs.txt not saved")) Log("Couldn't save jobs.txt: " + e.Message); }
var h = CatalogChanged;
if (h != null) h();
}
public JobInfo PickJob(Header h) { return PickJob(h, Catalog); }
public JobInfo PickJob(Header h, List<JobInfo> catalog)
{
if (S.JobMode == JobMode.OneJob)
{
JobInfo one;
return OneJobWhy(h, catalog, out one) == null && !JobAside(one) ? one : null;
}
int lv = JobLevel(h);
int maxE = h != null && h.EnergyMax > 0 ? h.EnergyMax : int.MaxValue;
var open = catalog.Where(j => j.TierLevel <= lv && j.Cost > 0 && j.Cost <= maxE && !j.Gone && !JobAside(j)).ToList();
if (S.JobMode == JobMode.Mastery)
{
var next = open.OrderBy(j => j.Order).FirstOrDefault(j => j.MasteryRank < MasteryGold);
if (next != null || open.Count == 0) return next;
}
return open.OrderByDescending(j => j.XpPerEnergy).ThenByDescending(j => j.Xp).FirstOrDefault();
}
bool foldDue = true;
HashSet<int> FoldCities()
{
var picked = PickJob(last);
int keep = picked == null ? -1 : GameData.CityIndex(picked.City);
return new HashSet<int>(Catalog.GroupBy(j => GameData.CityIndex(j.City))
.Where(g => g.Key >= 0 && g.Key != keep && g.All(j => j.MasteryRank >= MasteryGold)).Select(g => g.Key));
}
public bool MasteryDone(JobInfo picked) { return S.JobMode == JobMode.Mastery && picked != null && picked.MasteryRank >= MasteryGold; }
const int MasteryGold = 3;
string oneJobSaid;
public string OneJobWhy(Header h, List<JobInfo> catalog, out JobInfo job)
{
return OneJobWhy(S.OneJob, catalog, JobLevel(h), h != null && h.EnergyMax > 0 ? h.EnergyMax : 0, out job);
}
int levelBelow = int.MaxValue;
DateTime levelBelowUntil;
int jumpLevel = -1, statsLevel = -1;
DateTime jumpSince, statsAt;
bool TakeLevel(int read)
{
if (level <= 0 || read <= level + 5) { jumpLevel = -1; return true; }
var now = DateTime.UtcNow;
if (read != jumpLevel)
{
jumpLevel = read; jumpSince = now;
Log("The top bar reads level " + read + ", the bot had " + level + " - a jump that big is checked on the game's STATS first");
if (S.GameStats) nextGameStats = now;
}
if (statsLevel > 0 && statsAt >= jumpSince) return Math.Abs(statsLevel - read) <= 1;
return now - jumpSince >= TimeSpan.FromMinutes(30);
}
int JobLevel(Header h)
{
int lv = level > 0 ? level : h != null && h.Level > 0 ? h.Level : int.MaxValue;
return levelBelow < int.MaxValue && DateTime.UtcNow < levelBelowUntil ? Math.Min(lv, Math.Max(1, levelBelow - 1)) : lv;
}
internal static string OneJobWhy(string name, List<JobInfo> catalog, int level, int energyMax, out JobInfo job)
{
job = null;
name = (name ?? "").Trim();
if (name.Length == 0) return OneJobNone;
job = catalog.FirstOrDefault(j => j.Name == name) ?? catalog.FirstOrDefault(j => string.Equals(j.Name, name, StringComparison.OrdinalIgnoreCase));
if (job == null || job.Cost <= 0) return "isn't in the job list";
if (job.Gone) return "wasn't in the game's job list";
if (job.TierLevel > Math.Max(1, level)) return "opens at level " + job.TierLevel;
if (energyMax > 0 && job.Cost > energyMax) return "needs " + job.Cost + " energy and your bar holds " + energyMax;
return null;
}
public const string OneJobNone = "no job picked";
public DateTime JobAsideUntil(string name)
{
DateTime until;
lock (jobAside) return name != null && jobAside.TryGetValue(name, out until) && DateTime.UtcNow < until ? until : DateTime.MinValue;
}
void NoteOneJob(Header h)
{
JobInfo job;
string why = OneJobWhy(h, Catalog, out job);
string name = (S.OneJob ?? "").Trim();
string said = why == null ? null : name + "|" + why;
if (said == oneJobSaid) return;
if (why == OneJobNone) Log("Jobs: no job picked - no jobs until you click one (One job, Energy page)");
else if (why != null)
Log("Jobs: \"" + name + "\" " + why + " - no jobs" + (why.StartsWith("opens at") ? " until then" : why.StartsWith("needs") ? " until it holds more" : "")
+ " (One job, Energy page)");
else if (oneJobSaid != null && oneJobSaid.StartsWith(name + "|")) Log("Jobs: \"" + job.Name + "\" can be done now - doing only that job (One job)");
oneJobSaid = said;
Info("job", why ?? job.Name);
}
readonly Dictionary<string, DateTime> jobAside = new Dictionary<string, DateTime>();
readonly Dictionary<string, int> jobMisses = new Dictionary<string, int>();
bool JobAside(JobInfo j)
{
DateTime until;
lock (jobAside) return jobAside.TryGetValue(j.Name, out until) && DateTime.UtcNow < until;
}
void JobMissed(JobInfo job, string why)
{
int n;
jobMisses.TryGetValue(job.Name, out n);
jobMisses[job.Name] = ++n;
game.NoteFailure("\"" + job.Name + "\" " + why);
if (n < 2) return;
jobMisses.Remove(job.Name);
lock (jobAside) jobAside[job.Name] = DateTime.UtcNow.AddHours(1);
Log("Jobs: \"" + job.Name + "\" " + why + " twice in a row - it waits an hour, "
+ (S.JobMode == JobMode.OneJob ? "no other job meanwhile (One job)" : "the next best job meanwhile"));
ReportTarget();
nextJobs = DateTime.UtcNow;
}
bool JobReady(Header h)
{
var job = PickJob(h);
if (job == null || h.Energy < job.Cost) return false;
if (h.EnergyMax <= 0) return true;
if (h.Energy >= h.EnergyMax - 2 * job.Cost) return true;
if (ExpRunning(DateTime.UtcNow)) return true;
return h.XpNext > 0 && h.Xp >= 0 && job.Xp > 0 && (long)(h.Energy / job.Cost) * job.Xp >= h.XpNext - h.Xp;
}
void ReportTarget()
{
if (S.Jobs && S.JobMode == JobMode.OneJob) NoteOneJob(last);
var job = PickJob(last);
Info("job", job == null ? (S.JobMode == JobMode.Mastery ? "all jobs have the chosen mastery" : S.JobMode == JobMode.OneJob ? "one job: not now" : "none") :
string.Format("{0} ({1} energy, {2} XP, mastery {3}{4})", job.Name, job.Cost, job.Xp, job.MasteryLabel, MasteryDone(job) ? ", the best XP job now that every job is Gold" : ""));
}
void DoJobs()
{
var job = PickJob(last);
if (job == null) { Neutral(); return; }
if (last != null && last.Energy >= 0 && last.Energy < job.Cost) { Neutral(); return; }
if (MasteryDone(job) && firstSeen.Add("mastery done " + job.Name))
Log("Jobs: every job open has Gold mastery - doing the best XP job, \"" + job.Name + "\", until a new one opens");
if (!game.OpenTab(Tab.Jobs)) { nextJobs = DateTime.UtcNow.AddMinutes(2); return; }
if (foldDue)
{
foldDue = false;
var fold = FoldCities();
if (fold.Count > 0) game.SetCities(new int[0], fold);
}
var card = game.FindJob(job, Catalog);
if (card == null)
{
int ci = GameData.CityIndex(job.City);
if (ci >= 0 && game.ExpandCity(ci)) card = game.FindJob(job, Catalog);
}
if (card == null)
{
var saw = game.JobsSeen.Take(12).ToList();
Log("Couldn't find \"" + job.Name + "\" in the job list - trying again in 5 minutes"
+ (saw.Count > 0 ? " (it showed: " + string.Join(", ", saw) + (game.JobsSeen.Count > 12 ? "..." : "") + ")" : ""));
game.Snapshot("job not found " + job.Name, 60);
nextJobs = DateTime.UtcNow.AddMinutes(5);
JobMissed(job, "wasn't found in the job list");
return;
}
var lm = Regex.Match(card.ButtonText ?? "", @"LEVEL\s*([0-9OIlS]+)", RegexOptions.IgnoreCase);
int opensAt;
if (lm.Success && int.TryParse(Parse.Digits(lm.Groups[1].Value), out opensAt) && opensAt > job.TierLevel && GameData.InCityLevels(job.City, opensAt))
{
if (S.JobMode != JobMode.OneJob) Log(string.Format("\"{0}\" opens at level {1} - picking another job", job.Name, opensAt));
job.TierLevel = opensAt;
int by = JobLevel(last);
if (opensAt <= by && (opensAt < levelBelow || DateTime.UtcNow >= levelBelowUntil))
{
levelBelow = opensAt; levelBelowUntil = DateTime.UtcNow.AddHours(1);
Log("Jobs: the bot went by level " + (by == int.MaxValue ? "none" : by.ToString()) + " (the top bar read "
+ (last != null && last.Level > 0 ? last.Level.ToString() : "no level") + ") - only jobs below level " + opensAt + " for an hour");
}
SaveCatalog();
ReportTarget();
nextJobs = DateTime.UtcNow;
return;
}
if (card.Button == ButtonState.Locked)
{
if (level <= 0 && (last == null || last.Level <= 0))
{
int below = lm.Success && int.TryParse(Parse.Digits(lm.Groups[1].Value), out opensAt) && opensAt > 1 ? Math.Min(opensAt, job.TierLevel) : job.TierLevel;
if (below > 1 && (below < levelBelow || DateTime.UtcNow >= levelBelowUntil))
{
levelBelow = below; levelBelowUntil = DateTime.UtcNow.AddHours(1);
Log("Jobs: your level didn't read, and \"" + job.Name + "\" isn't open yet - jobs below level " + below + " for now");
ReportTarget();
nextJobs = DateTime.UtcNow;
return;
}
}
int shown, by = JobLevel(last);
if (lm.Success && int.TryParse(Parse.Digits(lm.Groups[1].Value), out shown) && shown > 1 && shown <= by && GameData.InCityLevels(job.City, shown)
&& (shown < levelBelow || DateTime.UtcNow >= levelBelowUntil))
{
levelBelow = shown; levelBelowUntil = DateTime.UtcNow.AddHours(1);
Log("Jobs: \"" + job.Name + "\" shows LEVEL " + shown + " though the bot went by level " + (by == int.MaxValue ? "none" : by.ToString())
+ " (the top bar read " + (last != null && last.Level > 0 ? last.Level.ToString() : "no level") + ") - only jobs below level " + shown + " for an hour");
ReportTarget();
nextJobs = DateTime.UtcNow;
return;
}
nextJobs = DateTime.UtcNow.AddMinutes(5);
JobMissed(job, "shows \"" + (card.ButtonText ?? "").Trim() + "\" where DO JOB should be");
return;
}
int energy = last != null ? last.Energy : job.Cost;
int times = Math.Max(1, Math.Min(30, energy / Math.Max(1, job.Cost)));
if (S.JobMode == JobMode.Mastery && job.MasteryRank >= 0 && job.MasteryRank < MasteryGold && job.MasteryCur >= 0)
{
int left = 100 - job.MasteryCur;
if (left > 0) times = Math.Min(times, left);
}
int rankBefore = job.MasteryRank;
int done = game.DoJob(card, times, Catalog);
Count.Jobs += done;
if (done > 0) jobMisses.Remove(job.Name);
else if (card.Button != ButtonState.Dim) JobMissed(job, "couldn't be done (its DO JOB didn't take)");
if (done > 0 && last != null && last.Energy >= 0) last.Energy = Math.Max(0, last.Energy - done * job.Cost);
if (done > 0) energyRegen.Forget();
string before = job.MasteryLabel, label = null;
for (int look = 0; look < 3; look++)
{
if (look > 0) Sleep(700);
using (var f = game.Capture())
{
var fresh = game.ReadJobCards(f, Catalog, false).Where(c => c.Info == job).OrderBy(c => Math.Abs(c.NameY - card.NameY)).FirstOrDefault();
if (fresh != null) card = fresh;
label = Game.ReadMasteryText(f, card);
Game.ApplyMastery(job, label, f, card);
}
if (done == 0 || job.MasteryLabel != before || job.MasteryRank >= 3) break;
}
if (done > 0)
{
Log(string.Format("Did \"{0}\" x{1}, mastery now {2}", job.Name, done, job.MasteryLabel));
if (job.MasteryLabel == before && job.MasteryRank < 3)
{
Log(" (mastery label read as \"" + label + "\")");
if (!Game.IsMasteryLabel(label)) SnapshotOnce("mastery label " + Game.MasteryWording(label));
}
}
if (S.JobMode == JobMode.Mastery && job.MasteryRank >= MasteryGold && rankBefore < MasteryGold) Log("Finished the mastery of \"" + job.Name + "\" - moving to the next job");
SaveCatalog();
ReportTarget();
nextJobs = DateTime.UtcNow.AddSeconds(done > 0 ? 2 : 30);
}
void DoHeists()
{
DateTime helpsAt = nextHeists;
bool helpsDue = DateTime.UtcNow >= helpsAt;
nextHeists = DateTime.UtcNow.AddMinutes(3);
if (!game.OpenTab(Tab.Heists))
{
if (!helpsDue) { nextHeists = helpsAt; nextHeistStart = DateTime.UtcNow.AddMinutes(3); }
return;
}
game.HeistsTop();
CollectHeistTake();
DismissFailedHeist();
if (S.StartHeists && DateTime.UtcNow >= nextHeistStart) StartHeist();
if (!helpsDue) { nextHeists = helpsAt; return; }
bool lookedAgain = false, reopened = false;
int energyLeft = int.MaxValue, favorsLeft = int.MaxValue;
for (int i = 0; i < 30; i++)
{
var h = ReadBarsSettled(5000, x => x.Energy >= 0);
if (h.Energy < 0)
{
if (energyLeft != int.MaxValue) h.Energy = energyLeft;
else { Log("Couldn't read energy - no heist helps this round"); game.NoteFailure("the energy bar didn't read"); SnapshotOnce("heists energy unreadable"); return; }
}
if (Math.Min(h.Energy, energyLeft) < helpEnergy) return;
int favors;
List<HeistRow> rows;
using (var f = game.Capture()) { favors = game.ReadFavors(f); rows = game.ReadHeists(f); }
for (int look = 0; look < 6 && favors < 0; look++)
{
if (look > 0) game.Wait(800);
using (var f = game.Capture()) { favors = game.ReadFavors(f); rows = game.ReadHeists(f); }
}
if (favors > 0 && favorsLeft == int.MaxValue)
{
int again;
using (var f = game.Capture()) again = game.ReadFavors(f, 1);
if (again < 0) using (var f = game.Capture()) again = game.ReadFavors(f);
if (again != favors)
{
int third;
game.Wait(600);
using (var f = game.Capture()) third = game.ReadFavors(f);
favors = third >= 0 && (third == favors || third == again) ? third : -1;
}
}
int cost = rows.Where(r => r.Energy > 0).Select(r => r.Energy).DefaultIfEmpty(-1).Max();
if (cost > 0) helpEnergy = Math.Max(HelpEnergy, cost);
else if (h.EnergyMax > 0) helpEnergy = Math.Max(helpEnergy, (int)Math.Ceiling(h.EnergyMax / 20.0));
if (Math.Min(h.Energy, energyLeft) < helpEnergy) return;
if (favors >= 0) { favors = Math.Min(favors, favorsLeft); Info("favors", favors.ToString()); }
if (favors == 0)
{
var midnight = DateTime.UtcNow.Date.AddDays(1).AddMinutes(1);
nextHeists = DateTime.UtcNow.AddHours(1) < midnight ? DateTime.UtcNow.AddHours(1) : midnight;
Log("No heist favors left - checking again in " + (midnight < DateTime.UtcNow.AddHours(1) ? (int)Math.Ceiling((midnight - DateTime.UtcNow).TotalMinutes) + " minutes, after the reset" : "an hour"));
return;
}
if (favors < 0)
{
Tab? on;
using (var f = game.Capture()) on = game.SelectedTab(f);
if (on != null && on != Tab.Heists && !reopened)
{
reopened = true;
Log("Heists: the " + View.TabLabels[on.Value] + " page was showing, not HEISTS - opening it again");
if (!game.OpenTab(Tab.Heists)) return;
game.HeistsTop();
i--;
continue;
}
Log("Couldn't read how many favors are left - not helping this time");
game.NoteFailure("the favors left didn't read");
game.Snapshot("heist favors unread", 60);
nextHeists = DateTime.UtcNow.AddMinutes(15);
return;
}
double perEnergy = JobXpPerEnergy();
var best = rows.Where(r => r.CanHelp && S.WouldHelp(r.YourFamily, r.XpPerHelp, perEnergy * HelpCost(r)))
.OrderByDescending(r => r.YourFamily).ThenByDescending(r => r.XpPerHelp).FirstOrDefault();
if (best == null)
{
if (i == 0) { nextHeists = DateTime.UtcNow.AddMinutes(10); return; }
if (lookedAgain) return;
lookedAgain = true;
game.Wait(1500);
game.HeistsTop();
continue;
}
lookedAgain = false;
if (Math.Min(h.Energy, energyLeft) < Math.Max(helpEnergy, best.Energy)) return;
game.HelpHeist(best);
string toast = game.ReadToast(900, t => Game.Refused(t) || Parse.Has(t, "HEIST") || Parse.Has(t, "HELP") || Parse.Has(t, "FAVOR"));
if (Game.Refused(toast))
{
Log("Heist help refused: " + toast);
nextHeists = DateTime.UtcNow.AddMinutes(10);
return;
}
if (toast != null) SnapshotOnce("heist help notification");
energyLeft = Math.Min(h.Energy, energyLeft) - Math.Max(helpEnergy, best.Energy);
if (last != null && last.Energy >= 0) last.Energy = Math.Max(0, Math.Min(last.Energy, energyLeft));
favorsLeft = favors - 1;
Count.Helps++;
Log(string.Format("Helped \"{0}\" ({1}){2}", best.Name, best.XpPerHelp >= 0 ? "+" + best.XpPerHelp + " XP" : "XP not read",
best.YourFamily ? " - a heist of your family" : ""));
nextHeists = DateTime.UtcNow.AddSeconds(30);
}
}
DateTime nextHeistStart;
const int HelpEnergy = 6;
int helpEnergy = HelpEnergy;
int heistEnergy;
const int HeistsPerDay = 2;
static string HeistDay() { return DateTime.UtcNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); }
internal static bool HeistsUsedUp(string toast)
{
toast = toast == null ? null : toast.Replace("alreadv", "already").Replace("todav", "today");
return toast != null && (Parse.Has(toast, "ALREADY STARTED") || Parse.Has(toast, "ALREADY ATTEMPTED")) && Parse.Has(toast, "HEISTS TODAY");
}
internal static TimeSpan? HeistsResetIn(string words)
{
var m = System.Text.RegularExpressions.Regex.Match(words ?? "", @"RESET\s*IN\s*(\d{1,2})\s*[:.]\s*(\d{2})", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
if (!m.Success) return null;
int hh = int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture), mm = int.Parse(m.Groups[2].Value, CultureInfo.InvariantCulture);
if (hh > 24 || mm > 59) return null;
return new TimeSpan(hh, mm, 0);
}
bool HeistsUsedUpSaid(string words)
{
if (!HeistsUsedUp(words)) return false;
heistEnergy = 0;
S.HeistsToday = HeistDay() + "|" + HeistsPerDay;
SaveSettings();
var left = HeistsResetIn(words);
nextHeistStart = left.HasValue ? DateTime.UtcNow.Add(left.Value).AddMinutes(2) : DateTime.UtcNow.Date.AddDays(1).AddMinutes(2);
SayOnce("heists used " + HeistDay(), "Heists: the game says the day's " + HeistsPerDay + " heists are used (\"" + Cut(words.Trim(), 140) + "\") - the next one "
+ (left.HasValue ? "in " + (int)left.Value.TotalHours + "h" + left.Value.Minutes.ToString("00") : "after midnight UTC") + ", the energy goes to the jobs");
return true;
}
int HeistsToday()
{
var p = (S.HeistsToday ?? "").Split('|');
int n;
return p.Length == 2 && p[0] == HeistDay() && int.TryParse(p[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out n) && n > 0 ? n : 0;
}
bool HeistEnergyHeld(Header h)
{
return S.Heists && S.StartHeists && heistEnergy > 0 && h.Energy >= 0 && h.Energy < heistEnergy && (h.EnergyMax <= 0 || heistEnergy <= h.EnergyMax);
}
void CollectHeistTake()
{
OcrLine button;
string heist, haul;
using (var f = game.Capture()) button = game.CollectTheTake(f, out heist, out haul);
if (button == null) return;
game.PressCollectTheTake(button);
OcrLine still = null;
string h2, haul2;
for (int look = 0; look < 6 && (look == 0 || still != null); look++)
{
if (look > 0) game.Wait(look < 3 ? 1000 : 2000);
using (var f = game.Capture()) still = game.CollectTheTake(f, out h2, out haul2);
}
if (still != null)
{
Log("Heists: COLLECT THE TAKE didn't take" + (heist.Length > 0 ? " on " + heist : "") + " - again next round");
game.Snapshot("heist take not collected", 60);
return;
}
Log("Heists: collected the take" + (heist.Length > 0 ? " of " + heist : "") + (haul.Length > 0 ? " (" + haul + ")" : ""));
SnapshotOnce("heist take collected");
}
DateTime nextHeistDismiss = DateTime.MinValue;
void DismissFailedHeist()
{
if (DateTime.UtcNow < nextHeistDismiss) return;
OcrLine button;
string heist, how;
using (var f = game.Capture()) button = game.HeistFailedDismiss(f, out heist, out how);
if (button == null) return;
game.PressHeistDismiss(button);
OcrLine still = null;
string h2, how2;
for (int look = 0; look < 3 && (look == 0 || still != null); look++)
{
if (look > 0) game.Wait(1000);
using (var f = game.Capture()) still = game.HeistFailedDismiss(f, out h2, out how2);
}
if (still != null)
{
nextHeistDismiss = DateTime.UtcNow.AddMinutes(30);
Log("Heists: DISMISS didn't take" + (heist.Length > 0 ? " on " + heist : "") + " - again in 30 minutes");
game.Snapshot("heist dismiss not taken", 60);
return;
}
bool free;
using (var f = game.Capture()) free = game.NoActiveHeist(f) != null;
Log("Heists: dismissed " + (heist.Length > 0 ? heist : "your heist") + (how.Length > 0 ? " (" + how + ", the stake lost)" : "") + (free ? "" : " - the page doesn't say NO ACTIVE HEIST yet"));
SnapshotOnce(free ? "heist failed dismissed" : "heist failed dismissed page unread");
}
internal static string OwnHeistKind(string words)
{
if (string.IsNullOrWhiteSpace(words)) return null;
string k = Parse.Key(words).Replace("SUCCESSREWARDS", "");
foreach (var w in new[] { Tuple.Create("CIAIM", "claim"), Tuple.Create("COIIECT", "collect"), Tuple.Create("DISMISS", "dismiss"),
Tuple.Create("CIOSE", "close"), Tuple.Create("REMOVE", "remove"), Tuple.Create("RETRY", "retry") })
if (k.Contains(w.Item1)) return w.Item2;
if (k.Contains("FAIIED") || k.Contains("SABOTAGED") || k.Contains("EXPIRED") || k.Contains("TIMEUP")) return "failed";
if (k.Contains("SUCCESS") || k.Contains("COMPIETE") || k.Contains("PAYOUT") || k.Contains("WON")) return "won";
if (k.Contains("ENDSIN") || k.Contains("HEIPS")) return "running";
return "other";
}
void StartHeist()
{
nextHeistStart = DateTime.UtcNow.AddMinutes(10);
if (HeistsToday() >= HeistsPerDay)
{
heistEnergy = 0;
nextHeistStart = DateTime.UtcNow.Date.AddDays(1).AddMinutes(2);
return;
}
OcrLine button;
string own = "";
using (var f = game.Capture()) { button = game.NoActiveHeist(f); own = game.OwnHeistWords(f); }
if (button != null)
{
game.Wait(800);
using (var f = game.Capture()) { button = game.NoActiveHeist(f); own = game.OwnHeistWords(f); }
}
if (HeistsUsedUpSaid(own)) return;
if (button == null)
{
string kind = OwnHeistKind(own);
if (kind != null && saidOnce.Add("own heist " + kind + " " + DateTime.UtcNow.ToString("yyyyMMddHH")))
{
Log("Heists: your own heist shows \"" + Cut(own, 200) + "\"");
if (kind != "running") SnapshotOnce("heist of yours " + kind);
}
heistEnergy = 0;
return;
}
var h = ReadBarsSettled(5000, x => x.Energy >= 0 && x.Cash >= 0);
if (h.Energy < 0)
{
heistEnergy = 0;
Log("Heists: couldn't read the energy - no heist started (again in 10 minutes)");
SnapshotOnce("heist start energy unread");
return;
}
if (heistEnergy > 0 && h.Energy < heistEnergy && (h.EnergyMax <= 0 || heistEnergy <= h.EnergyMax))
{
nextHeistStart = DateTime.UtcNow.AddMinutes(2);
return;
}
game.PressStartAHeist(button);
List<HeistOffer> offers = null;
OcrLine close = null;
HeistOffer pick = null;
for (int look = 0; look < 6 && pick == null; look++)
{
if (look > 0) { game.ScrollPage(0.55, -2); game.Wait(500); }
using (var f = game.Capture()) offers = game.ReadHeistOffers(f, out close);
if (offers == null) { game.Wait(700); continue; }
pick = offers.FirstOrDefault(o => Game.SameHeist(o.Name, S.StartHeist) && (o.Start != null || o.Short));
if (pick == null && offers.Any(o => Game.SameHeist(o.Name, S.StartHeist) && o.Start == null && !o.Short))
pick = offers.Where(o => o.Start != null || o.Short).OrderByDescending(o => o.Level).FirstOrDefault();
}
if (offers == null)
{
string now;
using (var f = game.Capture()) now = game.NoActiveHeist(f) == null ? game.OwnHeistWords(f) : "";
if (OwnHeistKind(now) == "running")
{
Log("Heists: one of yours is running after all - no heist started");
heistEnergy = 0;
return;
}
Log("Heists: the START A HEIST window didn't show");
game.Snapshot("heist start window not read", 60);
nextHeistStart = DateTime.UtcNow.AddMinutes(60);
return;
}
if (pick == null)
{
SayOnce("heist pick " + S.StartHeist, "Heists: \"" + S.StartHeist + "\" wasn't found in the START A HEIST window - no heist started (looking again in an hour)");
game.Snapshot("heist pick not found", 60);
game.CloseHeistOffers(close);
nextHeistStart = DateTime.UtcNow.AddMinutes(60);
return;
}
if (!Game.SameHeist(pick.Name, S.StartHeist)) SayOnce("heist lower " + pick.Name, "Heists: " + S.StartHeist + " is above your level - starting " + pick.Name + " instead");
if (pick.Energy <= 0)
{
SayOnce("heist energy unread " + pick.Name, "Heists: couldn't read " + pick.Name + "'s energy in the START A HEIST window - not started (again in 10 minutes)");
game.CloseHeistOffers(close);
return;
}
if (h.Energy < pick.Energy)
{
heistEnergy = pick.Energy;
SayOnce("heist energy " + pick.Name, "Heists: saving " + pick.Energy + " energy to start " + pick + " - the jobs wait for it");
game.CloseHeistOffers(close);
nextHeistStart = DateTime.UtcNow.AddMinutes(h.EnergyMax > 0 && pick.Energy > h.EnergyMax ? 60 : 2);
return;
}
bool took = false;
try
{
if (pick.Start == null || pick.Stake > 0 && (h.Cash < 0 || h.Cash < pick.Stake))
{
game.CloseHeistOffers(close);
took = WithdrawFor(pick.Stake, false);
double cash = CashNow();
if (cash < pick.Stake)
{
SayOnce("heist cash " + pick.Name, "Heists: not enough cash on hand for " + pick + "'s stake - looking again in 30 minutes");
game.CloseHeistOffers(close);
nextHeistStart = DateTime.UtcNow.AddMinutes(30);
return;
}
if (!game.OpenTab(Tab.Heists)) return;
game.HeistsTop();
using (var f = game.Capture()) button = game.NoActiveHeist(f);
if (button == null) return;
game.PressStartAHeist(button);
pick = null;
for (int look = 0; look < 6 && pick == null; look++)
{
if (look > 0) { game.ScrollPage(0.55, -2); game.Wait(500); }
using (var f = game.Capture()) offers = game.ReadHeistOffers(f, out close);
if (offers != null) pick = offers.FirstOrDefault(o => o.Start != null && Game.SameHeist(o.Name, S.StartHeist))
?? offers.Where(o => o.Start != null && o.Level <= Math.Max(1, level)).OrderByDescending(o => o.Level).FirstOrDefault();
}
if (pick == null)
{
SayOnce("heist needs " + S.StartHeist, "Heists: " + S.StartHeist + " still shows NEEDS (its stake) after the cash came out - again in 10 minutes");
game.CloseHeistOffers(close);
return;
}
}
using (var f = game.Capture())
{
var again = game.ReadHeistOffers(f, out close);
var row = again == null ? null : again.FirstOrDefault(o => o.Start != null && Game.SameHeist(o.Name, pick.Name));
if (row == null) { Log("Heists: " + pick.Name + "'s START moved before it was pressed - again in 10 minutes"); game.CloseHeistOffers(close); return; }
pick = row;
}
if (pick.Energy <= 0 || h.Energy < pick.Energy)
{
Log("Heists: " + pick.Name + "'s energy " + (pick.Energy <= 0 ? "didn't read" : "(" + pick.Energy + ") is more than the bar's " + h.Energy) + " before START - again in 10 minutes");
game.CloseHeistOffers(close);
return;
}
game.PressHeistStart(pick);
game.Wait(1500);
List<HeistOffer> still;
using (var f = game.Capture()) still = game.ReadHeistOffers(f, out close);
string toast = still != null ? game.ReadToast(500, t => Parse.Has(t, "HEIST")) : null;
for (int look = 0; look < 3 && still != null && !HeistsUsedUp(toast); look++)
{
game.Wait(1500);
using (var f = game.Capture()) still = game.ReadHeistOffers(f, out close);
}
OcrLine none;
using (var f = game.Capture()) none = game.NoActiveHeist(f);
if (still == null && none == null)
{
heistEnergy = 0;
S.HeistsToday = HeistDay() + "|" + (HeistsToday() + 1);
SaveSettings();
if (last != null && last.Energy >= 0) last.Energy = Math.Max(0, Math.Min(last.Energy, h.Energy) - pick.Energy);
Count.HeistsStarted++;
Log("Heists: started " + pick);
SnapshotOnce("heist started");
nextHeistStart = DateTime.UtcNow.AddMinutes(Math.Max(10, pick.Minutes > 0 ? pick.Minutes : 30));
return;
}
heistEnergy = 0;
if (toast == null) toast = game.ReadToast(1000, t => Parse.Has(t, "HEIST"));
if (HeistsUsedUp(toast))
{
if (still != null) game.CloseHeistOffers(close);
HeistsUsedUpSaid(toast);
return;
}
Log("Heists: START on " + pick.Name + " didn't start it" + (still != null ? " (the window is still open - the day's 2 heists used?)" : "")
+ (!string.IsNullOrWhiteSpace(toast) ? " - the game said \"" + Cut(toast.Trim(), 120) + "\"" : "") + " - again in an hour");
game.Snapshot("heist start did nothing", 60);
if (still != null) game.CloseHeistOffers(close);
nextHeistStart = DateTime.UtcNow.AddMinutes(60);
}
finally { if (took) DepositBack(); }
}
public double JobXpPerEnergy() { return JobXpPerEnergy(last, Catalog); }
public double JobXpPerEnergy(Header h, List<JobInfo> catalog)
{
if (S.JobMode == JobMode.OneJob)
{
JobInfo one;
return OneJobWhy(h, catalog, out one) == null ? one.XpPerEnergy : 0;
}
int lv = JobLevel(h);
int maxE = h != null && h.EnergyMax > 0 ? h.EnergyMax : int.MaxValue;
double best = 0;
foreach (var j in catalog)
if (j.TierLevel <= lv && j.Cost > 0 && j.Cost <= maxE && j.Xp > 0) best = Math.Max(best, j.XpPerEnergy);
return best;
}
int HelpCost(HeistRow r) { return r.Energy > 0 ? r.Energy : helpEnergy; }
void DoFights()
{
if (!FamilyTakes()) { DoFights(100, AboveKept); return; }
if (DoFights(100, -1) > 0) givenSinceFights = 0;
}
const int AboveKept = -2;
int DoFights(int max, int reserve)
{
nextFights = DateTime.UtcNow.AddSeconds(30);
if (!game.OpenTab(Tab.Fight)) return 0;
if (game.ProfileShowing() && !game.CloseProfile()) { Log("Fights: a player's profile is open and didn't close - fights wait 5 minutes"); nextFights = DateTime.UtcNow.AddMinutes(5); return 0; }
int fought = 0, staminaLeft = -1;
bool missed = false;
bool rolledUp = false;
bool refreshed = false;
int moved = 0;
int calledOff = 0;
bool respectSaid = false;
int scoutsPaid = 0;
for (int i = 0; i < max; i++)
{
var h = ReadBarsSettled(i == 0 ? 6000 : 5000);
List<FightRow> rows;
bool hospital = false, war;
using (var f = game.Capture()) { rows = game.ReadFightRows(f, out war); if (i == 0) hospital = game.Hospitalized(f) && !h.HealthAtLeast(50); }
if (hospital && !HealWithItem()) { Hospital(h, fought > 0); return fought; }
if (S.RespectCycle && i % 3 == 0 && RespectTurn())
{
if (!game.OpenTab(Tab.Fight)) { nextFights = DateTime.UtcNow.AddMinutes(2); return fought; }
continue;
}
bool scout = S.ScoutFights && !Lowering;
if (BankAtMidFights(h))
{
Log("Fights: the cash on hand is " + M(h.Cash) + (S.BankAt > 0 ? ", over the " + M(S.BankAt) + " to bank at" : "") + " - banking between attacks");
DoBank();
if (!game.OpenTab(Tab.Fight)) { Log("Fights: the fight list didn't come back after the bank - fights wait 5 minutes"); nextFights = DateTime.UtcNow.AddMinutes(5); return fought; }
continue;
}
if (rows.Count == 0 && game.ProfileShowing())
{
if (!game.CloseProfile()) { Log("Fights: a player's profile is open and didn't close - fights wait 5 minutes"); nextFights = DateTime.UtcNow.AddMinutes(5); return fought; }
using (var f = game.Capture()) rows = game.ReadFightRows(f, out war);
}
if (war && S.WarTargetsOnly && !rows.Any(r => r.War) && !rolledUp)
{
rolledUp = true;
game.ScrollPage(0.5, 10);
game.Wait(500);
using (var f = game.Capture()) rows = game.ReadFightRows(f, out war);
}
SayWar(war, rows.Count);
if (war && S.WarTargetsOnly) rows = rows.Where(r => r.War).ToList();
if (h.Stamina < 0 && staminaLeft >= 0) h.Stamina = staminaLeft;
if (h.Stamina < 0 && game.RobloxBar && barFixTries < 2)
{
barFixTries++;
Log("Fights: the stamina bar didn't read and Roblox's own title bar is over the top bar - the bot clears it first");
if (FixRobloxBarNow()) continue;
}
if (h.Stamina < 0) { Log("Couldn't read stamina (" + game.BarLook(1) + ") - no attacks this round"); game.Snapshot("stamina unreadable at fights", 60); game.NoteFailure("the stamina bar didn't read"); break; }
if (reserve == AboveKept) reserve = Kept(h.StaminaMax);
else if (reserve < 0) reserve = Math.Max(0, h.Stamina - Kept(h.StaminaMax));
if (h.Stamina <= reserve)
{
if (S.FightItems && UseConsumable(true, "the fights") && game.OpenTab(Tab.Fight)) { staminaLeft = -1; continue; }
break;
}
int spare = h.Stamina - reserve;
if (scout && (ownAttack < 0 || DateTime.UtcNow - ownPowerAt > TimeSpan.FromMinutes(30)))
{
var own = game.ReadOwnProfile();
bool showing = game.ProfileShowing();
if (own == null && !showing && S.LastAttackPower > 0 && S.LastDefensePower > 0)
{
SayOnce("own profile", "Fights: your profile's numbers didn't read - using the Safehouse's attack " + S.LastAttackPower + " and defense " + S.LastDefensePower);
ownAttack = S.LastAttackPower; ownDefense = S.LastDefensePower; ownPowerAt = DateTime.UtcNow;
}
else if (own == null || showing)
{
SayOnce("own profile", "Fights: couldn't read your own attack and defense (your profile) - fights wait 5 minutes (\"Scout players first\" is on)");
nextFights = DateTime.UtcNow.AddMinutes(5);
return fought;
}
else { ownAttack = own.Attack; ownDefense = own.Defense; ownPowerAt = DateTime.UtcNow; }
}
var now = DateTime.UtcNow;
var ready = rows.Where(r => r.Ready && r.StaminaCost <= spare && !(fightSkip.ContainsKey(FightKey(r)) && fightSkip[FightKey(r)] > now)
&& !Spared(r) && !(scout && BadOdds(ScoutedNow(r))) && (Lowering || Wanted(r) != null || !LeftAlone(r, now))).ToList();
var notHit = ready.Where(r => (r.War && !Program.Whale) || !HitOnList(r)).ToList();
if (notHit.Count == 0 && rows.Count > 0 && !refreshed && !(war && S.WarTargetsOnly))
{
refreshed = true;
bool pressed;
if (NewFightList(rows, out pressed) || pressed) { i--; continue; }
}
if (notHit.Count > 0) ready = notHit;
if (ready.Count == 0)
{
if (fought == 0 && scout && rows.Any(r => r.Ready) && rows.Where(r => r.Ready).All(r => BadOdds(ScoutedNow(r))))
{
nextFights = now.AddMinutes(15);
SayOnce("all below the odds", "Fights: every player on the list is too strong for \"" + Settings.WinChanceName(S.FightWinChance) + "\" (scouted) - looking again in 15 minutes");
break;
}
if (fought == 0)
{
var free = rows.Select(r => FightKey(r)).Where(k => fightSkip.ContainsKey(k) && fightSkip[k] > now).Select(k => fightSkip[k]).ToList();
var wait = rows.Count == 0 ? TimeSpan.FromMinutes(2) : free.Count > 0 ? free.Min() - now : TimeSpan.FromMinutes(5);
nextFights = now + TimeSpan.FromSeconds(Math.Max(30, Math.Min(300, wait.TotalSeconds)));
SayOnce("no player to attack", "Fights: no player to attack right now - looking again in a few minutes");
}
break;
}
var cheap = ready.Where(r => r.StaminaCost == 1).ToList();
if (cheap.Count > 0) ready = cheap;
if (scout && scoutsPaid >= 3)
{
var known = ready.Where(r => ScoutedNow(r) != null).ToList();
if (known.Count == 0)
{
if (fought == 0) nextFights = now.AddMinutes(5);
Log("Fights: 3 players scouted this round and none worth attacking - looking again " + (fought == 0 ? "in 5 minutes" : "next round"));
break;
}
ready = known;
}
var target = Program.Whale ? TopDown(ready, Lowering).First()
: ready.OrderBy(r => Wanted(r) != null ? 0 : 1).ThenByDescending(r => BigPayer(r, now)).ThenByDescending(r => ExpectedRespect(r, now))
.ThenBy(r => scout && ScoutedNow(r) != null ? 0 : 1).ThenBy(r => r.Level < 0 ? 999 : r.Level).First();
if (Program.Whale) { }
else if (Wanted(target) != null) Log("Fights: " + target.Name + (target.Family.Length > 0 ? " (" + target.Family + ")" : "") + " is on your attack-first list (\"" + Wanted(target) + "\") - attacked first");
else if (BigPayer(target, now) > 0) Log("Fights: " + target.Name + " paid " + M(BigPayer(target, now)) + " in the last day (" + memory.Record(target.Name) + ") - attacked first");
else if (!respectSaid && ExpectedRespect(target, now) > 0)
{
respectSaid = true;
Log(string.Format(CultureInfo.InvariantCulture, "Fights: {0} is the best respect bet right now (+{1:0.0} expected)", target.Name, ExpectedRespect(target, now)));
}
if (scout && ScoutedNow(target) == null)
{
bool may = spare >= target.StaminaCost + 1, paid;
string why;
var p = game.ScoutPlayer(target, may, out paid, out why);
if (paid) { scoutsPaid++; staminaLeft = h.Stamina - 1; }
if (game.ProfileShowing() && !game.CloseProfile())
{
Log("Fights: a player's profile stayed open - fights wait 5 minutes");
nextFights = DateTime.UtcNow.AddMinutes(5);
return fought;
}
if (p == null)
{
if (!may && !paid && why == Game.NoStaminaToScout) { if (fought == 0) nextFights = DateTime.UtcNow.AddMinutes(1); break; }
fightSkip[FightKey(target)] = DateTime.UtcNow.AddMinutes(why == Game.NoProfile ? 6 * 60 : 30);
Log("Fights: couldn't scout a " + (target.Level > 0 ? "level " + target.Level + " " : "") + "player (" + why + ") - another one");
continue;
}
var s = scouted[FightKey(target)] = new Scouted { Attack = p.Attack, Defense = p.Defense, At = DateTime.UtcNow };
Log(string.Format(CultureInfo.InvariantCulture, "Fights: scouted a {0}player{1} - attack {2:N0}, defense {3:N0} (yours {4:N0} attack, {5:N0} defense): {6}% to win, {7}",
target.Level > 0 ? "level " + target.Level + " " : "", paid ? "" : " (scouted before)", s.Attack, s.Defense, ownAttack, ownDefense,
WinChance(ownAttack, s.Defense), BadOdds(s) ? "left alone, you asked for \"" + Settings.WinChanceName(S.FightWinChance) + "\"" : "attacking"));
continue;
}
if (!game.Attack(target))
{
if (++moved >= 3)
{
Log("Fights: the list kept changing under the ATTACK buttons - fights wait 2 minutes");
game.Snapshot("fight attack not there", 60);
nextFights = DateTime.UtcNow.AddMinutes(2);
return fought;
}
Log("Fights: the list changed (no ATTACK where it was) - reading it again");
game.Wait(1000);
i--;
continue;
}
moved = 0;
string result = game.FinishFight(15000);
if (result == null)
{
bool inHospital, banner;
using (var f = game.Capture()) { banner = game.Hospitalized(f); inHospital = Game.TopRightYouHospitalized(f); }
if (!inHospital) inHospital = Game.YouHospitalized(game.LastFightRefusal) || Game.YouHospitalized(game.LastFightToast);
if (!inHospital && game.AfterAttack != null)
using (var after = new Frame((System.Drawing.Bitmap)game.AfterAttack.Clone())) inHospital = Game.TopRightYouHospitalized(after);
if (!inHospital && banner && !h.HealthAtLeast(50)) inHospital = true;
else if (!inHospital && banner) game.Snapshot("hospital banner at good health", 60, failure: false);
if (inHospital)
{
if (HealWithItem() && game.OpenTab(Tab.Fight)) continue;
Hospital(h, fought > 0);
return fought;
}
string why = game.LastFightRefusal;
if (why == null && game.AfterAttack != null)
using (var after = new Frame((System.Drawing.Bitmap)game.AfterAttack.Clone())) why = Game.TopRightHospitalized(after);
if (Game.TargetHospitalized(why))
{
fightSkip[FightKey(target)] = DateTime.UtcNow.AddMinutes(10);
SayOnce("target in hospital", "Fights: a player you beat is in the hospital - the bot attacks another one");
continue;
}
if (Game.DailyLimit(why)) { DailyLimit(target); continue; }
int shield = Game.ShieldSeconds(why);
if (shield >= 0)
{
if (shield == 0) shield = 3600;
fightSkip[FightKey(target)] = DateTime.UtcNow.AddSeconds(Math.Min(shield, 24 * 3600) + 60);
Log("Fights: " + (target.Name.Length > 0 ? target.Name : "a level " + target.Level + " player") + " is shielded for another " + SpanText(shield) + " - left alone until then");
continue;
}
if (game.FightCalledOff)
{
fightSkip[FightKey(target)] = DateTime.UtcNow.AddMinutes(30);
string said = game.LastFightToast != null ? " (the game said: " + game.LastFightToast + ")" : "";
Log("Fights: the game closed the fight window with " + (target.Name.Length > 0 ? target.Name : "a level " + target.Level + " player")
+ " without a fight" + said + " - that player waits 30 minutes");
if (firstSeen.Add("fight called off")) game.Snapshot("fight called off", 0, true, picture: game.FightGone);
if (++calledOff >= 3)
{
Log("Fights: the game called off 3 fights in a row - fights wait 5 minutes");
game.Snapshot("fights called off", 60, failure: false, picture: game.FightGone);
nextFights = DateTime.UtcNow.AddMinutes(5);
return fought;
}
continue;
}
if (game.FightWindowStuck)
{
Log("Fights: the fight window stayed open with no result - fights wait 2 minutes");
game.Snapshot("fight window stuck", 60);
nextFights = DateTime.UtcNow.AddMinutes(2);
return fought;
}
why = why ?? game.LastFightToast;
if (!missed)
{
missed = true;
fightSkip[FightKey(target)] = DateTime.UtcNow.AddMinutes(2);
game.Snapshot("attack did not start", 60, failure: false, picture: game.AfterAttack);
continue;
}
Log("The attack didn't start twice - fights paused for 5 minutes" + (why != null ? " (the game said: " + why + ")" : ""));
game.Snapshot("attack did not start", 60);
nextFights = DateTime.UtcNow.AddMinutes(5);
return fought;
}
game.ForgetAfterAttack();
fought++;
missed = false;
calledOff = 0;
refreshed = false;
hitOnList.Add(FightKey(target));
staminaLeft = h.Stamina - target.StaminaCost;
Count.Fights++;
if (result.StartsWith("VICTORY")) Count.Wins++;
else if (result.StartsWith("DEFEAT")) Count.Losses++;
if (target.War) Log(string.Format("Fight vs war target {0}: {1}", target.Name.Length > 0 ? target.Name : "#" + (target.Index + 1), result));
else Log(string.Format("Fight vs level {0} player: {1}", target.Level, result));
bool won = result.StartsWith("VICTORY");
double took = won && target.Name.Length > 0 && (S.WhaleHunt || S.RememberPlayers) ? WinTook(h.Cash) : -1;
if (Lowering) Won(target, result, took); else Remember(target, won, result, took);
if (Lowering && won && !(S.WhaleHunt && took >= S.WhaleCash)) fightSkip[FightKey(target)] = DateTime.UtcNow.AddHours(6);
string summary = memory.RespectSummary(DateTime.UtcNow);
if (!summary.StartsWith("no ")) SayOnce("respect " + DateTime.UtcNow.ToString("yyyyMMddHH"), "Fights: " + summary);
if (S.WhaleHunt && S.WhaleHitsNow > 1 && won && target.Name.Length > 0 && max > 1 && took >= S.WhaleCash) fought += HitAgain(target, took, reserve, ref staminaLeft);
}
return fought;
}
double WinTook(double cashBefore)
{
if (game.LastFightCash >= 0) return game.LastFightCash;
if (game.LastFightPending) return -1;
var after = ReadBarsSettled(2000, x => x.Cash >= 0);
return cashBefore >= 0 && after.Cash >= 0 ? Math.Max(0, after.Cash - cashBefore) : -1;
}
int HitAgain(FightRow target, double took, int reserve, ref int staminaLeft)
{
string name = target.Name;
int hits = S.WhaleHitsNow;
Log("Fights: the win on " + name + " took " + M(took) + " - banking it and hitting again (up to " + hits + " hits, while each takes " + M(S.WhaleCash) + " or more)");
int more = 0;
for (int hit = 2; hit <= hits; hit++)
{
if (S.Bank && Can(Tab.Bank)) DoBank();
if (!game.OpenTab(Tab.Fight)) return more;
Game.AttackEntry e = null;
game.AttackHistoryToTop();
int end = Environment.TickCount + 60000, missing = Environment.TickCount + 25000;
List<Game.AttackEntry> read = null;
while (true)
{
using (var f = game.Capture()) read = game.ReadAttackHistory(f);
e = read.FirstOrDefault(x => Game.SamePlayer(x.Name, name));
if (e == null && read.Count > 0 && Game.CloseName(read[0].Name, name)) e = read[0];
if ((e != null && (e.Again != null || e.Limit)) || end - Environment.TickCount <= 0) break;
if (e == null && missing - Environment.TickCount <= 0) break;
if (e == null) game.AttackHistoryToTop();
game.Wait(2000);
}
if (e != null && e.Limit && e.Again == null) { DailyLimit(target); break; }
if (e == null)
{
Log("Fights: " + name + " isn't in the attack history (" + (read == null || read.Count == 0 ? "no entry of yours read" : "read: " + string.Join(", ", read.Select(x => "'" + x.Name + "'"))) + ") - on with the list");
game.Snapshot("attack history entry not found", 60);
break;
}
if (e.Again == null) { Log("Fights: no ATTACK AGAIN for " + name + " within a minute - on with the list"); break; }
var h = ReadBarsSettled(5000);
if (h.Stamina < 0 || h.Stamina - reserve < target.StaminaCost) break;
game.AttackAgain(e);
string result = game.FinishFight(15000);
if (result == null)
{
bool inHospital;
using (var f = game.Capture()) inHospital = game.Hospitalized(f) && !h.HealthAtLeast(50);
if (inHospital) { Hospital(h, true); break; }
if (Game.DailyLimit(game.LastFightRefusal)) { DailyLimit(target); break; }
Log("Fights: hitting " + name + " again didn't start" + (game.FightCalledOff ? " (the game closed the fight window without a fight)" : "")
+ (game.LastFightRefusal != null ? " (the game said: " + game.LastFightRefusal + ")" : "") + " - on with the list");
if (game.FightWindowStuck) { game.Snapshot("fight window stuck", 60); nextFights = DateTime.UtcNow.AddMinutes(2); }
break;
}
more++;
staminaLeft = h.Stamina - target.StaminaCost;
Count.Fights++;
if (result.StartsWith("VICTORY")) Count.Wins++;
else if (result.StartsWith("DEFEAT")) Count.Losses++;
double got = result.StartsWith("VICTORY") ? WinTook(h.Cash) : -1;
if (Lowering) Won(target, result, got); else Remember(target, result.StartsWith("VICTORY"), result, got);
Log(string.Format("Fight vs {0} again (hit {1} of {2}): {3}{4}", name, hit, hits, result, got >= 0 ? " - took " + M(got) : ""));
if (got < S.WhaleCash) break;
}
if (S.Bank && Can(Tab.Bank)) DoBank();
game.OpenTab(Tab.Fight);
return more;
}
FightMemory memory;
void DailyLimit(FightRow target)
{
fightSkip[FightKey(target)] = DateTime.UtcNow.AddHours(3);
SayOnce("daily limit " + FightKey(target) + DateTime.UtcNow.ToString("yyyyMMdd"), "Fights: the game's daily limit on "
+ (target.Name.Length > 0 ? target.Name : "a player") + " is reached - another one");
}
public FightMemory Memory { get { return memory; } }
void Won(FightRow r, string result, double took)
{
if (result.StartsWith("VICTORY"))
{
if (took < 0) took = game.LastFightCash >= 0 ? game.LastFightCash : game.LastFightPending ? -1 : Game.LastMoney(result);
if (took > 0) Count.FightCash += took;
}
}
void Remember(FightRow r, bool won, string result, double took)
{
Won(r, result, took);
var roll = new FightMemory.Rolls { OwnAttack = game.LastOwnAttack, TheirDefense = game.LastOtherDefense, OwnRoll = game.LastOwnRoll, TheirRoll = game.LastOtherRoll };
if (roll.OwnAttack > 0 && roll.TheirDefense > 0)
Log(string.Format(CultureInfo.InvariantCulture, "Fights: the fight window shows your attack was {0:N0} and {1}'s defense {2:N0}{3}",
roll.OwnAttack, r.Name.Length > 0 ? r.Name : "the player", roll.TheirDefense,
roll.OwnRoll > 0 && roll.TheirRoll > 0 ? string.Format(CultureInfo.InvariantCulture, " - rolled {0:N0} against {1:N0}", roll.OwnRoll, roll.TheirRoll) : ""));
if (!S.RememberPlayers || r.Name.Length < 3 || r.War || !(won || result.StartsWith("DEFEAT"))) return;
var sc = ScoutedNow(r);
memory.Add(r.Name, won, took, Parse.Signed(result, "Respect"), sc != null ? sc.Defense : -1, roll, DateTime.UtcNow);
}
double ExpectedRespect(FightRow r, DateTime now)
{
if (!S.ScoutFights || ownAttack <= 0 || memory == null) return 0;
var s = ScoutedNow(r);
if (s == null) return 0;
double p = WinChance(ownAttack, s.Defense) / 100.0;
return p * AverageRespect(r.Name, now, true, 25) + (1 - p) * AverageRespect(r.Name, now, false, -15);
}
double AverageRespect(string name, DateTime now, bool won, double fallback)
{
double v = memory.AverageRespect(name, now, won);
if (!double.IsNaN(v)) return v;
v = memory.GlobalAverageRespect(now, won);
return double.IsNaN(v) ? fallback : v;
}
string MemoryFile(string account)
{
string shared = Path.Combine(dir, "fight memory.txt");
if (string.IsNullOrEmpty(account)) return shared;
string mine = Accounts.Fights(dir, account);
try
{
Directory.CreateDirectory(Accounts.Folder(dir, account));
if (!File.Exists(mine) && File.Exists(shared)) File.Copy(shared, mine);
}
catch (Exception) { return shared; }
return mine;
}
double BigPayer(FightRow r, DateTime now)
{
if (!S.RememberPlayers || r.Name.Length < 3 || r.War) return -1;
double big = memory.BigWin(r.Name, now);
return big >= S.WhaleCash ? big : -1;
}
bool LeftAlone(FightRow r, DateTime now)
{
if (!S.RememberPlayers || r.Name.Length < 3 || r.War) return false;
var until = memory.LeftAloneUntil(r.Name, now);
if (until <= now) return false;
SayOnce("left alone " + FightKey(r) + until.ToString("yyyyMMddHH"), "Fights: " + r.Name + " beat you twice in a row - left alone until "
+ until.ToLocalTime().ToString("ddd HH:mm", CultureInfo.InvariantCulture) + ", then tried again");
return true;
}
string Wanted(FightRow r) { return Program.Whale ? null : AttackFirst(S.AttackFirst, r); }
internal static string AttackFirst(string list, FightRow r)
{
string e = NeverAttack(list, r);
return string.IsNullOrEmpty(e) ? null : e;
}
bool Spared(FightRow r)
{
string e = NeverAttack(S.NeverAttack, r);
if (e == null) return false;
SayOnce("never attack " + FightKey(r), "Fights: " + (r.Name.Length > 0 ? r.Name : "a player whose name didn't read") + (r.Family.Length > 0 ? " (" + r.Family + ")" : "")
+ " left alone - " + (r.Name.Length > 0 ? "\"" + e + "\" is on your never-attack list" : "your never-attack list can't be checked without the name"));
return true;
}
internal static string NeverAttack(string list, FightRow r)
{
var entries = NeverAttackEntries(list);
if (entries.Count == 0) return null;
int badge;
string name = Parse.Key(Game.SplitPrestige(r.Name, out badge)), fam = Parse.Key(r.Family);
foreach (var e in entries)
{
string k = Parse.Key(e);
if (k.Length == 0) continue;
if (name.Length > 0 && (name == k || (k.Length >= 6 && View.Distance(name, k) <= 1))) return e;
if (fam.Length > 0 && FamilyHolds(fam, k)) return e;
}
if (name.Length > 0) return null;
if (fam.Length > 0 && entries.All(e => !RobloxNameLike(e))) return null;
return "";
}
static bool FamilyHolds(string fam, string k)
{
if (fam == k || (k.Length >= 4 && fam.Contains(k))) return true;
if (k.Length >= 6)
for (int i = 0; i + k.Length - 1 <= fam.Length; i++)
for (int n = k.Length - 1; n <= k.Length + 1 && i + n <= fam.Length; n++)
if (View.Distance(fam.Substring(i, n), k) <= 1) return true;
return false;
}
static bool RobloxNameLike(string e) { return Regex.IsMatch((e ?? "").Trim(), @"^[A-Za-z0-9_]{3,20}$"); }
internal static List<string> NeverAttackEntries(string list)
{
return (list ?? "").Split(new[] { ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim())
.Where(x => x.Length > 0).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
bool atWar;
void SayWar(bool war, int rows)
{
if (war == atWar || (!war && rows == 0)) return;
atWar = war;
if (!war) { Log("Fights: no war on the Fight page any more - the whole list again"); return; }
Log(S.WarTargetsOnly ? "Fights: your family is at war - only war targets are attacked"
: "Fights: your family is at war (\"Only attack war targets\" is off: the whole list)");
SnapshotOnce("fights at war");
}
readonly Dictionary<string, DateTime> fightSkip = new Dictionary<string, DateTime>();
readonly HashSet<string> hitOnList = new HashSet<string>();
DateTime refreshBrokenUntil = DateTime.MinValue;
bool HitOnList(FightRow r) { return HitBefore(hitOnList, r); }
internal static IEnumerable<FightRow> TopDown(IEnumerable<FightRow> rows, bool lowering)
{
if (!lowering) return rows.OrderBy(r => r.Index);
return rows.OrderBy(r => r.NoFamily ? 1 : 0).ThenBy(r => r.Level < 0 ? 1 : 0).ThenByDescending(r => r.Level).ThenBy(r => r.Index);
}
internal static bool HitBefore(ICollection<string> hit, FightRow r)
{
string k = FightKey(r);
if (hit.Contains(k)) return true;
return r.Name.Length > 0 && k.Length >= 6 && hit.Any(x => !x.StartsWith("#") && x.Length >= 6 && View.Distance(x, k) <= 1);
}
DateTime lastRefreshPress = DateTime.MinValue;
bool NewFightList(List<FightRow> before, out bool pressed)
{
pressed = false;
if (DateTime.UtcNow < refreshBrokenUntil) return false;
var was = new HashSet<string>(before.Where(r => r.Name.Length > 0).Select(FightKey));
if (Program.Whale) was.UnionWith(hitOnList);
var since = DateTime.UtcNow - lastRefreshPress;
if (since < TimeSpan.FromSeconds(10.5)) game.Wait((int)(TimeSpan.FromSeconds(10.5) - since).TotalMilliseconds + 1);
if (!game.RefreshFightList())
{
SayOnce("refresh not found", "Fights: couldn't find the list's round arrow (refresh) - the same players are attacked again for now");
game.Snapshot("fight refresh not found", 60, failure: false);
NoNewList();
return false;
}
pressed = true;
lastRefreshPress = DateTime.UtcNow;
int start = Environment.TickCount;
while (true)
{
List<FightRow> rows;
bool loading;
using (var f = game.Capture()) { rows = game.ReadFightRows(f); loading = rows.Count == 0 && game.FightListLoading(f); }
int fresh = rows.Count(r => r.Name.Length > 0 && !HitBefore(was, r));
if (fresh > 0)
{
if (!Program.Whale || hitOnList.Count >= 64) hitOnList.Clear();
Log("Fights: everyone on the list had a hit - pressed refresh, " + fresh + " new players");
SnapshotOnce("fight list refreshed");
return true;
}
int ms = Environment.TickCount - start;
if (ms >= 30000 || (!loading && ms >= 3000)) break;
game.Wait(600);
}
SayOnce("refresh changed nothing", "Fights: refresh brought no new players - the same ones are attacked again for now");
game.Snapshot("fight refresh changed nothing", 60, failure: false);
NoNewList();
return false;
}
void NoNewList()
{
if (Program.Whale) hitOnList.Clear();
else refreshBrokenUntil = DateTime.UtcNow.AddMinutes(2);
}
sealed class Scouted { public int Attack, Defense; public DateTime At; }
readonly Dictionary<string, Scouted> scouted = new Dictionary<string, Scouted>();
int ownAttack = -1, ownDefense = -1;
DateTime ownPowerAt = DateTime.MinValue;
Scouted ScoutedNow(FightRow r)
{
Scouted s;
return scouted.TryGetValue(FightKey(r), out s) && DateTime.UtcNow - s.At < TimeSpan.FromHours(1) ? s : null;
}
bool BadOdds(Scouted s)
{
return s != null && ownAttack > 0 && WinChance(ownAttack, s.Defense) < Settings.WinChanceStep(S.FightWinChance);
}
internal const double FightRoll = 0.20;
internal static int WinChance(int attack, int defense)
{
if (attack <= 0 || defense <= 0) return 0;
double x1 = attack * (1 - FightRoll), x2 = attack * (1 + FightRoll);
double y1 = defense * (1 - FightRoll), y2 = defense * (1 + FightRoll);
double lo = Math.Max(x1, y1), hi = Math.Min(x2, y2);
double wins = Math.Max(0, x2 - Math.Max(x1, y2));
if (hi > lo) wins += ((hi - y1) * (hi - y1) - (lo - y1) * (lo - y1)) / (2 * (y2 - y1));
return (int)Math.Round(100 * Math.Max(0, Math.Min(1, wins / (x2 - x1))));
}
public bool CollectFights;
static string FightKey(FightRow r) { return r.Name.Length > 0 ? Parse.Key(r.Name) : "#" + r.Index + " L" + r.Level; }
void Hospital(Header h = null, bool afterFights = false)
{
string health = h != null && h.Health >= 0 && h.HealthMax > 0 ? " (health " + h.Health + "/" + h.HealthMax + ")" : "";
if (afterFights || DateTime.UtcNow - hospitalSaid > TimeSpan.FromMinutes(30))
Log("You're in the hospital" + health + " - player fights wait until your health is back, next look in 10 minutes");
hospitalSaid = DateTime.UtcNow;
nextFights = DateTime.UtcNow.AddMinutes(10);
}
DateTime hospitalSaid = DateTime.MinValue;
string pointsSaid;
string pointsStopped;
string PointsStopKey() { return S.Account + "|" + S.PointsStat + "|" + S.PointsStop(S.PointsStat); }
bool feeSaid;
int bankUnconfirmed;
bool bankHiddenSaid;
int bankFee = -1;
string feeLookedUp;
int BankFee()
{
if (bankFee >= 0) return bankFee;
string name = S.Account.Length > 0 ? S.Account : last != null ? (last.Name ?? "").Trim() : "";
if (name.Length == 0 || feeLookedUp == name) return bankFee;
feeLookedUp = name;
var p = Profile.Load(Profile.FileFor(dir, name));
if (p != null && p.BankFee >= 0) bankFee = p.BankFee;
return bankFee;
}
bool CanWithdraw() { return S.Bank && Can(Tab.Bank); }
double Spendable(Header h) { return h.Cash < 0 ? h.Cash : h.Cash + (CanWithdraw() && h.Banked > 0 ? h.Banked : 0); }
bool cashOut;
bool WithdrawFor(double need, bool bigBuy) { double floor; return WithdrawKeeping(need, bigBuy, need, 0, out floor); }
bool WithdrawUpTo(double need, bool bigBuy, double upTo) { double floor; return WithdrawKeeping(need, bigBuy, upTo, 0, out floor); }
bool WithdrawKeeping(double need, bool bigBuy, double upTo, double keep, out double floor)
{
floor = Math.Max(0, keep);
outOnHand = outRead = -1;
if (!CanWithdraw() || need <= 0) return false;
if (!bigBuy && BankFee() != 0) return false;
double cash = CashNow();
if (cash < 0 || cash >= need) return false;
double banked;
using (var f = game.Capture()) banked = game.ReadHeader(f).Banked;
long amount;
bool all;
double after;
if (!WithdrawPlan(need, upTo, cash, banked, keep, out amount, out all, out after)) return false;
nextBank = DateTime.UtcNow;
if (amount > 0)
{
string why;
bool seen;
string typed = game.WithdrawAmount(amount, out why, out seen);
if (typed != null && seen)
{
Log("Bank: took " + M(amount) + " out for a purchase with the typed amount (" + typed + ")");
SnapshotOnce("bank withdraw amount");
cashOut = true;
floor = after;
outOnHand = cash + amount;
outRead = cash;
return true;
}
if (typed != null)
{
cashOut = true;
Log("Bank: WITHDRAW pressed for " + M(amount) + ", but " + (typed == Game.BankUnread ? "the top bar didn't read" : "the top bar shows no cash coming out") + " - no purchase this time");
game.Snapshot("bank withdraw amount unconfirmed", 60);
}
else
{
SayOnce("bank amount " + why.Split('(', '"')[0], "Bank: the typed amount wasn't used (" + why + ") - no purchase this time (the bank is never emptied for one)");
game.Snapshot("bank amount not used", 60);
}
return false;
}
if (!all) return false;
string msg = game.WithdrawAll();
if (msg == null) return false;
if (msg == Game.BankUnchanged)
{
Log("Bank: WITHDRAW ALL pressed, but the top bar shows no cash coming out");
game.Snapshot("bank withdraw did nothing", 60);
return false;
}
Log("Bank: took the cash out for a purchase (" + msg.Trim() + ")");
SnapshotOnce("bank withdraw");
cashOut = true;
floor = after;
outOnHand = outRead = cash + banked;
return true;
}
internal static bool WithdrawPlan(double need, double upTo, double cash, double banked, double keep, out long amount, out bool all, out double floor)
{
amount = 0;
all = false;
floor = Math.Max(0, keep);
if (need <= 0 || cash < 0 || cash >= need) return false;
double target = WithdrawTarget(need, upTo, cash, banked, keep);
if (target <= 0) return false;
amount = AmountToWithdraw(target, cash, banked);
if (amount <= 0 && upTo > need) amount = AmountToWithdraw(need, cash, banked);
if (amount > 0)
{
floor = KeepOnHand(keep, banked - amount);
if (floor > 0 && FreeCash(cash, banked, keep) < need * 1.01) { amount = 0; floor = Math.Max(0, keep); return false; }
return true;
}
amount = 0;
if (cash + banked < (need + Math.Max(0, keep)) * 1.005) return false;
all = true;
return true;
}
internal static double KeepOnHand(double keep, double banked) { return Math.Max(0, keep - Math.Max(0, banked)); }
internal static double FreeCash(double cash, double banked, double keep) { return cash < 0 ? -1 : cash + Math.Max(0, banked) - Math.Max(0, keep); }
internal static double WithdrawTarget(double need, double upTo, double cash, double banked, double keep)
{
if (need <= 0 || cash < 0 || banked < 0) return 0;
double free = FreeCash(cash, banked, keep);
if (free < need) return 0;
return Math.Min(Math.Max(need, upTo), free);
}
double KeepFloor(double keep, bool anyFee) { double floor = KeepFloorRead(keep, anyFee); return floor < 0 ? keep : floor; }
double KeepFloorRead(double keep, bool anyFee)
{
if (keep <= 0) return 0;
if (!CanWithdraw() || (!anyFee && BankFee() != 0)) return keep;
double banked;
using (var f = game.Capture()) banked = game.ReadHeader(f).Banked;
return banked < 0 ? -1 : KeepOnHand(keep, banked);
}
internal static long AmountToWithdraw(double target, double cash, double banked)
{
if (target <= 0 || cash < 0 || banked <= 0) return 0;
double x = target - cash + target * 0.03;
if (x < 1) x = 1;
double step = Math.Pow(10, Math.Max(0, Math.Floor(Math.Log10(x)) - 2));
x = Math.Ceiling(x / step) * step;
if (x >= banked * 0.95 || x > 9e14) return 0;
return (long)x;
}
void DepositBack(double spent = -1)
{
double onHand = outOnHand, read = outRead;
outOnHand = outRead = -1;
if (BankFee() != 0) { nextBank = DateTime.UtcNow; return; }
var bankedAt = lastBank;
DoBank();
double back = game.DepositCashBefore, gone = lastBank != bankedAt ? OnHandGone(onHand, read, spent, back) : 0;
if (gone > 0)
Log("Bank: " + M(gone) + " of the cash taken out is gone - " + M(back) + " went back in, " + M(onHand - spent) + " was left after the purchase (an attack while it was on hand?)");
}
double outOnHand = -1, outRead = -1;
internal static double OnHandGone(double onHand, double read, double spent, double back)
{
if (onHand <= 0 || spent < 0 || back < 0) return 0;
double expect = onHand - spent;
if (expect <= 0) return 0;
double gone = expect - back;
return gone > Math.Max(expect * 0.01, (Math.Max(0, read) + back) * 0.01) ? gone : 0;
}
static bool BigBuy(string task) { return task == "properties" || task == "safehouse upgrade"; }
void BankNext(List<string> due, int i)
{
if (!cashOut || !S.Bank || !Can(Tab.Bank)) return;
if (i + 1 < due.Count && (due[i + 1] == "bank" || (BankFee() != 0 && BigBuy(due[i + 1])))) return;
int at = due.IndexOf("bank", i + 1);
if (at < 0 && due.IndexOf("bank") >= 0) return;
if (at >= 0) due.RemoveAt(at);
due.Insert(i + 1, "bank");
}
const int BankMinutes = 1;
bool BankAtMidFights(Header h)
{
if (!S.Bank || h.Cash <= 0 || !Can(Tab.Bank) || AutoRollHoldsBank()) return false;
var since = DateTime.UtcNow - lastBank;
return S.BankAt > 0 ? h.Cash >= S.BankAt && since >= TimeSpan.FromSeconds(30) : since >= TimeSpan.FromMinutes(BankMinutes);
}
internal bool BankJustRan(DateTime now) { return !cashOut && now - lastBank < TimeSpan.FromSeconds(15); }
void DoBank()
{
if (AutoRollHoldsBank())
{
SayOnce("bank auto roll " + autoRun.StartedAt.Ticks, "Bank: waits while the crew's Auto Roll rolls on the cash on hand");
nextBank = DateTime.UtcNow.AddMinutes(1);
return;
}
int fee;
string msg = game.DepositAll(out fee);
if (fee >= 0) bankFee = fee;
else if (msg != null && Game.FeeIn(msg) >= 0) bankFee = Game.FeeIn(msg);
lastBank = DateTime.UtcNow;
nextBank = DateTime.UtcNow.AddMinutes(BankMinutes);
if (fee > 0 && !feeSaid)
{
Log("Bank: each deposit costs " + fee + "% without the No Bank Fees pass");
feeSaid = true;
}
if (msg == null) { if (game.DepositCashBefore == 0) cashOut = false; return; }
if (msg == Game.BankHidden)
{
cashOut = false;
if (!bankHiddenSaid) Log("Bank: DEPOSIT ALL pressed " + msg);
bankHiddenSaid = true;
Neutral();
return;
}
if (msg == Game.BankUnchanged || msg == Game.BankUnread)
{
Log("Bank: DEPOSIT ALL pressed " + msg);
if (++bankUnconfirmed >= 2) game.Snapshot("bank deposit unconfirmed", 60);
else nextBank = DateTime.UtcNow;
return;
}
bankUnconfirmed = 0;
cashOut = false;
if (NothingToDeposit(msg)) { Log("Bank: nothing to deposit"); return; }
Count.Deposits++;
bool toast = !msg.StartsWith("the notification didn't show");
string line = toast ? TidyDeposit(msg) : "deposited (" + msg + ")";
if (toast && !line.StartsWith("deposited $") && game.DepositCashBefore > 0) line += " - about " + M(game.DepositCashBefore) + " (the cash on hand before it)";
Log("Bank: " + line);
NoteBankFee(msg);
}
public static bool NothingToDeposit(string msg)
{
return Regex.IsMatch(msg ?? "", @"n\s?o\s?t\s?h\s?i\s?n\s?g\s*t\s?o\s*d\s?e\s?p\s?o\s?s", RegexOptions.IgnoreCase);
}
public static string TidyDeposit(string msg)
{
msg = msg ?? "";
var fee = Regex.Match(msg, @"\(\s*(no\s*fee|\$\s?[\d,.]+\s?[KMBT]?\s*fee)\s*\)", RegexOptions.IgnoreCase);
string feeText = fee.Success ? " (" + Regex.Replace(fee.Groups[1].Value, @"\s+", " ").ToLowerInvariant().Replace("$ ", "$") + ")" : "";
var m = Regex.Match(fee.Success ? msg.Remove(fee.Index, fee.Length) : msg, @"\$\s?\d(?:[\d,.]|\s(?=[\d,.]*\d))*(?:\s?[KMBT]\b)?", RegexOptions.IgnoreCase);
string amount = m.Success ? m.Value.Replace(" ", "") : "";
amount = amount.TrimEnd('.');
if (amount.EndsWith(",")) amount = "";
if (amount.Length == 0) return Regex.IsMatch(msg, @"dep\w*s\w*ted|ba\s?n\s?k\s?e\s?d", RegexOptions.IgnoreCase) ? "deposited" + feeText : msg.Trim();
return "deposited " + amount + feeText;
}
string feeNotedFor;
void NoteBankFee(string msg)
{
string name = last != null ? (last.Name ?? "").Trim() : "";
int fee = Game.FeeIn(msg);
if (name.Length == 0 || fee < 0 || feeNotedFor == name) return;
feeNotedFor = name;
string file = Profile.FileFor(dir, name);
var p = Profile.Load(file);
if (p == null || p.BankFee >= 0) return;
p.BankFee = fee;
p.BankFeeFrom = "a deposit";
p.BankLine = msg.Trim();
try { p.Save(file); } catch (Exception) { return; }
Log(fee == 0 ? "(The deposit says there's no bank fee: noted in your game check)" : "(The deposit says each one costs " + fee + "%: noted in your game check)");
var h = ProfileChanged;
if (h != null) h(p);
}
public event Action<string> AccountChanged;
string accountSeen;
int accountVotes;
DateTime accountFirstVote;
readonly object accountLock = new object();
bool NoteAccount(string name)
{
if (!Accounts.Plausible(name)) return false;
name = Accounts.Known(dir, name.Trim());
lock (accountLock)
{
string whose = Whose();
if (!IsOther(name, whose))
{
accountSeen = null; accountVotes = 0;
if (S.Account.Length == 0) Adopt(name);
return false;
}
var now = DateTime.UtcNow;
bool known = Directory.Exists(Accounts.Folder(dir, name));
bool strict = !known || Accounts.Same(name, whose);
bool same = accountSeen != null && (strict ? name == accountSeen : Accounts.Same(name, accountSeen));
if (!same) { accountSeen = name; accountVotes = 1; accountFirstVote = now; return true; }
accountVotes++;
if (accountVotes < (strict ? 8 : 3) || now - accountFirstVote < TimeSpan.FromSeconds(strict ? 60 : 10)) return true;
accountSeen = null; accountVotes = 0;
if (strict)
{
string again = Accounts.Known(dir, (NameAgain ?? ReadNameAgain)());
if (!Accounts.Alike(again, name))
{
SayOnce("account again " + name + "|" + again, "Account: the top bar read \"" + name + "\" for a minute, but once more at another zoom \""
+ (again ?? "nothing") + "\" - not taken as another account");
return true;
}
}
if (S.Account.Length == 0) S.Account = whose;
if (!known && Accounts.Part(name, S.Account)) RenameAccount(name);
else SwitchAccount(name);
return true;
}
}
bool OtherAccount(string name)
{
if (!Accounts.Plausible(name)) return false;
lock (accountLock) return IsOther(Accounts.Known(dir, name.Trim()), Whose());
}
string Whose()
{
if (!ownerLooked && S.Account.Length == 0) { ownerLooked = true; ownerHint = Accounts.OwnerHint(dir); }
return S.Account.Length > 0 ? S.Account : ownerHint ?? "";
}
bool IsOther(string name, string whose)
{
if (whose.Length > 0 && Accounts.Alike(name, whose)) return false;
bool own = whose.Length > 0 && !string.Equals(Profile.Key(name), Profile.Key(whose), StringComparison.OrdinalIgnoreCase)
&& Directory.Exists(Accounts.Folder(dir, name));
return whose.Length > 0 && (own || !Accounts.Same(name, whose));
}
internal Func<string> NameAgain = null;
string ReadNameAgain()
{
if (game == null) return null;
using (var f = game.Capture()) return game.NameAgain(f);
}
string ownerHint;
bool ownerLooked;
void Adopt(string name)
{
string ini = Accounts.Ini(dir, name), jobs = Accounts.Jobs(dir, name);
bool back = File.Exists(ini);
if (back)
{
var o = Settings.Load(ini);
S.TakeAccountFields(o);
if (o.Unreadable != null) S.Unreadable = o.Unreadable;
level = S.LastLevel;
crewUnequipped = S.CrewGearOff;
ownOpenBefore = S.CrewOpenBefore;
}
if (catalogBuiltIn && File.Exists(jobs))
{
catalogBuiltIn = false;
Catalog = GameData.LoadCatalog(jobs);
CatalogView = Catalog.Select(j => j.Copy()).ToList();
var cc = CatalogChanged;
if (cc != null) cc();
}
S.Account = name;
SaveAccount();
Log(back ? "Account: " + name + ". Its own settings are back (accounts\\" + Profile.Key(name) + ")."
: "Account: " + name + ". Its settings and job list are kept for it from now on (accounts\\" + Profile.Key(name) + ").");
var h = AccountChanged;
if (h != null) h(name);
}
void SaveAccount()
{
if (S.Account.Length == 0) return;
try
{
Directory.CreateDirectory(Accounts.Folder(dir, S.Account));
S.Save(Accounts.Ini(dir, S.Account), true);
SavedOwn();
if (lead == null) S.Save(Path.Combine(dir, "IdleMafiaBot.ini"));
GameData.SaveCatalog(Accounts.Jobs(dir, S.Account), Catalog);
GameData.SaveCatalog(catalogFile, Catalog);
}
catch (Exception e) { Log("Couldn't save " + S.Account + "'s settings: " + e.Message); }
}
void SwitchAccount(string name)
{
string before = S.Account;
FoldTotals(false);
SaveAccount();
string ini = Accounts.Ini(dir, name), jobs = Accounts.Jobs(dir, name);
bool known = File.Exists(ini);
var theirs = known ? Settings.Load(ini) : Accounts.NewFrom(S);
S.TakeAccountFields(theirs);
if (theirs.Unreadable != null)
{
S.Unreadable = theirs.Unreadable;
Log("Couldn't read " + name + "'s settings (" + theirs.Unreadable + "). It plays with a new player's settings and nothing is saved this time. Start the bot again to get them back.");
}
S.Account = name;
lock (totalsLock) totalsFolded = Count.Copy();
catalogBuiltIn = !File.Exists(jobs);
Catalog = GameData.LoadCatalog(jobs);
CatalogView = Catalog.Select(j => j.Copy()).ToList();
level = S.LastLevel;
ResetAccountState(before);
SaveAccount();
Log(known ? "Account: " + name + " is playing now. Its own settings and job list are back (" + before + "'s are kept)."
: "Account: " + name + " is new here: it starts with " + before + "'s settings and the game's job list.");
var cc = CatalogChanged;
if (cc != null) cc();
var h = AccountChanged;
if (h != null) h(name);
}
void RenameAccount(string name)
{
string before = S.Account, from = Accounts.Folder(dir, before), to = Accounts.Folder(dir, name);
try { if (Directory.Exists(from) && !Directory.Exists(to)) Directory.Move(from, to); }
catch (Exception e) { Log("Couldn't move " + before + "'s folder to " + name + "'s: " + e.Message); }
S.Account = name;
memory = new FightMemory(MemoryFile(S.Account));
SaveAccount();
Log("Account: " + name + " - the same account as \"" + before + "\" (a part of its name was read before). Its settings and job list go on under the whole name (accounts\\" + Profile.Key(name) + ").");
var h = AccountChanged;
if (h != null) h(name);
}
void ResetAccountState(string before)
{
DateTime now = DateTime.UtcNow;
nextOps = nextBosses = nextPlaytime = nextContracts = nextCrates = nextHeists = nextFights = nextBank = nextPoints = nextJobs = nextLearn = nextGameStats = now;
nextBosses = BossesDoneUntil(now);
ResetExpState();
ResetMenuNumbers();
nextEquip = nextShop = nextSafehouse = nextProperties = nextTutorialLook = nextMarket = now;
nextBriefcases = nextEvents = nextGlobalBoss = nextKnock = now; ResetGlobalBoss(); badgeSig = eventsSig = null; eventsOn.Clear(); briefcasesSaid = null; briefcasesStopped = false;
nextHeistStart = now; heistEnergy = 0;
ResetFamily(now);
LoadDefence();
lastBank = DateTime.MinValue;
learnedThisRun = false; foldDue = true;
last = null;
propPrice = 0; propLotsOpen = -1; propReplace = false; replaceSaid = null; propWontBuild.Clear();
upgradePrice = -1; nextUpgrade = now; upgradeSaid = ""; upgradeNeedsLevel = -1;
levelBelow = int.MaxValue;
nextCrew = nextCrewSlot = now; crewSlotPrice = -1; crewSaid = ""; bankFee = -1; feeLookedUp = null; crewInfo = null;
nextReroll = now; rerollCrew = null; rerollEmpty = false; emptiedRarity = -1; emptiedByStats = false; rerollCash = -1; rerollSaid = null; rerollShort = false;
listShortFails = 0; hiredRead = -1; rerollSkip.Clear(); sortStuck.Clear(); sortStuckRounds = 0;
autoRun = null; autoRollOff = false; autoRollGrey = 0; autoRollNext = autoRollRest = rollStopNext = DateTime.MinValue; rollStopTries = 0;
if (Running && !CheckOrScan) RestoreRun();
lock (jobAside) jobAside.Clear(); jobMisses.Clear();
ownStats.Clear(); ownReadAt = ownConfirmAt = DateTime.MinValue; crewUnequipped = S.CrewGearOff; ownOpenBefore = S.CrewOpenBefore; gearBackFails = 0; gearBackHold = DateTime.MinValue; ownUnknownLeft = -1; ownJustRead = false;
energyRegen.Forget(); staminaRegen.Forget();
ownAttack = ownDefense = -1; ownPowerAt = DateTime.MinValue; scouted.Clear(); fightSkip.Clear(); hitOnList.Clear(); refreshBrokenUntil = DateTime.MinValue;
memory = new FightMemory(MemoryFile(S.Account));
crewHirePrice = new double[] { 10000, 10e6, 10e9 };
tutorialHint = tutorialItem = tutorialAction = tutorialLogged = null;
tutorialOver = tutorialFought = false;
feeSaid = saidBestGear = saidDeposit = false;
saidOnce.Clear();
levelSaid = levelSeen = levelSaidXp = -1; summaryLevel = -1;
streaks.Clear(); ShowStuck();
if (game != null) { game.ResetAccount(before, S.Account); game.SeedMax(S.BarMax); }
}
bool TutorialOn { get { return tutorialHint != null; } }
string tutorialHint, tutorialItem, tutorialAction;
DateTime tutorialSince, nextTutorialLook;
bool tutorialOver, tutorialFought;
void LookForTutorial()
{
if (tutorialOver || game == null || (level > 8 && tutorialHint == null) || DateTime.UtcNow < nextTutorialLook) return;
nextTutorialLook = DateTime.UtcNow.AddSeconds(15);
Game.TutorialBox box;
using (var f = game.Capture()) box = game.ReadTutorial(f);
if (box == null)
{
if (tutorialHint != null && tutorialFought) { tutorialOver = true; Log("Tutorial: finished"); }
tutorialHint = null; tutorialAction = null; tutorialItem = null;
return;
}
bool changed = box.Hint != tutorialHint;
tutorialHint = box.Hint;
if (changed && box.Hint != tutorialLogged)
{
tutorialLogged = box.Hint;
tutorialSince = DateTime.UtcNow;
Log("Tutorial: \"" + box.Hint + "\"");
}
bool stuck = DateTime.UtcNow - tutorialSince > TimeSpan.FromMinutes(30);
if (box.Done != null) { tutorialAction = "done"; return; }
if (stuck && box.Skip != null) { tutorialAction = "skip"; return; }
string u = box.Hint.ToUpperInvariant();
var buy = Regex.Match(box.Hint, @"Buy the (.+?)\s*[.!]", RegexOptions.IgnoreCase);
bool fightStep = !buy.Success && !u.Contains("PROPERT") && (u.Contains("FIGHT PAGE") || u.Contains("REAL PLAYER") || u.Contains("TO ATTACK"));
if (fightStep) { tutorialAction = tutorialFought ? null : "fight"; return; }
if (!changed) return;
tutorialItem = buy.Success ? buy.Groups[1].Value.Trim() : null;
tutorialAction = null;
if (u.Contains("PROPERT")) { propPrice = 0; nextProperties = DateTime.UtcNow; }
else if (tutorialItem != null || u.Contains("SHOP")) { nextShop = DateTime.UtcNow; game.MenuChanged(Tab.Shop); }
else if (u.Contains("EQUIP") || u.Contains("CREW")) nextEquip = DateTime.UtcNow;
else if (!u.Contains("JOB") && !u.Contains("EXP")) SnapshotOnce("tutorial step " + Regex.Replace(box.Hint, "[^A-Za-z ]", "").Trim());
}
string tutorialLogged;
void DoTutorial()
{
string what = tutorialAction;
tutorialAction = null;
nextTutorialLook = DateTime.UtcNow;
if (what == "fight")
{
int n = DoFights(1, 0);
tutorialFought = n > 0;
if (!tutorialFought) nextTutorialLook = DateTime.UtcNow.AddMinutes(2);
return;
}
Game.TutorialBox box;
using (var f = game.Capture()) box = game.ReadTutorial(f);
if (box == null) return;
if (what == "done" && box.Done != null) { game.PressTutorial(box.Done); Log("Tutorial: DONE pressed"); }
else if (what == "skip" && box.Skip != null) { game.PressTutorial(box.Skip); Log("Tutorial: skipped \"" + box.Hint + "\" (stuck on it for 30 minutes)"); }
}
DateTime nextProperties;
double propPrice;
int propLotsOpen = -1;
readonly Dictionary<string, DateTime> propWontBuild = new Dictionary<string, DateTime>();
bool WontBuild(Game.PropertyCard c)
{
DateTime until;
return c != null && propWontBuild.TryGetValue(Parse.Key(c.Name), out until) && DateTime.UtcNow < until;
}
void DoProperties()
{
nextProperties = DateTime.UtcNow.AddMinutes(2);
propRecheck = false;
propReplace = false;
bool took = false;
try
{
var cards = game.ReadBuildCards();
if (cards == null || cards.Count == 0)
{
SayReplace("the BUILD page's cards didn't read. The bot looks again in 30 minutes.");
propPrice = 0; propRecheck = true;
nextProperties = DateTime.UtcNow.AddMinutes(30);
return;
}
string seen = string.Join("|", cards.Where(c => (c.Gold > 0 || c.Income > 0) && c.Name.Trim().Length > 0)
.Select(c => (c.Gold > 0 ? "g:" : "") + c.Name.Trim()).Distinct().ToArray());
if (seen.Length > 0 && seen != S.PropertySeen) { S.PropertySeen = seen; SaveSettings(); }
int open;
var lots = game.ReadBlock(cards.Where(c => c.Gold > 0).ToList(), out open);
if (lots == null || open < 0)
{
SayReplace("YOUR BLOCK didn't read. The bot looks again in 30 minutes.");
game.Snapshot("property lots not read", 60);
propPrice = 0; propRecheck = true;
nextProperties = DateTime.UtcNow.AddMinutes(30);
return;
}
propLotsOpen = open;
double spend = SpendNow();
var planCards = cards.Where(c => !WontBuild(c)).ToList();
var plan = PlanProperty(planCards, lots, open, S.GoldLots, () => game.GoldLotsOwned(lots), spend, S.PropertyOnly);
if (plan.Target == null)
{
propPrice = 0; propRecheck = true;
nextProperties = DateTime.UtcNow.AddMinutes(plan.Why != null && plan.Why.Contains("didn't read") ? 30 : 360);
SayReplace(plan.Why ?? "every lot holds the best property you can build. The bot looks again in 6 hours or after a level-up.");
return;
}
propPrice = plan.Target.Price;
propReplace = plan.Replace != null;
string what = plan.Replace != null ? plan.Target + " in place of a " + plan.Replace : plan.Target + " on a free lot";
if (spend < 0 || spend < plan.Target.Price * 1.01)
{
SayReplace("saving up for " + what);
return;
}
var worst = plan.Replace;
if (worst != null && (worst = SecondBlockRead(worst, lots)) == null) return;
double need = plan.Target.Price * 1.01;
took = WithdrawFor(need, true);
double cash = plan.Replace != null ? CashSure() : CashNow();
if (cash < 0 || cash < need) { SayReplace("saving up for " + what); return; }
if (worst != null && !Replace(worst, plan.Target, ref took)) return;
string why;
bool grey;
if (!game.BuildCard(plan.Target, out why, out grey))
{
if (grey && plan.Target.Name.Trim().Length > 0)
{
propWontBuild[Parse.Key(plan.Target.Name)] = DateTime.UtcNow.AddHours(12);
Log("Properties: " + plan.Target.Name + " won't build (" + why + ") - left out for 12 hours, the next best goes on the lot");
propRecheck = true;
if (plan.Replace != null) propLotsOpen = Math.Max(1, propLotsOpen);
nextProperties = DateTime.UtcNow;
return;
}
if (plan.Replace != null) { Log("Properties: the lot is free but nothing was built yet (" + why + ") - the next look builds on it"); propLotsOpen = Math.Max(1, propLotsOpen); }
else Log("Properties: didn't build " + plan.Target + " - " + why);
nextProperties = DateTime.UtcNow.AddMinutes(plan.Replace != null ? 1 : 10);
return;
}
PropertyBuilt(plan.Target, worst);
if (propLotsOpen > 0) propLotsOpen--;
replaceSaid = null;
propRecheck = true;
nextProperties = DateTime.UtcNow;
}
catch (Exception) { propRecheck = true; throw; }
finally { if (took) DepositBack(); }
}
double SpendNow()
{
double cash = CashNow();
if (cash < 0) return -1;
if (!CanWithdraw()) return cash;
double banked;
using (var f = game.Capture()) banked = game.ReadHeader(f).Banked;
return cash + Math.Max(0, banked);
}
internal sealed class PropertyPlan
{
public Game.PropertyCard Target;
public Game.BlockLot Replace;
public string Why;
}
internal static PropertyPlan PlanProperty(List<Game.PropertyCard> cards, List<Game.BlockLot> lots, int lotsOpen, int goldLots, Func<int> goldOwned, double spend, string only = "")
{
foreach (var c in cards.Where(c => c.Income > 0 && c.Gold <= 0))
{
var same = lots.Where(l => l.Gold <= 0 && l.Income > 0 && SameProperty(l.Name, c.Name)).Select(l => l.Income).OrderBy(x => x).ToList();
if (same.Count == 0) continue;
double mid = same[same.Count / 2];
if (Math.Abs(c.Income - mid) > mid * 0.1) c.Income = mid;
}
var cash = cards.Where(c => c.Income > 0 && c.Gold <= 0 && c.Price > 0).ToList();
if (!string.IsNullOrEmpty(only) && only.Trim().Length > 0)
{
var pickedGold = cards.Where(c => c.Gold > 0 && c.Price > 0 && SameProperty(c.Name, only)).OrderBy(c => c.Price).FirstOrDefault();
if (pickedGold != null)
{
if (lotsOpen > 0) return new PropertyPlan { Target = pickedGold };
var weakCash = Game.WorstLot(lots);
if (weakCash != null) return new PropertyPlan { Target = pickedGold, Replace = weakCash };
var weakGold = Game.WorstGoldLot(lots);
if (weakGold != null && weakGold.Gold < pickedGold.Gold) return new PropertyPlan { Target = pickedGold, Replace = weakGold };
if (!lots.Any()) return new PropertyPlan { Why = "every lot is built on, and YOUR BLOCK's lots didn't read. The bot looks again in 30 minutes." };
return new PropertyPlan();
}
cash = cash.Where(c => SameProperty(c.Name, only)).ToList();
if (cash.Count == 0)
return new PropertyPlan { Why = only.Trim() + " (your pick) isn't on the BUILD page to build: not open yet, or its name didn't read. The bot looks again in 30 minutes." };
}
var gold = cards.Where(c => c.Gold > 0 && c.Price > 0).ToList();
Func<Game.PropertyCard, bool> covered = c => spend >= 0 && c.Price * 1.01 <= spend;
Func<List<Game.PropertyCard>, Func<Game.PropertyCard, double>, Game.PropertyCard> pick = (from, worth) =>
from.Where(covered).OrderByDescending(worth).ThenBy(c => c.Price).FirstOrDefault() ?? from.OrderBy(c => c.Price).FirstOrDefault();
var worstCash = Game.WorstLot(lots);
var better = worstCash == null ? new List<Game.PropertyCard>() : cash.Where(c => c.Income > worstCash.Income && !SameProperty(c.Name, worstCash.Name)).ToList();
Func<List<Game.PropertyCard>, Game.PropertyCard> pickCash = from =>
{
var fair = from.Where(c => !from.Any(d => d.Price <= c.Price && d.Income >= c.Income && (d.Price < c.Price || d.Income > c.Income)))
.OrderBy(c => c.Price).ThenByDescending(c => c.Income).ToList();
return fair.Where(covered).OrderByDescending(c => c.Income).FirstOrDefault() ?? fair.FirstOrDefault();
};
bool cashDone = cash.Count == 0 || (worstCash != null && better.Count == 0);
int owned = int.MinValue;
Func<int> ownedNow = () => { if (owned == int.MinValue) owned = goldLots > 0 && gold.Count > 0 ? goldOwned() : 0; return owned; };
if (lotsOpen > 0)
{
if (cashDone && goldLots > 0 && gold.Count > 0)
{
int n = ownedNow();
if (n < 0) return new PropertyPlan { Why = "the gold lots' count didn't read. The bot looks again in 30 minutes." };
if (n < goldLots) return new PropertyPlan { Target = pick(gold, c => c.Gold) };
}
if (cash.Count == 0) return new PropertyPlan { Why = "a lot is free, but no property to build on it read. The bot looks again in 30 minutes (didn't read)." };
return new PropertyPlan { Target = pickCash(cash) };
}
if (better.Count > 0) return new PropertyPlan { Target = pickCash(better), Replace = worstCash };
if (worstCash == null && !lots.Any(l => l.Gold > 0))
return new PropertyPlan { Why = "every lot is built on, and YOUR BLOCK's lots didn't read. The bot looks again in 30 minutes." };
if (goldLots > 0 && gold.Count > 0)
{
int n = ownedNow();
if (n < 0) return new PropertyPlan { Why = "the gold lots' count didn't read. The bot looks again in 30 minutes." };
if (n < goldLots && worstCash != null) return new PropertyPlan { Target = pick(gold, c => c.Gold), Replace = worstCash };
}
var worstGold = Game.WorstGoldLot(lots);
if (worstGold != null && goldLots > 0)
{
var more = gold.Where(c => c.Gold > worstGold.Gold).ToList();
if (more.Count > 0) return new PropertyPlan { Target = pick(more, c => c.Gold), Replace = worstGold };
}
return new PropertyPlan();
}
internal static bool SameProperty(string a, string b)
{
Func<string, string> key = t => new string((t ?? "").Where(char.IsLetterOrDigit).Select(char.ToLowerInvariant).ToArray());
string x = key(a), y = key(b);
if (x.Length == 0 || y.Length == 0) return false;
if (x == y) return true;
if (Math.Min(x.Length, y.Length) < 10 || Math.Abs(x.Length - y.Length) > 1) return false;
int i = 0, j = 0, diff = 0;
while (i < x.Length && j < y.Length)
{
if (x[i] == y[j]) { i++; j++; continue; }
if (++diff > 1) return false;
if (x.Length > y.Length) i++; else if (y.Length > x.Length) j++; else { i++; j++; }
}
return diff + (x.Length - i) + (y.Length - j) <= 1;
}
void PropertiesLevelUp() { nextProperties = DateTime.UtcNow; propPrice = 0; propLotsOpen = -1; propReplace = false; propRecheck = false; }
bool propReplace;
bool propRecheck;
string replaceSaid;
Game.BlockLot SecondBlockRead(Game.BlockLot worst, List<Game.BlockLot> lots)
{
int open;
var again = game.ReadBlock(null, out open, 2);
if (again == null || again.Count == 0)
{
Log("Properties: didn't replace a " + worst + " - YOUR BLOCK didn't read a second time");
nextProperties = DateTime.UtcNow.AddMinutes(30);
return null;
}
if (worst.Gold <= 0)
{
var lower = Game.WorstLot(lots, again);
if (lower != null && lower.Income < worst.Income)
{
Log("Properties: the second read of YOUR BLOCK found a " + lower + ", worse than the " + worst + " the first read picked - that one goes");
worst = lower;
}
}
return worst;
}
bool Replace(Game.BlockLot worst, Game.PropertyCard target, ref bool took)
{
double buyOut;
bool ready = game.BuildReady(target, out buyOut);
if (!ready && buyOut > 0)
{
if (WithdrawFor(buyOut * 1.01, true)) took = true;
ready = game.BuildReady(target);
}
if (!ready)
{
Log("Properties: didn't replace a " + worst + " - " + target.Name + "'s BUILD isn't showing gold yet");
nextProperties = DateTime.UtcNow.AddMinutes(10);
return false;
}
string why;
if (!game.DemolishLot(worst, out why))
{
Log("Properties: didn't replace a " + worst + " - " + why);
nextProperties = DateTime.UtcNow.AddMinutes(30);
return false;
}
Log("Properties: demolished a " + worst + " to build " + target);
propLotsOpen = Math.Max(1, propLotsOpen);
return true;
}
void PropertyBuilt(object p, object instead)
{
Log("Properties: built " + p);
Count.Properties++;
Discord.Post(Ping.Property, "\U0001F3E0 New property", "Built " + p + (instead != null ? " in place of a " + instead : ""), Shot, Discord.Green);
}
void SayReplace(string what)
{
if (replaceSaid == what) return;
replaceSaid = what;
Log("Properties: " + what);
}
double PropertyKeep() { return S.Properties && propLotsOpen > 0 && propPrice > 0 && !TutorialOn ? propPrice : 0; }
double SlotKeep()
{
double keep = S.Properties && propPrice > 0 && !TutorialOn ? propPrice : 0;
if (S.SafehouseUpgrade && upgradePrice > 0) keep += upgradePrice;
return keep;
}
DateTime nextUpgrade;
double upgradePrice = -1;
int upgradeNeedsLevel = -1;
string upgradeSaid = "";
void DoSafehouseUpgrade()
{
double keep = PropertyKeep(), floor = keep;
bool took = upgradePrice > 0 && BankFee() == 0 && WithdrawKeeping(upgradePrice, true, upgradePrice, keep, out floor);
if (!took) floor = KeepFloor(keep, true);
string why = DoSafehouseUpgradeHere(!took, floor);
if (!took && why == "saving up" && upgradePrice > 0 && WithdrawKeeping(upgradePrice, true, upgradePrice, keep, out floor))
{
took = true;
DoSafehouseUpgradeHere(true, floor);
}
if (took) DepositBack();
}
string DoSafehouseUpgradeHere(bool sayWaiting, double keep)
{
nextUpgrade = DateTime.UtcNow.AddMinutes(10);
var r = game.UpgradeSafehouse(keep);
if (r.Upgraded)
{
var a = r.After;
upgradePrice = a == null ? -1 : a.Level > 0 && a.Level >= a.MaxLevel ? 0 : a.Price;
Log("Safehouse: upgraded to " + r.Next + (a != null ? " (level " + a.Level + " of " + a.MaxLevel + ")" : "") + " for " + SafehouseInfo.Money(r.Price)
+ (upgradePrice > 0 && a.Next != null ? ". Next: " + a.Next + " for " + SafehouseInfo.Money(upgradePrice) : upgradePrice == 0 ? ". That's the top level" : ""));
SnapshotOnce("safehouse upgraded");
nextUpgrade = nextSafehouse = DateTime.UtcNow;
return null;
}
if (r.Why == "top level") { upgradePrice = 0; nextUpgrade = DateTime.UtcNow.AddHours(24); return r.Why; }
if (r.NeedLevel > 0)
{
upgradePrice = -1;
upgradeNeedsLevel = last != null && last.Level >= r.NeedLevel ? -1 : r.NeedLevel;
nextUpgrade = DateTime.UtcNow.AddHours(6);
if (upgradeSaid != "level" + r.NeedLevel) { upgradeSaid = "level" + r.NeedLevel; Log("Safehouse: the next one" + (r.Next != null ? ", " + r.Next + "," : "") + " needs level " + r.NeedLevel + ". The bot upgrades it once you get there."); }
return r.Why;
}
upgradeNeedsLevel = -1;
if (r.Price > 0) upgradePrice = r.Price;
if (r.Why == "saving up")
{
bool bankCovers = r.Price > 0 && last != null && Spendable(last) >= r.Price + PropertyKeep() && CanWithdraw();
if (sayWaiting && !bankCovers && upgradeSaid != r.Next + r.Price) { upgradeSaid = r.Next + r.Price; Log("Safehouse: saving up for " + r.Next + " (" + SafehouseInfo.Money(r.Price) + ")"); }
return r.Why;
}
if (r.Why != null) { nextUpgrade = DateTime.UtcNow.AddHours(1); Log("Safehouse: can't upgrade right now - " + r.Why); return r.Why; }
Log("Safehouse: UPGRADE pressed but the level didn't change" + (r.Toast != null ? " (\"" + r.Toast.Trim() + "\")" : "")
+ (!string.IsNullOrEmpty(r.Window) ? ". On screen: \"" + r.Window + "\"" : ""));
game.Snapshot("safehouse upgrade did nothing", 60);
nextUpgrade = DateTime.UtcNow.AddHours(6);
upgradePrice = -1;
return null;
}
DateTime nextCrew, nextCrewSlot;
bool crewQuiet;
double crewSlotPrice = -1;
double[] crewHirePrice = { 10000, 10e6, 10e9 };
string crewSaid = "";
double CashNow()
{
for (int i = 0; i < 3; i++)
{
if (i > 0) game.Wait(400);
double c;
using (var f = game.Capture()) c = game.ReadCash(f);
if (c >= 0) return c;
}
return -1;
}
double CashFor(double need)
{
double cash = CashNow();
if (cash >= need) return cash;
game.Wait(500);
double again;
using (var f = game.Capture()) again = game.ReadCash(f, true);
return Math.Max(cash, again);
}
double CashSure()
{
double a = CashNow();
if (a < 0) return -1;
game.Wait(500);
double b;
using (var f = game.Capture()) b = game.ReadCash(f, true);
return b < 0 ? -1 : Math.Min(a, b);
}
static string M(double v) { return SafehouseInfo.Money(v); }
public event Action<CrewInfo> CrewRead;
CrewInfo crewInfo;
bool BuySlots { get { return S.CrewSlots && S.CrewFill; } }
void DoCrew()
{
var info = new CrewInfo { At = DateTime.Now, SlotPrice = crewSlotPrice };
List<Game.CrewRow> crew = null;
rerollCrew = null;
try { DoCrewHere(info, out crew); }
finally { ShowCrewInfo(info, crew); }
}
void ShowCrewInfo(CrewInfo info, List<Game.CrewRow> crew)
{
if (crew == null) return;
foreach (var r in crew) if (r.Rarity >= 0 && r.Rarity < 8) info.ByRarity[r.Rarity]++;
var worst = crew.Where(r => r.Rarity >= 0).OrderBy(r => r.Rarity).ThenBy(r => r.Power).FirstOrDefault();
info.Worst = worst != null ? worst.ToString() : "";
if (info.Henchmen < 0) info.Henchmen = crew.Count;
crewInfo = info;
var h = CrewRead;
if (h != null) h(info);
}
DateTime slotWithdrawAfter = DateTime.MinValue;
void DoCrewHere(CrewInfo info, out List<Game.CrewRow> crew)
{
crew = null;
nextCrew = DateTime.UtcNow.AddMinutes(30);
nextCrewSlot = DateTime.UtcNow.AddMinutes(5);
Game.CrewPage page;
var rows = ReadWholeCrewClosing(out page);
if (page == null || !page.Page) { Log("Crew: couldn't read the crew page"); game.Snapshot("crew not read", 60); return; }
crew = rows.Where(r => !r.Empty && !r.Locked && r.Rarity >= 0).ToList();
bool rolling = game.CrewRollsSeen;
info.Henchmen = page.Henchmen; info.Slots = page.Slots; info.Empty = rows.Count(r => r.Empty);
if (game.CrewTotalsSeen != null && game.CrewTotalsSeen.Read) { info.Attack = game.CrewTotalsSeen.Attack; info.Defense = game.CrewTotalsSeen.Defense; }
foreach (var e in rows.Where(r => r.Empty)) for (int t = 0; t < 3; t++) if (e.HirePrice[t] > 0) crewHirePrice[t] = e.HirePrice[t];
bool changed = false, more = false;
double keep = PropertyKeep();
if (BuySlots)
{
double price = -1, cash = CashNow(), slotKeep = SlotKeep(), floor = KeepFloor(slotKeep, true);
double paid = cash >= 0 ? game.UnlockCrewSlot(cash, floor, out price) : -1;
if (price > 0) crewSlotPrice = info.SlotPrice = price;
if (paid == 0 && price > 0 && DateTime.UtcNow >= slotWithdrawAfter && WithdrawKeeping(price, true, price, slotKeep, out floor))
{
if (game.OpenTab(Tab.Crew)) { cash = CashNow(); paid = cash >= 0 ? game.UnlockCrewSlot(cash, floor, out price) : -1; }
if (paid <= 0)
{
slotWithdrawAfter = DateTime.UtcNow.AddHours(6);
game.Snapshot("crew slot not bought", 360);
Log("Crew: the cash taken out still didn't buy the slot" + (paid < 0 ? " (UNLOCK SLOT not found, or the press didn't take)" : "") + " - the bank isn't used for it again for 6 hours");
}
DepositBack(paid >= 0 ? paid : -1);
if (!game.OpenTab(Tab.Crew)) return;
}
if (paid > 0)
{
Log("Crew: bought a new slot for " + M(paid));
changed = true; crewSlotPrice = info.SlotPrice = -1; nextCrewSlot = slotBoughtAt = DateTime.UtcNow;
if (info.Slots > 0) info.Slots++;
info.Status = "Bought a new slot.";
}
else if (paid == 0 && price > 0)
{
info.Status = "Saving up for the next slot (" + M(price) + ").";
if (crewSaid != "slot" + price) { crewSaid = "slot" + price; Log("Crew: saving up for the next slot (" + M(price) + ")"); }
}
}
if (S.CrewFill)
for (int n = 0; n < 5; n++)
{
Game.CrewPage p;
var empty = game.FindCrewRow(r => r.Empty, out p);
if (empty == null) break;
for (int t = 0; t < 3; t++) if (empty.HirePrice[t] > 0) crewHirePrice[t] = empty.HirePrice[t];
CrewRoll used;
double paid;
var hire = RollNow();
double hireCash = CashFor(empty.HirePrice[(int)hire] > 0 ? empty.HirePrice[(int)hire] : 0);
hire = EmptySlotHire(hire, S.CrewRollType, empty.HirePrice[(int)hire], hireCash);
var got = game.HireInto(empty, hire, true, hireCash, 0, out used, out paid);
if (got == null && game.HiredUnseen != null)
{
Count.Hired++;
Count.CrewCash += paid;
changed = more = true;
Log("Crew: hired " + game.HiredUnseen + " with a " + used + " hire (" + M(paid) + ") - the game said so; its row hadn't shown yet");
PingHenchman(game.HiredUnseen, -1, false, crew);
break;
}
if (got == null)
{
if (game.HireBusy) Log("Crew: " + CrewBusy + " - the empty slot waits for the next look");
else if (paid > 0) { Log("Crew: a " + used + " hire showed no recruit"); game.Snapshot("crew hire no recruit", 60); }
else if (crewSaid != "hire") { crewSaid = "hire"; Log("Crew: an empty slot waits - no hire the cash covers yet"); }
break;
}
Count.Hired++;
Count.CrewCash += paid;
changed = true;
crew.Add(got);
Game.NoteOwn(ownStats, got);
Log("Crew: hired " + got + " with a " + used + " hire (" + M(paid) + ")");
PingHenchman(got, -1, false, crew);
SnapshotOnce("crew hired");
}
if (changed && S.EquipBest) DoEquip();
bool sorted = true;
if (!S.CrewFill && !S.CrewReroll && !S.TrainCrew) { }
else if (rolling) SayOnce("crew sort auto roll " + DateTime.UtcNow.ToString("yyyyMMddHH"), "Crew: the order waits while a slot is auto-rolling in the game");
else if (crew.Any(r => r.Training)) SayOnce("crew sort training " + DateTime.UtcNow.ToString("yyyyMMddHH"), "Crew: the order waits while a henchman trains");
else if (!changed && page.Henchmen > 0 && crew.Count == page.Henchmen && Game.InOrder(crew, ByOwnStats ? ownStats : null, SortSkipped)) { }
else SortCrew(40, out sorted);
if (!sorted) more = true;
if (more) nextCrew = DateTime.UtcNow.AddMinutes(1);
if (sortWasStuck) { sortStuckRounds++; nextCrew = DateTime.UtcNow.AddMinutes(sortStuckRounds == 1 ? 1 : sortStuckRounds == 2 ? 5 : 30); }
else if (sorted) sortStuckRounds = 0;
crewQuiet = !S.CrewFill && !S.CrewReroll && !S.TrainCrew && !more && !sortWasStuck;
if (crewQuiet) nextCrew = DateTime.UtcNow.AddHours(6);
if (S.TrainCrew && S.TrainAtOnce > 0) TrainCrew(crew, keep, info);
}
void TrainCrew(List<Game.CrewRow> crew, double keep, CrewInfo info)
{
int busy = crew.Count(r => r.Training && r.TrainSecondsLeft > 0);
int soonest = crew.Where(r => r.Training && r.TrainSecondsLeft > 0).Select(r => r.TrainSecondsLeft).DefaultIfEmpty(-1).Min();
int best = Game.BestRoll(RollNow());
LoadTrained();
foreach (var r in crew.Where(r => !r.Empty && !r.Locked && !string.IsNullOrEmpty(r.Name)))
{
int lv = Math.Max(r.TrainLevel, r.TrainingTo);
foreach (var k in trainedTo) if (Game.NamedLike(r, k.Key)) lv = Math.Max(lv, k.Value);
if (lv > r.TrainLevel) r.TrainLevel = lv;
if (lv > 0) NoteTrained(r, lv);
}
var picks = TrainFirst(crew.Where(r => !(r.Training && r.TrainSecondsLeft > 0) && !r.Stationed && !r.Recovering && r.Rarity >= (int)Rarity.Epic && r.TrainLevel < 10 && (r.Train == null || r.TrainGold) && !RerollWouldReplace(r, best)),
S.TrainOrder, r => { var o = Game.OwnOf(r, ownStats); return o != null ? o.Sum : r.Power; });
var noTimer = crew.Where(r => r.Training && r.TrainSecondsLeft <= 0).Select(r => r.Name).ToList();
string nt = noTimer.Count > 0 ? string.Join(", ", noTimer) : null;
if (nt != trainNoTimerSaid) { trainNoTimerSaid = nt; if (nt != null) Log("Crew: read as training but no time left on their row: " + nt + " - not counted, training goes on"); }
int started = 0;
string why = null;
if (picks.Count == 0 && busy < Math.Min(S.TrainAtOnce, Settings.MaxTraining))
{
int epic = crew.Count(r => r.Rarity >= (int)Rarity.Epic);
why = "nobody to train (" + epic + " Epic or better: " + crew.Count(r => r.Rarity >= (int)Rarity.Epic && r.TrainLevel >= 10) + " at +10, "
+ crew.Count(r => r.Rarity >= (int)Rarity.Epic && r.Rarity < best && S.CrewReroll) + " the rerolls would replace, "
+ crew.Count(r => r.Rarity >= (int)Rarity.Epic && r.Train != null && !r.TrainGold) + " with a TRAIN that isn't gold, "
+ crew.Count(r => r.Rarity >= (int)Rarity.Epic && r.Train == null && !r.Training) + " with no TRAIN read)";
}
if (busy >= Math.Min(S.TrainAtOnce, Settings.MaxTraining))
why = busy + " training already, the most at once (" + string.Join(", ", crew.Where(r => r.Training && r.TrainSecondsLeft > 0).Select(r => r.Name + " " + r.RarityName
+ (r.TrainSecondsLeft > 0 ? ", " + Parse.Short(r.TrainSecondsLeft) + " left" : ", no time read"))) + ")";
double tooDear = double.MaxValue;
string waiting = null;
int opened = 0;
try
{
foreach (var pick in picks)
{
if (busy >= Math.Min(S.TrainAtOnce, Settings.MaxTraining) || started >= 5) break;
double known;
if (pick.TrainLevel > 0 && trainPrice.TryGetValue(pick.TrainLevel + 1, out known) && known >= tooDear) continue;
if (opened >= 4) break;
Game.CrewPage p;
var row = game.FindCrewRow(r => Game.NamedLike(r, pick), out p, true);
if (row == null) { why = why ?? "couldn't find " + pick.Name + " in the list"; continue; }
if (row.Training && row.TrainSecondsLeft > 0) { why = why ?? pick.Name + " reads as training on a second look"; continue; }
if (row.Stationed || row.Recovering) continue;
double cash = CashNow();
if (cash < 0) { why = "the cash on hand didn't read"; break; }
Game.TrainWindow offer;
opened++;
double floor = KeepFloor(keep, false);
int got = game.TrainHenchman(row, pick, cash, floor, out offer);
if (got == 0 && offer != null && offer.Price > 0 && offer.Names(pick.Name) && WithdrawKeeping(offer.Price, false, offer.Price, keep, out floor))
{
if (game.OpenTab(Tab.Crew))
{
row = game.FindCrewRow(r => Game.NamedLike(r, pick), out p, true);
if (row != null && !(row.Training && row.TrainSecondsLeft > 0)) got = game.TrainHenchman(row, pick, CashNow(), floor, out offer);
}
DepositBack(got == 1 && offer != null ? offer.Price : got == 0 ? 0 : -1);
game.OpenTab(Tab.Crew);
}
if (offer != null && offer.Price > 0 && offer.ToLevel > 0 && offer.Names(pick.Name)) trainPrice[offer.ToLevel] = offer.Price;
if (got == 1)
{
if (offer.ToLevel > 0) NoteTrained(pick, offer.ToLevel);
busy++; started++;
Count.CrewCash += offer.Price;
string mins = offer.Minutes > 0 ? ", off duty for " + Parse.Short(offer.Minutes * 60) : "";
Log("Crew: training " + row.Name + " (" + row.RarityName + ") to " + (offer.ToLevel > 0 ? "+" + offer.ToLevel : "the next level (none read)") + " for " + M(offer.Price) + mins);
info.Status = "Training " + row.Name + (offer.ToLevel > 0 ? " to +" + offer.ToLevel : "") + ".";
if (offer.Minutes > 0 && (soonest < 0 || offer.Minutes * 60 < soonest)) soonest = offer.Minutes * 60;
continue;
}
string who = pick.Name + " (" + pick.RarityName + (pick.TrainLevel > 0 ? ", +" + pick.TrainLevel : "") + ")";
if (got == 0 && offer != null && offer.Price > 0 && offer.Names(pick.Name))
{
double onHand = CashNow();
string dear = who + (offer.ToLevel > 0 ? " to +" + offer.ToLevel : "") + " (" + M(offer.Price) + ", " + (onHand >= 0 ? M(onHand) : "?") + " on hand"
+ (keep > 0 ? ", " + M(keep) + " kept for the next property" : "") + ")";
if (waiting == null) waiting = dear;
why = "saving up to train " + waiting;
info.Status = "Saving up to train " + row.Name + " (" + M(offer.Price) + ").";
tooDear = Math.Min(tooDear, offer.Price);
continue;
}
else if (got == 0) why = who + ": the training window " + (offer == null ? "didn't read" : offer.Price <= 0 ? "showed no price the bot could read (\"" + offer.Words + "\")" : "named " + offer.Name) + " - CANCEL pressed";
else why = who + ": " + (game.TrainWhy ?? "nothing pressed");
break;
}
}
finally
{
if (started > 0 && waiting != null)
{
string key = Regex.Replace(waiting, @"[^ (]+ on hand", "");
if (key != trainSkipSaid) { trainSkipSaid = key; Log("Crew: " + waiting + " waits for the cash - the next best were trained meanwhile"); }
}
if (started > 0) trainSaid = null;
else if (why != null)
{
string key = Regex.Replace(Regex.Replace(why, @"[0-9]+[hms ]*[0-9]*[hms]* left", ""), @"[^ (]+ on hand", "");
if (key != trainSaid) { trainSaid = key; Log("Crew: nobody trained - " + why); }
}
if (soonest > 0)
{
var at = DateTime.UtcNow.AddSeconds(soonest + 20);
if (at < nextCrew) nextCrew = at;
}
if (started == 0 && game.TrainBusy)
{
var at = DateTime.UtcNow.AddMinutes(2);
if (at < nextCrew) nextCrew = at;
}
}
}
internal static List<Game.CrewRow> TrainFirst(IEnumerable<Game.CrewRow> rows, TrainOrder order, Func<Game.CrewRow, double> strength)
{
if (order == TrainOrder.Evenly)
return rows.OrderBy(r => r.TrainLevel).ThenByDescending(r => r.Rarity).ThenByDescending(strength).ToList();
return rows.OrderByDescending(r => r.Rarity).ThenByDescending(strength).ToList();
}
string trainSaid;
string trainSkipSaid;
readonly Dictionary<int, double> trainPrice = new Dictionary<int, double>();
readonly Dictionary<Game.CrewRow, int> trainedTo = new Dictionary<Game.CrewRow, int>();
string trainedFor;
void LoadTrained()
{
if (trainedFor == S.Account) return;
trainedFor = S.Account;
trainedTo.Clear();
foreach (var part in (S.TrainedLevels ?? "").Split(';'))
{
var m = Regex.Match(part, @"^(\d)\|(.+)=(\d{1,2})$");
if (m.Success) trainedTo[new Game.CrewRow { Rarity = int.Parse(m.Groups[1].Value), Name = m.Groups[2].Value }] = int.Parse(m.Groups[3].Value);
}
}
void NoteTrained(Game.CrewRow r, int level)
{
LoadTrained();
var had = trainedTo.Keys.FirstOrDefault(k => Game.NamedLike(r, k));
if (had != null) { if (trainedTo[had] >= level) return; trainedTo.Remove(had); }
trainedTo[new Game.CrewRow { Rarity = r.Rarity, Name = r.Name.Replace(";", "").Replace("=", "").Replace("|", "") }] = level;
S.TrainedLevels = string.Join(";", trainedTo.Select(k => k.Key.Rarity + "|" + k.Key.Name + "=" + k.Value));
SaveSettings();
}
string trainNoTimerSaid;
bool RerollWouldReplace(Game.CrewRow r, int best)
{
return S.CrewReroll && r.Rarity < best;
}
int dismissFails;
string dismissFailsOn;
int findFails;
int rerollHireFails;
int HireNoRecruit(CrewRoll roll, double paid, int fails, out string stop)
{
if (game.HireBusy) { stop = CrewBusy; return fails + 1; }
if (paid <= 0) { stop = "the " + roll + " hire isn't gold right now"; return fails + 1; }
rerollHireFails++;
stop = "the " + roll + " hire showed no recruit";
int wait = 10000 * rerollHireFails;
Log("Crew: a " + roll + " hire into an empty slot showed no recruit (try " + rerollHireFails + " of 3) - waiting "
+ (wait / 1000) + " s before the same slot is tried again");
game.Wait(wait);
return rerollHireFails >= 3 ? 3 : fails;
}
string findFailsOn;
readonly List<Game.OwnStats> ownStats = new List<Game.OwnStats>();
DateTime ownReadAt = DateTime.MinValue, gearBackHold = DateTime.MinValue;
DateTime ownConfirmAt = DateTime.MinValue;
bool crewUnequipped;
int gearBackFails;
int ownUnknownLeft = -1;
bool ownJustRead;
bool CrewUnequipped
{
get { return crewUnequipped; }
set
{
crewUnequipped = value;
if (!value) gearBackFails = 0;
if (S.CrewGearOff == value) return;
S.CrewGearOff = value;
SaveSettings();
}
}
readonly object totalsLock = new object();
Counters totalsFolded = new Counters();
DateTime totalsFoldedAt = DateTime.UtcNow;
public Counters AllTime
{
get { lock (totalsLock) { var t = Counters.Parse(S.Totals); t.Add(Count, totalsFolded); return t; } }
}
public void FoldTotals(bool save = true)
{
lock (totalsLock)
{
var t = Counters.Parse(S.Totals);
t.Add(Count, totalsFolded);
S.Totals = t.Text();
totalsFolded = Count.Copy();
totalsFoldedAt = DateTime.UtcNow;
}
if (save) SaveSettings();
}
internal void SaveSettings()
{
try
{
if (lead == null) S.Save(Path.Combine(dir, "IdleMafiaBot.ini"));
if (S.Account.Length > 0) { Directory.CreateDirectory(Accounts.Folder(dir, S.Account)); S.Save(Accounts.Ini(dir, S.Account), true); SavedOwn(); }
}
catch (Exception e) { if (firstSeen.Add("settings not saved")) Log("Couldn't save the settings: " + e.Message); }
}
int ReadOwnStats()
{
ownReadAt = DateTime.UtcNow;
bool pressed;
string why;
int read = 0;
CrewUnequipped = true;
bool off = game.UnequipAll(out pressed, out why);
if (!pressed) CrewUnequipped = false;
if (off)
{
Game.CrewPage page;
foreach (var r in game.ReadWholeCrew(out page))
if (!r.Empty && !r.Locked && r.Attack >= 0 && r.Defense >= 0) { Game.NoteOwn(ownStats, r); read++; }
if (read > 0)
foreach (var r in game.ReadWholeCrew(out page, 3))
if (!r.Empty && !r.Locked && r.Attack >= 0 && r.Defense >= 0) Game.NoteOwn(ownStats, r);
}
if (pressed) { game.ScrollPage(0.62, 40); DoEquip(); }
if (read > 0) Log("Crew: read " + read + " henchmen's own stats with their gear off, then the best gear on again");
else
{
Log("Crew: " + (pressed ? "UNEQUIP ALL pressed, but " + (why ?? "no stats read") : "UNEQUIP ALL not pressed: " + why) + " - own stats looked at again in an hour");
ownReadAt = DateTime.UtcNow.AddHours(-5);
}
return read;
}
bool OwnReadDue(int unknown) { return OwnReadDue(unknown, ownUnknownLeft, DateTime.UtcNow - ownReadAt); }
internal static bool OwnReadDue(int unknown, int unknownLeft, TimeSpan sinceRead)
{
if (unknown <= 0) return false;
return sinceRead > TimeSpan.FromHours(6) || (unknown > unknownLeft && sinceRead > TimeSpan.FromHours(1));
}
DateTime nextReroll;
List<Game.CrewRow> rerollCrew;
DateTime rerollCrewAt;
bool rerollEmpty;
DateTime emptyFootSaidAt;
int emptiedRarity = -1;
bool emptiedByStats;
double rerollCash = -1;
string pausedIdle;
DateTime pausedIdleAt;
bool rerollShort;
DateTime rerollShortAt;
string rerollSaid;
int ownOpenBefore = -1;
int OwnOpenBefore
{
get { return ownOpenBefore; }
set { ownOpenBefore = value; }
}
void KeepOwnOpenBefore()
{
int keep = S.CrewFill ? -1 : ownOpenBefore;
if (S.CrewOpenBefore == keep) return;
S.CrewOpenBefore = keep;
SaveSettings();
}
internal static int OpenSlots(Game.CrewPage p)
{
return p != null && p.Page && p.Henchmen >= 0 && p.Slots > 0 && !p.AutoRolls ? Math.Max(0, p.Slots - p.Henchmen) : -1;
}
internal static bool RerollFills(bool emptyRow, bool crewFill, int open, int openBefore)
{
if (!emptyRow) return false;
if (crewFill) return true;
if (openBefore == -1) return false;
return open < 0 || openBefore < 0 || open > openBefore;
}
internal static bool OwnSlotFilled(int open, int openBefore)
{
if (openBefore == -1 || open < 0) return false;
return openBefore >= 0 ? open <= openBefore : open == 0;
}
int OpenBeforeDismiss()
{
int open = OpenSlots(game.CrewPageSeen);
return open == 0 ? 0 : OpenSlots(game.LookAtCrew());
}
bool OpenedSince(int before, out string count)
{
count = null;
if (before < 0) return false;
var now = game.LookAtCrew();
if (OpenSlots(now) != before + 1) return false;
count = now.Henchmen + " of " + now.Slots;
return true;
}
bool RerollCashOk(Header h, DateTime now)
{
if (!rerollShort) return true;
var roll = RollNow();
double need = crewHirePrice[(int)roll] + (rerollEmpty ? 0 : PropertyKeep());
if (h != null && h.Cash >= 0 && roll != S.CrewRollType) return Spendable(h) >= need;
return h != null && h.Cash >= 0 ? h.Cash >= need : now >= rerollShortAt.AddMinutes(1);
}
internal static int ShortWaitSeconds(bool shortBefore, double spent) { return shortBefore && spent <= 0 ? 15 : 3; }
public volatile bool RerollOnly;
public void StartRerollOnly()
{
if (Program.Reroller) { S.RerollerOnly(); rerollerBase = Count.CrewCash; }
StartRerollOnlyGo();
}
double rerollerBase;
volatile string rerollerBreak;
public double RerollerSpent { get { return Count.CrewCash - rerollerBase; } }
void StartRerollOnlyGo() { FightOnly = false; onlyFails = 0; onlyProgressAt = DateTime.UtcNow; nextReroll = DateTime.UtcNow; RerollOnly = true; if (!Running) Begin(false); }
public void StopRerollOnly() { if (!RerollOnly) return; RerollOnly = false; stop = true; }
void EndRerollOnly(string why, bool finished)
{
if (!RerollOnly) return;
Log("Only rerolling: " + why + " - the bot stops");
RerollOnly = false;
stop = true;
stopWhy = (finished ? "Only rerolling is over: " : "Only rerolling gave up: ") + why;
}
public volatile bool FightOnly;
public void StartFightOnly()
{
RerollOnly = false;
onlyFightFails = 0; onlyFightProgressAt = DateTime.UtcNow; nextFights = DateTime.UtcNow;
FightOnly = true;
if (!Running) Begin(false);
}
public void StopFightOnly() { if (!FightOnly) return; FightOnly = false; stop = true; }
void EndFightOnly(string why)
{
if (!FightOnly) return;
Log("Only fighting: " + why + " - the bot stops");
FightOnly = false;
stop = true;
stopWhy = "Only fighting gave up: " + why;
}
int onlyFightFails;
DateTime onlyFightProgressAt;
void AfterFightOnly(int fought)
{
var now = DateTime.UtcNow;
if (fought > 0) { onlyFightFails = 0; onlyFightProgressAt = now; return; }
var h = ReadBarsSettled(2000);
int kept = S.OnlyFightsSpendAll ? 0 : Kept(h.StaminaMax);
bool noStamina = h.Stamina >= 0 && (S.OnlyFightsSpendAll ? h.Stamina <= 0 : h.Stamina - kept < FightSpare(h.StaminaMax, kept));
if (noStamina)
{
Log("Only fighting: waiting for stamina (" + h.Stamina + (h.StaminaMax > 0 ? "/" + h.StaminaMax : "") + ")");
if (nextFights < now.AddMinutes(1)) nextFights = now.AddMinutes(1);
return;
}
onlyFightFails++;
if (onlyFightFails >= 3 || now - onlyFightProgressAt > TimeSpan.FromHours(1))
{
EndFightOnly("nothing left worth attacking (after " + onlyFightFails + " tries)");
return;
}
if (nextFights < now.AddMinutes(1)) nextFights = now.AddMinutes(1);
Log("Only fighting: nothing worth attacking right now (try " + onlyFightFails + " of 3) - looking again at "
+ nextFights.ToLocalTime().ToString("HH:mm"));
}
void DoRerolls(bool only)
{
var info = new CrewInfo { At = DateTime.Now, SlotPrice = crewSlotPrice };
List<Game.CrewRow> crew = null;
try { RerollsHere(info, only, out crew); }
finally { KeepOwnOpenBefore(); ShowCrewInfo(info, crew); }
}
List<Game.CrewRow> ReadCrewForRerolls(CrewInfo info, int step = 4)
{
Game.CrewPage page;
var rows = ReadWholeCrewClosing(out page, step);
if (page == null || !page.Page) { Log("Crew: couldn't read the crew page"); game.Snapshot("crew not read", 60); return null; }
info.Henchmen = page.Henchmen >= 0 ? page.Henchmen : game.CrewCountSeen;
if (info.Henchmen < 0 && hiredRead >= 0 && hiredReadAt > slotBoughtAt) info.Henchmen = hiredRead;
info.Slots = page.Slots; info.Empty = rows.Count(r => r.Empty);
if (game.CrewTotalsSeen != null && game.CrewTotalsSeen.Read) { info.Attack = game.CrewTotalsSeen.Attack; info.Defense = game.CrewTotalsSeen.Defense; }
foreach (var e in rows.Where(r => r.Empty)) for (int t = 0; t < 3; t++) if (e.HirePrice[t] > 0) crewHirePrice[t] = e.HirePrice[t];
int open = OpenSlots(page);
if (OwnSlotFilled(open, OwnOpenBefore)) OwnOpenBefore = -1;
rerollEmpty = RerollFills(rows.Any(r => r.Empty), S.CrewFill, open, OwnOpenBefore);
return rows.Where(r => !r.Empty && !r.Locked && r.Rarity >= 0).ToList();
}
List<Game.CrewRow> ReadWholeCrewClosing(out Game.CrewPage page, int step = 4)
{
var rows = game.ReadWholeCrew(out page, step);
if (page != null && page.AutoRollWindow && game.CloseStrayAutoRoll()) rows = game.ReadWholeCrew(out page, step);
return rows;
}
List<Game.CrewRow> ReadAllHenchmen(CrewInfo info, out string why)
{
why = null;
for (int look = 0; look < 3; look++)
{
int step = 4 - (listShortFails * 2 + look) % 3;
string pages = look == 2 && listShortFails == 0 && !string.IsNullOrEmpty(game.ProblemsDir)
? Path.Combine(game.ProblemsDir, "active", "crew list short pages " + DateTime.Now.ToString("yyyyMMdd-HHmmss")) : null;
game.CrewPagesDir = pages;
List<Game.CrewRow> rows;
try { rows = ReadCrewForRerolls(info, step); }
finally { game.CrewPagesDir = null; }
if (rows == null) { DropPages(pages); why = "couldn't read the crew page"; return null; }
int want = info.Henchmen - 1;
if (info.Henchmen > 0 && rows.Count == want) { DropPages(pages); ListReadWhole(); return rows; }
if (info.Henchmen > 0 && rows.Count > want)
{
var merged = Game.MergeRepeats(rows, rows.Count - want);
if (merged != null)
{
Log("Crew: read " + rows.Count + " of " + want + " henchmen - " + (rows.Count - want == 1 ? "one was" : (rows.Count - want) + " were")
+ " read twice where two pages meet (the same rarity, attack and defense), taken once");
DropPages(pages);
ListReadWhole();
return merged;
}
}
why = info.Henchmen <= 0 ? "couldn't read how many henchmen there are" : "read " + rows.Count + " of " + want + " henchmen";
if (look == 2 && listShortFails == 0) Log("Crew: the list read as " + string.Join(", ", rows.Select(r => r.ToString())));
if (pages != null && Directory.Exists(pages)) Log("Crew: the pages of that read are in problems\\active\\" + Path.GetFileName(pages));
}
if (listShortFails++ == 0) listShortSince = DateTime.UtcNow;
game.Snapshot("crew list short", 60);
game.NoteFailure(why);
return null;
}
static void DropPages(string dir)
{
if (dir != null) try { if (Directory.Exists(dir)) Directory.Delete(dir, true); } catch (Exception) { }
}
int listShortFails;
DateTime listShortSince;
int hiredRead = -1;
DateTime hiredReadAt = DateTime.MinValue, slotBoughtAt = DateTime.MinValue;
bool SlotStandsEmpty(out string count)
{
count = null;
Func<Game.CrewPage, bool> open = p => p != null && p.Henchmen >= 0 && p.Slots > 0 && p.Henchmen < p.Slots && !p.AutoRolls;
if (!open(game.CrewPageSeen)) return false;
var now = game.LookAtCrew();
if (!open(now)) return false;
count = now.Henchmen + " of " + now.Slots;
return true;
}
void ListReadWhole()
{
if (listShortFails > 0) Log("Crew: the whole list reads right again (it didn't from " + listShortSince.ToLocalTime().ToString("HH:mm") + ")");
listShortFails = 0;
}
void RerollsHere(CrewInfo info, bool only, out List<Game.CrewRow> crew)
{
crew = null;
var start = DateTime.UtcNow;
nextReroll = start.AddMinutes(1);
bool shortBefore = rerollShort;
rerollShort = false;
rerollCash = -1;
var roll = RollNow();
int best = Game.BestRoll(roll);
double keep = only ? 0 : PropertyKeep();
bool fromCache = false;
if (rerollCrew != null && autoRun == null && start - rerollCrewAt < TimeSpan.FromMinutes(10) && game.OpenTab(Tab.Crew))
{
crew = rerollCrew;
fromCache = true;
if (crewInfo != null) { info.Henchmen = crewInfo.Henchmen; info.Slots = crewInfo.Slots; info.Attack = crewInfo.Attack; info.Defense = crewInfo.Defense; }
}
else crew = ReadCrewForRerolls(info);
rerollCrew = null;
if (crew == null) { if (only) OnlyGo(false, "couldn't read the crew page", 0, keep); return; }
if (rerollEmpty && !S.CrewFill && OwnOpenBefore == -1) rerollEmpty = false;
string rollWait;
bool rollEnds = AutoRollHere(info, keep, fromCache, ref crew, out rollWait);
NoteRun();
if (rollEnds)
{
info.Status = rollWait + ".";
if (only) OnlyGo(false, rollWait, 0, keep);
return;
}
double keepAll = keep;
if (AutoRollMay(false, -1, best) && crew.Any(r => r.Rarity >= 0 && r.Rarity < best && Dismissable(r)))
{
double rollFloor;
RollCash(roll, keep, autoRun != null ? autoRun.SpentBefore + Math.Max(0, autoRun.Spent) : 0, out rollFloor);
keep = rollFloor;
}
int rolls = 0, fails = 0, readAt = Environment.TickCount - 60000, badgesAt = Environment.TickCount;
int emptyHolds = 0;
double cashEst = -1;
double spent = 0;
Game.CrewRow lastHired = null;
var better = new List<string>();
string stop = null, pausedFor = null, idleFor = null;
int idleUntil = 0;
bool done = false, readAgain = false, reread = false, statsWait = false, statsSoon = false, listShort = false, skipWait = false, awayWait = false;
bool counted = false;
bool finished = false;
var clock = start;
rerollsYield = false;
try
{
while (true)
{
if (!only && S.Events && EventOn(GameEvent.RecruitLuck) && Environment.TickCount - badgesAt > 30000)
{
badgesAt = Environment.TickCount;
BadgesInRerolls();
}
if (!only && !RerollsLead())
{
var other = OtherTaskDue(ref readAt);
if (other != null && other == idleFor && Environment.TickCount < idleUntil) other = null;
else if (other != null && rolls == 0 && other == pausedIdle && DateTime.UtcNow - pausedIdleAt < TimeSpan.FromMinutes(1))
{
SayOnce("rerolls idle " + other, "Crew: the rerolls paused for " + other + " before their first reroll, and the round after didn't find it due - they now go on 30 s before looking again");
idleFor = other; idleUntil = Environment.TickCount + 30000; other = null;
}
if ((pausedFor = other) != null) break;
}
else if (!only)
{
OtherTaskDue(ref readAt, true);
var now = DateTime.UtcNow;
string full = null;
if (last != null && now - fullBreakAt > TimeSpan.FromMinutes(1)
&& ((last.EnergyMax > 0 && last.Energy >= last.EnergyMax) || (last.StaminaMax > 0 && last.Stamina >= last.StaminaMax)))
full = FullBarBreak(WhatIsDue(last, false), last);
if (full != null) { fullBreakAt = now; pausedFor = full + " (a full bar)"; break; }
var timed = TimedBreak(now, lastBank > clock ? lastBank : clock, S.Bank && Can(Tab.Bank), ShopOn() && now >= nextShop && Can(Tab.Shop));
if (timed == "") clock = now;
else if (timed != null) { rerollsYield = true; pausedFor = timed + (timed == "shop" ? " (its new stock)" : " (every " + BreakMinutes + " minutes)"); break; }
}
if (only && Program.Reroller && (S.RerollerJobs || S.RerollerGifts) && DateTime.UtcNow - fullBreakAt > TimeSpan.FromMinutes(3))
{
OtherTaskDue(ref readAt, true);
string full = last == null ? null : FullBarBreak(WhatIsDue(last, false), last);
if (full != null) { fullBreakAt = DateTime.UtcNow; rerollerBreak = full; pausedFor = full + " (a full bar)"; break; }
}
if (TurnUp("rerolls") && turns.Wanted(this)) { pausedFor = "the next account's turn"; break; }
if (rolls >= 500) break;
if (fails >= 3) break;
if (RollNow() != roll)
{
pausedFor = LuckRollShort() ? "not enough cash for " + LuckRollFor(S.CrewRollType, S.CrewLuckRoll, true) + " hires, " + RollNow() + " hires meanwhile"
: roll != S.CrewRollType && S.Events && EventOn(GameEvent.RecruitLuck) ? "enough cash again, " + RollNow() + " hires from now"
: "recruitment luck " + (roll == S.CrewRollType ? "began" : "ended") + ", " + RollNow() + " hires from now";
break;
}
if (readAgain)
{
readAgain = false;
var again = ReadCrewForRerolls(info);
if (again == null) { stop = "couldn't read the crew page"; fails = 3; break; }
crew = again;
counted = false;
}
double hold = rerollEmpty ? 0 : keep;
double cash = CashFor(crewHirePrice[(int)roll] + hold);
if (cash < 0 && Program.Reroller && cashEst >= 2 * crewHirePrice[(int)roll] + hold && crewHirePrice[(int)roll] > 0) { SnapshotOnce("reroller cash unread"); cash = cashEst; }
else if (cash >= 0) cashEst = cash;
if (cash < 0) { stop = "couldn't read the cash on hand"; fails++; game.Wait(1000); continue; }
if (!only && crewHirePrice[(int)roll] > cash - hold && roll != S.CrewRollType)
{
double whole = rerollEmpty ? 0 : keepAll, floor;
hold = KeepFloor(whole, true);
if (crewHirePrice[(int)roll] > cash - hold && WithdrawKeeping(crewHirePrice[(int)roll], true, 2 * crewHirePrice[(int)roll], whole, out floor))
{
hold = floor;
if (!game.OpenTab(Tab.Crew)) { stop = "the Crew page didn't open after the bank"; fails = 3; break; }
cash = CashFor(crewHirePrice[(int)roll] + hold);
if (cash < 0) { stop = "couldn't read the cash on hand"; fails++; game.Wait(1000); continue; }
}
}
if (only && Program.Reroller && crewHirePrice[(int)roll] > cash - hold)
{
double floor;
if (WithdrawKeeping(crewHirePrice[(int)roll], true, Math.Max(0, cash) + S.RerollerLimit, 0, out floor))
{
if (!game.OpenTab(Tab.Crew)) { stop = "the Crew page didn't open after the bank"; fails = 3; break; }
cash = CashFor(crewHirePrice[(int)roll] + hold);
if (cash < 0) { stop = "couldn't read the cash on hand"; fails++; game.Wait(1000); continue; }
}
}
rerollCash = cash;
if (crewHirePrice[(int)roll] > cash - hold)
{
stop = hold > 0 && crewHirePrice[(int)roll] <= cash
? "the cash is kept for the next property (" + M(hold) + ")"
: "saving up for " + roll + " hires (" + M(crewHirePrice[(int)roll]) + " each, " + M(cash) + " on hand)";
rerollShort = true;
rerollShortAt = DateTime.UtcNow;
nextReroll = DateTime.UtcNow.AddSeconds(ShortWaitSeconds(shortBefore, spent));
break;
}
Game.CrewPage p;
CrewRoll used;
double paid;
Game.CrewRow got;
if (rerollEmpty)
{
var empty = game.FindCrewRow(r => r.Empty, out p, true);
var short1 = game.CrewPageSeen;
if (empty == null && short1 != null && short1.Page && short1.Slots > 0 && short1.Henchmen == short1.Slots - 1)
{
game.Wait(900);
empty = game.FindCrewRow(r => r.Empty, out p, true);
}
if (empty == null)
{
var seen = game.CrewPageSeen;
if (seen != null && seen.Henchmen >= 0 && seen.Slots > 0 && seen.Henchmen >= seen.Slots) { rerollEmpty = false; OwnOpenBefore = -1; continue; }
stop = "a slot stands empty" + (seen != null && seen.Henchmen >= 0 && seen.Slots > 0 ? " (" + seen.Henchmen + " of " + seen.Slots + ")" : "")
+ " but it wasn't found to hire into - nobody is dismissed until it's filled";
if (DateTime.UtcNow - emptyFootSaidAt > TimeSpan.FromHours(1))
{
emptyFootSaidAt = DateTime.UtcNow;
Log("Crew: the empty slot wasn't found - the list's foot shows: " + game.CrewFootSeen());
}
game.Snapshot("crew empty slot not found", 60);
fails++; readAgain = true;
if (!only) break;
continue;
}
got = HireForReroll(empty, roll, cash, 0, out used, out paid);
if (got == null && game.HiredUnseen != null)
{
var seen = game.HiredUnseen;
rerollEmpty = false; OwnOpenBefore = -1; fails = 0; rerollHireFails = 0; readAgain = true; counted = false;
Count.Hired++; Count.CrewCash += paid; spent += paid;
PingHenchman(seen, emptiedRarity >= 0 ? emptiedRarity : crew.Count > 0 ? crew.Min(r => r.Rarity) : -1, false, crew);
emptiedRarity = -1; emptiedByStats = false;
Log("Crew: hired " + seen + " into the empty slot with a " + used + " hire (" + M(paid) + ") - the game said so; its row hadn't shown yet, so the list is read again");
continue;
}
if (got == null)
{
if (paid > 0) readAgain = true;
fails = HireNoRecruit(roll, paid, fails, out stop);
if (!only) break;
continue;
}
rerollEmpty = false; OwnOpenBefore = -1; fails = 0; rerollHireFails = 0;
Count.Hired++; Count.CrewCash += paid; spent += paid;
counted = false;
PingHenchman(got, emptiedRarity >= 0 ? emptiedRarity : crew.Count > 0 ? crew.Min(r => r.Rarity) : -1,
emptiedByStats && got.Rarity >= best && got.Attack >= Settings.PerfectFor(got.Rarity) && got.Defense >= Settings.PerfectFor(got.Rarity), crew);
emptiedRarity = -1; emptiedByStats = false;
got.Recruit = true;
crew.Add(got);
Game.NoteOwn(ownStats, got);
Log("Crew: hired " + got + " into the empty slot with a " + used + " hire (" + M(paid) + ")");
continue;
}
var order = crew.Where(Dismissable).OrderBy(r => r.Rarity).ThenBy(r => r.Power).ToList();
var worst = order.FirstOrDefault();
if (worst == null) { stop = "no henchmen to reroll"; break; }
if (Skipped(worst))
{
var next = order.FirstOrDefault(r => r.Rarity == worst.Rarity && !Skipped(r));
if (next == null)
{
stop = worst.Name + " is left alone until " + SkipUntil(worst).ToLocalTime().ToString("HH:mm") + " (its DISMISS kept failing), and there's no other " + worst.RarityName + " to reroll";
skipWait = true;
break;
}
worst = next;
}
var away = crew.Where(r => r.Rarity >= 0 && r.Rarity < worst.Rarity && !Dismissable(r)).OrderBy(r => r.Rarity).FirstOrDefault();
if (away != null && (worst.Rarity >= (int)Rarity.Mythic || worst.Rarity >= best))
{
stop = away.Name + " (" + away.RarityName + ") is away - no " + Game.RarityTitle(worst.Rarity) + " is dismissed until it's back";
awayWait = true;
break;
}
bool byStats = false;
Game.OwnStats weakOwn = null;
if (worst.Rarity >= best)
{
string everyone = "everyone is " + Game.RarityTitle(best) + ", the best " + roll + " hires can roll";
int min = S.CrewRerollStats > 0 ? Settings.PerfectFor(best) : 0;
if (min <= 0) { stop = everyone; done = finished = true; break; }
if (!S.EquipBest) { stop = everyone + " (rerolls by their own stats need \"Equip the best gear\" on)"; done = true; break; }
int unknown, unconfirmed;
var weak = Game.WeakestOwn(crew, ownStats, best, min, out unknown, out unconfirmed, r => Dismissable(r) && !Skipped(r));
if (ownJustRead) { ownUnknownLeft = unknown; ownJustRead = false; }
if (unknown > 0 && OwnReadDue(unknown))
{
ReadOwnStats();
ownJustRead = true;
readAgain = true;
if (crewUnequipped) { stop = "the crew's gear isn't back on yet - that goes first"; break; }
continue;
}
if (weak == null && unknown == 0 && unconfirmed > 0 && DateTime.UtcNow - ownReadAt > TimeSpan.FromHours(1) && DateTime.UtcNow - ownConfirmAt > TimeSpan.FromHours(1))
{
ownConfirmAt = DateTime.UtcNow;
Log("Crew: " + unconfirmed + " " + Game.RarityTitle(best) + (unconfirmed == 1 ? "" : "s") + " under " + min + " read only once - their own stats are read again before one is dismissed");
ReadOwnStats();
ownJustRead = true;
readAgain = true;
if (crewUnequipped) { stop = "the crew's gear isn't back on yet - that goes first"; break; }
continue;
}
if (weak == null)
{
if (unknown == 0 && unconfirmed > 0) { stop = unconfirmed + " " + Game.RarityTitle(best) + (unconfirmed == 1 ? "" : "s") + " read only once - read again before one is dismissed"; statsWait = statsSoon = true; }
else if (unknown == 0) { stop = everyone + ", with " + min + "+ attack and defense of their own"; done = finished = true; }
else { stop = "couldn't read the own stats of " + unknown + " henchm" + (unknown == 1 ? "an" : "en"); statsWait = true; statsSoon = unknown > ownUnknownLeft; }
break;
}
bool sorted = false;
for (int go = 0; go < (only ? 10 : 2) && !sorted; go++) SortCrew(40, out sorted);
if (!sorted) { stop = "sorting the crew by their own stats first"; readAgain = true; break; }
worst = weak;
weakOwn = Game.OwnOf(weak, ownStats);
byStats = true;
info.Status = "Rerolling the weakest " + Game.RarityTitle(best) + ": " + weak.Name + " (own " + weakOwn + ", under " + min + ").";
}
else info.Status = "Rerolling the worst: " + worst + ".";
if ((byStats || worst.Rarity >= (int)Rarity.Mythic) && !counted)
{
string why;
var all = ReadAllHenchmen(info, out why);
if (all == null) { stop = "no Mythic or better dismissed: " + why; listShort = true; break; }
crew = all;
counted = true;
lastHired = null;
continue;
}
var row = !byStats && ReferenceEquals(worst, lastHired) ? worst : game.FindCrewRow(r => Game.NamedLike(r, worst), out p, true);
if (row == null)
{
if (!reread) { reread = readAgain = true; continue; }
stop = "couldn't find " + worst + " in the list";
Log("Crew: " + worst.Name + " not found - the list's foot shows: " + game.CrewFootSeen());
game.Snapshot("crew row not found", 60);
game.NoteFailure(stop);
fails++; readAgain = true;
findFails = findFailsOn == worst.Key ? findFails + 1 : 1;
findFailsOn = worst.Key;
if (findFails >= 3)
{
rerollSkip.Add(Tuple.Create(worst.Name, worst.Rarity, DateTime.UtcNow.AddMinutes(45)));
Log("Crew: couldn't find " + worst + " in the list 3 times - it's left alone for 45 minutes, the next one goes first");
findFails = 0; fails = 0;
continue;
}
if (!only) break;
continue;
}
findFails = 0;
if (!Dismissable(row))
{
worst.Training |= row.Training; worst.Stationed |= row.Stationed; worst.Recovering |= row.Recovering;
continue;
}
Game.CrewRow emptied;
bool sure = (worst.Rarity < best && crew.All(r => r.Rarity >= worst.Rarity))
|| (byStats && weakOwn != null && worst.Rarity == best && crew.All(r => r.Rarity >= best));
string open;
int openBefore = S.CrewFill ? -1 : OpenBeforeDismiss();
if (!S.CrewFill)
{
if (openBefore < 0) { stop = "the crew page's count didn't read - nobody is dismissed until it does"; fails++; game.Wait(1000); continue; }
OwnOpenBefore = openBefore;
}
if (S.CrewFill && SlotStandsEmpty(out open))
{
if (++emptyHolds > 3) { stop = "a slot stands empty (" + open + ") and couldn't be filled - nobody is dismissed"; game.Snapshot("crew empty slot not filled", 60); fails = 3; break; }
Log("Crew: a slot stands empty (" + open + ") - it's filled before anyone else is dismissed");
rerollEmpty = true;
continue;
}
if (!game.DismissHenchman(row, worst, sure, out emptied))
{
game.Wait(1200);
var late = game.EmptyRowNear(row.Anchor.CenterY);
emptied = late != null && late.Anchor.CenterY >= row.Top && late.Anchor.CenterY <= row.Bottom ? late : null;
string nowOpen;
if (emptied == null && (S.CrewFill ? SlotStandsEmpty(out nowOpen) : OpenedSince(openBefore, out nowOpen)))
{
rerollEmpty = true;
OwnOpenBefore = S.CrewFill ? -2 : openBefore;
counted = false;
if (game.FindCrewRow(r => Game.NamedLike(r, worst), out p) == null)
{
dismissFails = 0;
crew.Remove(worst);
if (byStats) ownStats.Remove(weakOwn);
Log("Crew: dismissed " + worst + " - its row didn't show the empty slot, but the page counts " + nowOpen + " henchmen and it's gone from the list: the slot is filled next");
emptiedRarity = worst.Rarity; emptiedByStats = byStats;
}
else Log("Crew: a slot stands empty (" + nowOpen + ") - it's filled before anyone else is dismissed");
continue;
}
if (emptied == null)
{
var again = game.FindCrewRow(r => Game.NamedLike(r, worst), out p);
if (again == null || !game.DismissHenchman(again, worst, sure, out emptied))
{
string lateOpen;
if (Program.Reroller && S.CrewFill) { game.Wait(1000); }
if (Program.Reroller && S.CrewFill && SlotStandsEmpty(out lateOpen))
{
dismissFails = 0;
crew.Remove(worst);
rerollEmpty = true; OwnOpenBefore = -2; counted = false;
emptiedRarity = worst.Rarity; emptiedByStats = byStats;
Log("Crew: dismissed " + worst + " - its slot showed empty late (" + lateOpen + "): it's filled next");
continue;
}
stop = "DISMISS didn't take on " + worst; game.Snapshot("crew dismiss failed", 60);
game.NoteFailure(stop);
dismissFails = dismissFailsOn == worst.Key ? dismissFails + 1 : 1;
dismissFailsOn = worst.Key;
fails++;
if (dismissFails >= 3)
{
rerollSkip.Add(Tuple.Create(worst.Name, worst.Rarity, DateTime.UtcNow.AddMinutes(45)));
Log("Crew: DISMISS failed 3 times on " + worst + " - it's left alone for 45 minutes, the next one goes first");
dismissFails = 0; fails = 0; readAgain = true;
continue;
}
if (!only) break;
continue;
}
}
}
dismissFails = 0;
crew.Remove(worst);
if (byStats)
{
ownStats.Remove(weakOwn);
Log("Crew: dismissed " + worst.Name + " (" + worst.RarityName + ", own " + weakOwn + ", under " + Settings.PerfectFor(worst.Rarity) + ") to roll a better one");
}
rerollEmpty = true;
OwnOpenBefore = openBefore >= 0 ? openBefore : -2;
emptiedRarity = worst.Rarity; emptiedByStats = byStats;
if (AutoRollMay(byStats, worst.Rarity, best))
{
if (StartRollInto(emptied, roll, best, worst, crew))
{
rerollEmpty = false; OwnOpenBefore = -1; emptiedRarity = -1; emptiedByStats = false; counted = false;
stop = "the game's Auto Roll rolls " + worst.Name + "'s slot for " + Game.RarityTitle(best) + " or better";
break;
}
if (!game.CrewClear())
{
stop = "the Auto Roll didn't start and something is over the crew list - the empty slot is filled next go";
game.Snapshot("crew auto roll page not clear", 60);
fails = 3;
break;
}
if (emptied != null) emptied = game.EmptyRowNear(emptied.Anchor.CenterY) ?? emptied;
}
got = HireForReroll(emptied, roll, cash, hold, out used, out paid);
if (got == null && game.HiredUnseen != null)
{
var seen = game.HiredUnseen;
rerollEmpty = false; OwnOpenBefore = -1; fails = 0; rerollHireFails = 0; reread = false; readAgain = true; counted = false; lastHired = null;
emptiedRarity = -1; emptiedByStats = false;
rolls++; spent += paid; Count.Rerolls++; Count.CrewCash += paid;
if (seen.Rarity > worst.Rarity) better.Add(seen.ToString());
PingHenchman(seen, worst.Rarity, false, crew);
Log("Crew: hired " + seen + " in place of " + worst + " - the game said so; its row hadn't shown yet, so the list is read again");
continue;
}
if (got == null)
{
if (paid > 0) { rerollEmpty = false; readAgain = true; emptiedRarity = -1; }
fails = HireNoRecruit(roll, paid, fails, out stop);
if (!only) break;
continue;
}
rerollEmpty = false; OwnOpenBefore = -1; fails = 0; rerollHireFails = 0; reread = false; emptiedRarity = -1; emptiedByStats = false;
rolls++; spent += paid; Count.Rerolls++; Count.CrewCash += paid; cashEst -= paid;
if (!Program.Reroller) counted = false;
got.Recruit = true;
crew.Add(got);
Game.NoteOwn(ownStats, got);
lastHired = got;
if (got.Rarity > worst.Rarity) better.Add(got.ToString());
PingHenchman(got, worst.Rarity, byStats && got.Rarity >= best && got.Attack >= Settings.PerfectFor(got.Rarity) && got.Defense >= Settings.PerfectFor(got.Rarity), crew);
SnapshotOnce("crew reroll");
}
}
finally
{
if (!readAgain && crew != null) { foreach (var r in crew) r.Recruit = false; rerollCrew = crew; rerollCrewAt = DateTime.UtcNow; }
if (rolls > 0 && crew != null)
Log(string.Format("Crew: {0} reroll{1} with {2} hires ({3}){4}. The worst now: {5}{6}", rolls, rolls == 1 ? "" : "s", roll, M(spent),
better.Count > 0 ? ", better than before: " + string.Join(", ", better) : "", crew.OrderBy(r => r.Rarity).ThenBy(r => r.Power).FirstOrDefault(),
pausedFor != null ? " (paused for: " + pausedFor + ")" : ""));
}
if (rolls == 0 && (pausedFor != null || rerollShort)) Neutral();
if (pausedFor != null && rolls == 0) { pausedIdle = pausedFor; pausedIdleAt = DateTime.UtcNow; }
if (pausedFor != null) { nextReroll = DateTime.UtcNow; info.Status = "Rerolls paused for: " + pausedFor + "."; }
else if (rolls >= 500) nextReroll = DateTime.UtcNow;
else if (stop != null)
{
info.Status = (done ? "Rerolls done: " : "Rerolls wait: ") + stop + ".";
if (done) nextReroll = DateTime.UtcNow.AddMinutes(30);
if (statsWait) nextReroll = ownReadAt.AddHours(statsSoon ? 1 : 6);
if (statsWait && statsSoon && ownConfirmAt.AddHours(1) > nextReroll) nextReroll = ownConfirmAt.AddHours(1);
if (skipWait) nextReroll = rerollSkip.Select(x => x.Item3).DefaultIfEmpty(DateTime.UtcNow.AddMinutes(30)).Min();
if (awayWait) nextReroll = DateTime.UtcNow.AddMinutes(5);
if (listShort)
{
nextReroll = DateTime.UtcNow.AddMinutes(listShortFails <= 1 ? 5 : listShortFails == 2 ? 10 : 20);
info.Status = "Rerolls stuck since " + listShortSince.ToLocalTime().ToString("HH:mm") + ": " + stop.Replace("no Mythic or better dismissed: ", "") + ".";
}
string said = rerollShort ? "saving up " + roll + (keep > 0 ? " and keeping " + M(keep) : "") : stop;
if (rerollSaid != said) { rerollSaid = said; if (!only) Log("Crew: rerolls " + (done ? "done" : "wait") + " - " + stop); }
}
var skipped = rerollSkip.Where(x => x.Item3 > DateTime.UtcNow).ToList();
if (skipped.Count > 0) info.Status += " Left alone for now (its DISMISS kept failing): " + string.Join(", ", skipped.Select(x => x.Item1 + " until " + x.Item3.ToLocalTime().ToString("HH:mm"))) + ".";
if (finished && !only) RerollsFirstDone(stop);
if (only) OnlyGo(done, stop, rolls, keep);
}
internal sealed class AutoRun
{
public CrewRoll Tier;
public int Target;
public string Replaced = "";
public int ReplacedRarity = -1;
public List<Game.CrewRow> Before;
public bool Running;
public DateTime StartedAt;
public double Spent = -1, SpentBefore;
public int Hires = -1, HiresBefore;
public string Now = "";
public int Starts;
public bool Restored;
public int Misses;
}
AutoRun autoRun;
bool autoRollOff;
int autoRollGrey;
DateTime autoRollNext = DateTime.MinValue;
DateTime autoRollRest = DateTime.MinValue;
const int AutoRollMinutes = 3;
bool AutoRollOn { get { return S.CrewAutoRoll && S.CrewReroll && !autoRollOff; } }
bool AutoRollMay(bool byStats, int rarity, int best)
{
var now = DateTime.UtcNow;
return AutoRollOn && !byStats && rarity < best && now >= autoRollNext && now >= autoRollRest;
}
bool AutoRollHoldsBank() { return autoRun != null && autoRun.Running && !FightOnly; }
const string RollStopTask = "stop auto roll";
const int RollStopSeconds = 15;
int rollStopTries;
DateTime rollStopNext = DateTime.MinValue;
bool RollUnwanted { get { return autoRun != null && autoRun.Running && !(S.CrewAutoRoll && S.CrewReroll); } }
bool RollStopDue(DateTime now) { return RollUnwanted && now >= rollStopNext && Can(Tab.Crew); }
void DoRollStop()
{
var run = autoRun;
if (run == null || !run.Running) return;
string why;
bool done;
try { done = StopOwnRoll(!S.CrewReroll ? "the rerolls are off" : "\"Use the game's Auto Roll\" is off", out why); }
catch (NeverPressException) { done = false; why = "the press was refused"; }
catch (UserBusyException) { throw; }
catch (StopException) { throw; }
catch (NeedUserException) { throw; }
catch (Exception e) { RollStopMissed(run, e is WindowChangedException ? e.Message : "an error: " + e.Message, false); throw; }
if (!done) RollStopMissed(run, why, true);
}
void RollStopMissed(AutoRun run, string why, bool say)
{
if (autoRun != run || !run.Running) return;
if (say) game.NoteFailure("the Auto Roll's STOP: " + why);
if (++rollStopTries >= 3 && RollStopGivesUp(S.Bank, S.BankAt, Can(Tab.Bank)))
{
Log("Auto Roll: STOP not pressed on " + run.Replaced + "'s slot (" + why + ", 3 tries) - left to the game; the bank goes on, its deposit ends the roll");
EndRun(run);
return;
}
int wait = rollStopTries >= 3 ? 10 : 1;
rollStopNext = DateTime.UtcNow.AddMinutes(wait);
if (say) Log("Auto Roll: STOP not pressed on " + run.Replaced + "'s slot (" + why + ") - again in " + (wait == 1 ? "a minute" : wait + " minutes"));
}
internal static bool RollStopGivesUp(bool bank, long bankAt, bool bankInMenu) { return bank && bankAt <= 0 && bankInMenu; }
bool StopOwnRoll(string reason, out string why)
{
why = null;
var run = autoRun;
if (run == null || !run.Running) return true;
if (!game.OpenTab(Tab.Crew)) { why = "the Crew page didn't open"; return false; }
if (game.LookAtCrew() == null) { why = "the Crew page didn't read"; return false; }
string told;
var row = FindOwnRoll(run, out told);
if (row == null)
{
if (told == "over")
{
Log("Auto Roll: " + run.Replaced + "'s slot isn't rolling any more - " + reason);
EndRun(run);
return true;
}
if (told == "yours" && run.Restored) { LeaveRun(run, "the slot rolling now isn't the bot's roll from before"); return true; }
why = told == "yours" ? "its slot wasn't found in the list" : "couldn't tell whether it still rolls";
game.Snapshot("crew auto roll stop not found", 60);
return false;
}
if (row.RollSpent >= 0) run.Spent = row.RollSpent;
if (row.RollHires >= 0) run.Hires = row.RollHires;
if (row.Name.Length > 0) run.Now = row.ToString();
if (!game.StopAutoRoll(row)) { why = "its STOP didn't take"; game.Snapshot("crew auto roll stop failed", 60); return false; }
Log("Auto Roll: stopped on " + run.Replaced + "'s slot - " + reason + "; " + (run.HiresBefore + Math.Max(0, run.Hires)) + " hires, "
+ M(run.SpentBefore + Math.Max(0, run.Spent)) + " spent" + (run.Now.Length > 0 ? ", " + run.Now + " stays" : ""));
EndRun(run);
return true;
}
internal static bool RollMayBe(int rollFor, bool tierRead, CrewRoll tier, int target, CrewRoll runTier)
{
return (rollFor < 0 || rollFor == target) && (!tierRead || tier == runTier);
}
internal static bool RunMayBe(int rollFor, bool tierRead, CrewRoll tier, int hires, double spent, AutoRun run)
{
if (!RollMayBe(rollFor, tierRead, tier, run.Target, run.Tier)) return false;
if (!run.Restored) return true;
return rollFor >= 0 && (hires < 0 || run.Hires < 0 || hires >= run.Hires) && (spent < 0 || run.Spent < 0 || spent >= run.Spent);
}
Game.CrewRow FindOwnRoll(AutoRun run, out string told)
{
told = null;
Func<Game.CrewRow, bool> mine = r => RunMayBe(r.RollFor, r.RollTierRead, r.RollTier, r.RollHires, r.RollSpent, run);
Game.CrewPage page;
var any = game.FindRollingRow(out page);
if (any == null)
{
if (page != null && page.Page && page.TopInSight && !page.AutoRolls) told = "over";
return null;
}
if (mine(any)) return any;
var row = game.FindRollingRow(out page, mine);
if (row == null && page != null && page.Page && page.TopInSight) told = "yours";
return row;
}
void EndRun(AutoRun run)
{
Count.CrewCash += run.SpentBefore + Math.Max(0, run.Spent);
if (autoRun == run) autoRun = null;
rollStopTries = 0; rollStopNext = DateTime.MinValue;
NoteRun();
}
void LeaveRun(AutoRun run, string why)
{
Log("Auto Roll: " + why + " - left to you");
if (autoRun == run) autoRun = null;
rollStopTries = 0; rollStopNext = DateTime.MinValue;
NoteRun();
}
bool Finishing { get { return finishing && !stopAgain && finishEnd - Environment.TickCount > 0; } }
void StopRollAtEnd()
{
var run = autoRun;
if (run == null || !run.Running) return;
string why = null;
bool done = false, turn = false;
var t = turns;
if (!stopAgain)
{
finishEnd = Environment.TickCount + RollStopSeconds * 1000;
finishing = true;
State("Stopping: the game's Auto Roll first");
try
{
while (Finishing && game.Win.UserActiveWithin(1000)) Thread.Sleep(200);
if (!Finishing) why = "you were using the PC";
else if (t != null && !(turn = t.Take(this, 100, () => !Finishing, Math.Max(1, finishEnd - Environment.TickCount)))) why = "another account had the mouse";
else if (!game.Win.Find()) why = "Roblox isn't open";
else if (!game.Win.BeginAct()) why = "Roblox couldn't be brought to the front";
else
{
acting = true;
try
{
while (!done && Finishing)
{
done = StopOwnRoll("the bot was stopped", out why);
if (!done) game.Wait(1000);
}
}
catch (UserBusyException) { why = "you used the PC"; }
finally { acting = false; game.Win.EndAct(S.GiveFocusBack, false); }
}
}
catch (StopException) { }
catch (Exception e) { why = e.Message; }
finally
{
finishing = false;
if (turn) t.Pass(this);
}
}
if (!done && autoRun == run)
Log("Auto Roll: STOP not pressed on " + run.Replaced + "'s slot (" + (stopAgain ? (keepRoll ? "the bot was closed" : "you pressed Stop again") : why ?? "it took too long")
+ ") - the bot stopped anyway; the next Start looks at it again");
}
void NoteRun()
{
var r = autoRun;
var now = DateTime.UtcNow;
string want = r != null && r.Running ? RunText(r, now) : "";
string had = S.AutoRollMine ?? "";
if (want == had) return;
DateTime seen;
if (want.Length > 0 && had.Length > 0 && RunBody(want) == RunBody(had) && RunSeen(had, out seen) && now - seen < TimeSpan.FromMinutes(10)) return;
S.AutoRollMine = want;
SaveSettings();
}
void RestoreRun()
{
var r = RunFrom(S.AutoRollMine, DateTime.UtcNow);
if (r == null)
{
if (!string.IsNullOrEmpty(S.AutoRollMine)) { S.AutoRollMine = ""; SaveSettings(); }
return;
}
autoRun = r;
rollStopTries = 0; rollStopNext = DateTime.MinValue;
Log("Auto Roll: the bot's own roll on " + r.Replaced + "'s slot may still run (from before) - the bot looks for its slot before it watches it");
}
internal static string RunText(AutoRun r, DateTime seen)
{
var c = CultureInfo.InvariantCulture;
return string.Join("|", seen.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", c), ((int)r.Tier).ToString(c), r.Target.ToString(c), r.SpentBefore.ToString("R", c),
r.HiresBefore.ToString(c), r.Spent.ToString("R", c), r.Hires.ToString(c), r.ReplacedRarity.ToString(c), r.Starts.ToString(c),
(r.Replaced ?? "").Replace("|", " ").Replace("\r", " ").Replace("\n", " "));
}
static string RunBody(string text) { int bar = text.IndexOf('|'); return bar < 0 ? text : text.Substring(bar); }
static bool RunSeen(string text, out DateTime seen)
{
int bar = text.IndexOf('|');
return DateTime.TryParse(bar < 0 ? text : text.Substring(0, bar), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out seen);
}
internal static AutoRun RunFrom(string text, DateTime now)
{
if (string.IsNullOrEmpty(text)) return null;
var p = text.Split(new[] { '|' }, 10);
if (p.Length != 10) return null;
var c = CultureInfo.InvariantCulture;
DateTime seen;
int tier, target, hiresBefore, hires, replacedRarity, starts;
double spentBefore, spent;
if (!RunSeen(text, out seen) || now - seen > TimeSpan.FromHours(6) || seen - now > TimeSpan.FromMinutes(10)) return null;
if (!int.TryParse(p[1], NumberStyles.Integer, c, out tier) || tier < 0 || tier > 2 || !int.TryParse(p[2], NumberStyles.Integer, c, out target) || target < 0
|| !double.TryParse(p[3], NumberStyles.Float, c, out spentBefore) || !int.TryParse(p[4], NumberStyles.Integer, c, out hiresBefore)
|| !double.TryParse(p[5], NumberStyles.Float, c, out spent) || !int.TryParse(p[6], NumberStyles.Integer, c, out hires)
|| !int.TryParse(p[7], NumberStyles.Integer, c, out replacedRarity) || !int.TryParse(p[8], NumberStyles.Integer, c, out starts)) return null;
return new AutoRun { Tier = (CrewRoll)tier, Target = target, SpentBefore = Math.Max(0, spentBefore), HiresBefore = Math.Max(0, hiresBefore), Spent = spent, Hires = hires,
ReplacedRarity = replacedRarity, Starts = Math.Max(1, starts), Replaced = p[9], Running = true, StartedAt = seen, Restored = true };
}
bool StartRollInto(Game.CrewRow emptied, CrewRoll roll, int best, Game.CrewRow worst, List<Game.CrewRow> crew)
{
if (emptied == null) return false;
if (emptied.Auto == null)
{
game.Wait(600);
emptied = game.EmptyRowNear(emptied.Anchor.CenterY) ?? emptied;
}
bool started = game.StartAutoRoll(emptied, null, roll, best);
autoRollNext = DateTime.UtcNow.AddMinutes(AutoRollMinutes);
if (!started)
{
autoRun = null;
if (game.AutoRollNoPass)
{
if (++autoRollGrey >= 3)
{
autoRollOff = true;
Log("Auto Roll: " + game.AutoRollWhy + " - three looks in a row, so off until the bot is started again; the bot hires itself as before");
}
else
{
autoRollNext = DateTime.UtcNow.AddMinutes(10);
Log("Auto Roll: " + game.AutoRollWhy + " (look " + autoRollGrey + " of 3) - looking again in 10 minutes; the bot hires itself meanwhile");
}
}
else
{
autoRollGrey = 0;
autoRollNext = DateTime.UtcNow.AddMinutes(30);
Log("Auto Roll: not started on " + worst.Name + "'s slot - " + game.AutoRollWhy + "; the bot hires into it itself (Auto Roll again in 30 minutes)");
}
NoteRun();
return false;
}
autoRollGrey = 0;
var run = autoRun != null && !autoRun.Running && autoRun.Tier == roll && autoRun.Target == best ? autoRun : new AutoRun { Tier = roll, Target = best };
if (run.Spent > 0) run.SpentBefore += run.Spent;
if (run.Hires > 0) run.HiresBefore += run.Hires;
run.Spent = -1; run.Hires = -1;
run.Replaced = worst.Name; run.ReplacedRarity = run.Starts == 0 ? worst.Rarity : run.ReplacedRarity;
run.Before = crew.ToList();
run.Running = true;
run.Restored = false; run.Misses = 0;
run.StartedAt = DateTime.UtcNow;
run.Starts++;
autoRun = run;
rollStopTries = 0; rollStopNext = DateTime.MinValue;
Log("Auto Roll: " + (run.Starts == 1 ? "started" : "started again (" + run.Starts + ")") + " on " + worst + "'s slot - " + roll + " hires until " + Game.RarityTitle(best)
+ " or better, this slot only" + (run.SpentBefore > 0 ? " (" + M(run.SpentBefore) + " spent there so far)" : "") + "; the bank waits while it rolls");
NoteRun();
SnapshotOnce("crew auto roll started");
return true;
}
bool AutoRollHere(CrewInfo info, double keep, bool fromCache, ref List<Game.CrewRow> crew, out string wait)
{
wait = null;
bool rolls;
Game.CrewRow rolling = null;
if (fromCache)
{
var p = game.LookAtCrew();
rolls = p != null && p.AutoRolls;
if (p != null) rolling = p.Rolling.FirstOrDefault(r => !r.Cut);
}
else { rolls = game.CrewRollsSeen; rolling = game.CrewRollingSeen; }
var now = DateTime.UtcNow;
switch (RollState(rolls, autoRun != null && autoRun.Running))
{
case "yours":
wait = "a slot is auto-rolling in the game (STOP ALL AUTO ROLLS shows) - the rerolls wait until it's done";
SayOnce("auto roll yours " + now.ToString("yyyyMMddHH"), "Crew: " + wait);
nextReroll = now.AddMinutes(5);
return true;
case "watch":
{
Game.CrewPage page;
var run = autoRun;
if (run.Restored)
{
string told;
rolling = FindOwnRoll(run, out told);
if (rolling == null)
{
if (told == "yours" || ++run.Misses >= 5)
{
LeaveRun(run, told == "yours" ? "the slot rolling now isn't the bot's roll from before" : "the bot's roll from before wasn't found in the list (5 looks)");
wait = "a slot is auto-rolling in the game that the bot didn't start - the rerolls wait until it's done";
nextReroll = now.AddMinutes(5);
return true;
}
wait = "the bot's Auto Roll from before: its slot wasn't found in the list";
game.Snapshot("crew auto roll row not found", 60);
nextReroll = now.AddMinutes(1);
return true;
}
run.Restored = false; run.Misses = 0;
if (run.Before == null) run.Before = crew.ToList();
Log("Auto Roll: the bot's own roll from before still rolls on " + run.Replaced + "'s slot - watched again");
}
else rolling = game.FindRollingRow(out page);
if (rolling == null)
{
wait = "the game's Auto Roll rolls, but its slot wasn't found in the list";
game.Snapshot("crew auto roll row not found", 60);
nextReroll = now.AddMinutes(1);
return true;
}
if (rolling.RollSpent >= 0) autoRun.Spent = rolling.RollSpent;
if (rolling.RollHires >= 0) autoRun.Hires = rolling.RollHires;
if (rolling.Name.Length > 0) autoRun.Now = rolling.ToString();
double total = autoRun.SpentBefore + Math.Max(0, autoRun.Spent);
double cashNow = CashNow();
double floor = KeepFloorRead(keep, false);
string stopWhy = RollOverLimit(autoRun.SpentBefore, autoRun.Spent, S.AutoRollLimit) ? "its rolls spent " + M(total) + " (the limit is " + M(S.AutoRollLimit) + ")"
: floor > 0 && cashNow >= 0 && cashNow < floor ? "the cash on hand is down to what's kept for the next property (" + M(floor) + ")" : null;
if (stopWhy != null)
{
bool stopped = game.StopAutoRoll(rolling);
Log("Auto Roll: " + (stopped ? "stopped" : "STOP didn't take on") + " the slot - " + stopWhy + ", " + (autoRun.HiresBefore + Math.Max(0, autoRun.Hires)) + " hires"
+ (autoRun.Now.Length > 0 ? "; " + autoRun.Now + " stays" : ""));
Count.CrewCash += total;
if (!stopped) { game.Snapshot("crew auto roll stop failed", 60); nextReroll = now.AddMinutes(1); wait = "the Auto Roll's STOP didn't take"; return true; }
autoRun = null;
autoRollRest = now.AddHours(1);
nextReroll = autoRollRest;
wait = "the game's Auto Roll stopped at its limit - the next in an hour";
return true;
}
double price = crewHirePrice[(int)autoRun.Tier];
if (price > 0 && !RollUnwanted && cashNow >= 0 && cashNow < price * 3 + Math.Max(0, floor) && RollCash(autoRun.Tier, keep, total, out floor)) game.OpenTab(Tab.Crew);
nextReroll = now.AddMinutes(1);
wait = "the game's Auto Roll rolls " + autoRun.Replaced + "'s slot for " + Game.RarityTitle(autoRun.Target) + " or better: "
+ (autoRun.HiresBefore + Math.Max(0, autoRun.Hires)) + " hires, " + M(total) + " spent" + (autoRun.Now.Length > 0 ? ", now " + autoRun.Now : "");
return true;
}
case "ended":
{
if (fromCache) { crew = ReadCrewForRerolls(info); if (crew == null) { wait = "couldn't read the crew page"; return true; } }
int fresh;
var got = RollLeft(autoRun.Before, crew, out fresh);
double total = autoRun.SpentBefore + Math.Max(0, autoRun.Spent);
int hires = autoRun.HiresBefore + Math.Max(0, autoRun.Hires);
autoRun.Running = false;
if (got == null)
{
Log("Auto Roll: the roll on " + autoRun.Replaced + "'s slot " + (autoRun.Restored ? "from before is over"
: "ended, but the henchman it left wasn't told apart (" + fresh + " new in the list)") + " - about " + hires + " hires, " + M(total) + " spent");
Count.CrewCash += total;
autoRun = null;
autoRollNext = DateTime.MinValue;
return false;
}
if (got.Rarity >= autoRun.Target)
{
Log("Auto Roll: got " + got.Name + " (" + got.RarityName + ") after " + (hires > 0 ? "about " + hires : "some") + " hires, " + M(total) + " spent");
Count.CrewCash += total; Count.Rerolls++;
PingHenchman(got, autoRun.ReplacedRarity, false, crew);
autoRun = null;
autoRollNext = DateTime.MinValue;
return false;
}
if (RollOverLimit(autoRun.SpentBefore, autoRun.Spent, S.AutoRollLimit))
{
Log("Auto Roll: the slot's rolls stopped with " + got + " after " + M(total) + " (the limit is " + M(S.AutoRollLimit) + ") - the next in an hour");
Count.CrewCash += total;
autoRun = null;
autoRollRest = now.AddHours(1);
nextReroll = autoRollRest;
wait = "the game's Auto Roll spent its limit on that slot - the next in an hour";
return true;
}
Log("Auto Roll: stopped short with " + got + " after " + (hires > 0 ? "about " + hires : "some") + " hires, " + M(total) + " spent (the cash on hand ran out, most likely)"
+ " - started again " + (now >= autoRollNext ? "now" : "at " + autoRollNext.ToLocalTime().ToString("HH:mm")));
if (now < autoRollNext)
{
nextReroll = autoRollNext;
wait = "the game's Auto Roll starts again at " + autoRollNext.ToLocalTime().ToString("HH:mm");
return true;
}
return false;
}
}
return false;
}
internal static string RollState(bool rolls, bool ours)
{
if (rolls) return ours ? "watch" : "yours";
return ours ? "ended" : "";
}
internal static bool RollOverLimit(double spentBefore, double spent, long limit)
{
return limit > 0 && spentBefore + Math.Max(0, spent) >= limit;
}
internal static Game.CrewRow RollLeft(List<Game.CrewRow> before, List<Game.CrewRow> now, out int fresh)
{
fresh = 0;
if (before == null || now == null) return null;
var left = before.ToList();
var news = new List<Game.CrewRow>();
foreach (var r in now)
{
var same = left.FirstOrDefault(b => Game.NamedLike(b, r)) ?? left.FirstOrDefault(b => b.Rarity == r.Rarity && Game.WholeNameSame(b.Name, r.Name))
?? left.FirstOrDefault(b => Game.SameHenchman(b, r));
if (same != null) left.Remove(same);
else news.Add(r);
}
fresh = news.Count;
return news.Count == 1 ? news[0] : null;
}
internal static double RollStake(double price, long limit, double spent)
{
if (price <= 0) return 0;
return Math.Max(price, Math.Min(limit > 0 ? limit - spent : price * 100, price * 100));
}
internal static double RollWant(double stake, double cash, double banked, double keep)
{
return Math.Min(stake - Math.Max(0, cash), banked - Math.Max(0, keep));
}
internal static bool RollTakes(double want, double cash, double price)
{
return want > 0 && cash + want >= price && (want >= price || cash < price);
}
internal static long RollAmount(double want, double cash, double banked, double keep)
{
long amount = AmountToWithdraw(cash + want, cash, banked);
if (amount <= 0) return 0;
double free = Math.Floor(banked * 0.995 - Math.Max(0, keep));
return free < 1 ? 0 : (long)Math.Min(amount, free);
}
bool RollCash(CrewRoll tier, double keep, double spent, out double floor)
{
floor = Math.Max(0, keep);
if (RerollOnly) return false;
double stake = RollStake(crewHirePrice[(int)tier], S.AutoRollLimit, spent);
if (stake <= 0 || !CanWithdraw() || BankFee() != 0) return false;
double cash = CashNow();
if (cash < 0) return false;
double banked;
using (var f = game.Capture()) banked = game.ReadHeader(f).Banked;
if (banked < 0) return false;
floor = KeepOnHand(keep, banked);
if (banked <= 0) return false;
double want = RollWant(stake, cash, banked, keep);
if (!RollTakes(want, cash, crewHirePrice[(int)tier])) return false;
long amount = RollAmount(want, cash, banked, keep);
if (amount <= 0 || !RollTakes(amount, cash, crewHirePrice[(int)tier])) return false;
string why;
bool seen;
string typed = game.WithdrawAmount(amount, out why, out seen);
if (typed != null && seen)
{
Log("Bank: took " + M(amount) + " out for the crew's Auto Roll (it rolls on the cash on hand)");
floor = KeepOnHand(keep, banked - amount);
return true;
}
if (typed != null) { cashOut = true; nextBank = DateTime.UtcNow; floor = KeepOnHand(keep, banked - amount); }
SayOnce("auto roll cash " + (why ?? "unseen").Split('(', '"')[0], "Bank: no cash came out for the Auto Roll (" + (why ?? "the top bar showed none coming out") + ")");
return false;
}
bool RerollsLead() { return S.RerollsFirst || (S.Events && EventOn(GameEvent.RecruitLuck)); }
CrewRoll RollNow()
{
var roll = LuckRollFor(S.CrewRollType, S.CrewLuckRoll, S.Events && EventOn(GameEvent.RecruitLuck));
return roll == S.CrewRollType || LuckRollPaid(roll) ? roll : S.CrewRollType;
}
bool LuckRollPaid(CrewRoll roll)
{
double price = crewHirePrice[(int)roll];
if (price <= 0 || last == null) return true;
if (RerollOnly)
{
double cash = rerollCash >= 0 ? rerollCash : last.Cash;
return cash < 0 || cash >= price;
}
if (last.Cash < 0) return true;
return Spendable(last) >= price + (rerollEmpty ? 0 : PropertyKeep());
}
internal static CrewRoll EmptySlotHire(CrewRoll now, CrewRoll usual, double price, double cash)
{
return now != usual && cash >= 0 && price > cash ? usual : now;
}
bool LuckRollShort() { return S.Events && EventOn(GameEvent.RecruitLuck) && LuckRollFor(S.CrewRollType, S.CrewLuckRoll, true) != S.CrewRollType && RollNow() == S.CrewRollType; }
internal static CrewRoll LuckRollFor(CrewRoll usual, LuckRoll luck, bool lucky)
{
if (!lucky || luck == LuckRoll.Same) return usual;
return luck == LuckRoll.Elite ? CrewRoll.Elite : CrewRoll.Professional;
}
public const int BreakMinutes = 10;
DateTime fullBreakAt = DateTime.MinValue;
bool rerollsYield;
internal static string FullBarBreak(List<string> due, Header h)
{
bool energyFull = h.EnergyMax > 0 && h.Energy >= h.EnergyMax;
bool staminaFull = h.StaminaMax > 0 && h.Stamina >= h.StaminaMax;
var tasks = due.Where(t => (energyFull && (t == "heists" || t == "jobs"))
|| (staminaFull && (t == "takedown" || t == "give stamina" || t == "bosses" || t == "fights"))).ToList();
return tasks.Count > 0 ? string.Join(", ", tasks) : null;
}
internal static string TimedBreak(DateTime now, DateTime since, bool bank, bool shopDue)
{
if (now - since < TimeSpan.FromMinutes(BreakMinutes)) return shopDue ? "shop" : null;
var tasks = new List<string>();
if (bank) tasks.Add("bank");
if (shopDue) tasks.Add("shop");
return string.Join(", ", tasks);
}
void RerollsFirstDone(string why)
{
if (!S.RerollsFirst) return;
S.RerollsFirst = false;
SaveSettings();
Log("Crew: " + why + " - the rerolls went back from First to On");
}
readonly List<Tuple<string, int, DateTime>> rerollSkip = new List<Tuple<string, int, DateTime>>();
internal static bool Dismissable(Game.CrewRow r) { return !r.Training && !r.Stationed && !r.Recovering; }
bool Skipped(Game.CrewRow r)
{
var now = DateTime.UtcNow;
rerollSkip.RemoveAll(x => x.Item3 <= now);
return rerollSkip.Any(x => SkipNames(x, r));
}
DateTime SkipUntil(Game.CrewRow r)
{
return rerollSkip.Where(x => SkipNames(x, r)).Select(x => x.Item3).DefaultIfEmpty(DateTime.UtcNow).Max();
}
static bool SkipNames(Tuple<string, int, DateTime> x, Game.CrewRow r)
{
return x.Item2 == r.Rarity && (Game.WholeNameSame(x.Item1, r.Name) || (Parse.Key(x.Item1).Length < 3 && Parse.Key(r.Name).Length < 3));
}
int onlyFails;
DateTime onlyProgressAt;
void OnlyGo(bool done, string why, int rolls, double keep)
{
var now = DateTime.UtcNow;
if (rolls > 0) { onlyFails = 0; onlyProgressAt = now; }
if (why == null && rolls > 0) return;
if (rerollerBreak != null) { onlyFails = 0; onlyProgressAt = now; return; }
if (autoRun != null) { onlyFails = 0; onlyProgressAt = now; if (nextReroll < now) nextReroll = now.AddMinutes(1); return; }
if (done) { EndRerollOnly(why, true); return; }
if (rerollShort)
{
game.Wait(1500);
double cash = CashNow();
if (cash < 0 || cash >= crewHirePrice[(int)RollNow()] + keep) { nextReroll = now; return; }
EndRerollOnly(why, true);
return;
}
onlyFails++;
if (onlyFails >= 3 || now - onlyProgressAt > TimeSpan.FromHours(1)) { EndRerollOnly(why + " (" + onlyFails + " tries in a row)", false); return; }
if (nextReroll < now.AddMinutes(1)) nextReroll = now.AddMinutes(onlyFails == 1 ? 1 : 2);
Log("Only rerolling: " + (why ?? "nothing rerolled") + " - trying again at " + nextReroll.ToLocalTime().ToString("HH:mm"));
}
const string CrewBusy = "the game says the crew is being updated (\"try again shortly\")";
Game.CrewRow HireForReroll(Game.CrewRow empty, CrewRoll roll, double cash, double keep, out CrewRoll used, out double paid)
{
var got = game.HireInto(empty, roll, false, cash, keep, out used, out paid);
if (game.HireBusy) return null;
if (got == null && paid <= 0)
{
game.Wait(400);
var again = game.EmptyRowNear(empty.Anchor.CenterY);
if (again != null) got = game.HireInto(again, roll, false, cash, keep, out used, out paid);
}
if (got == null && game.HiredUnseen == null && (paid <= 0 || rerollHireFails >= 2)) game.Snapshot(paid > 0 ? "crew hire no recruit" : "crew hire not pressed", 60);
return got;
}
string OtherTaskDue(ref int readAt, bool luck = false)
{
bool unsure = !energyRegen.HasValue || !staminaRegen.HasValue, read = false;
if (Environment.TickCount - readAt > (unsure ? 10000 : 30000))
{
readAt = Environment.TickCount;
var fresh = unsure && !luck ? ReadBarsSettled(4000, x => x.Energy >= 0 && x.Stamina >= 0) : ReadBarsSettled(0, x => true);
NoteTicks(fresh);
NoteHealth(fresh.Health, fresh.HealthMax, DateTime.UtcNow);
read = true;
if (last == null) last = fresh;
else
{
if (fresh.Level > 0) { last.Level = fresh.Level; last.Xp = fresh.Xp; last.XpNext = fresh.XpNext; }
if (fresh.SkillPoints >= 0) last.SkillPoints = fresh.SkillPoints;
if (fresh.Health >= 0) { last.Health = fresh.Health; last.HealthMax = fresh.HealthMax; }
if (fresh.Cash >= 0) last.Cash = fresh.Cash;
if (fresh.Banked >= 0) last.Banked = fresh.Banked;
}
using (var f = game.Capture()) NoteMenuNumbers(f);
}
if (last == null) return null;
var now = DateTime.UtcNow;
int e = energyRegen.Guess(now), st = staminaRegen.Guess(now);
if (e >= 0) { last.Energy = e; last.EnergyMax = energyRegen.Max; }
if (st >= 0) { last.Stamina = st; last.StaminaMax = staminaRegen.Max; }
if (read) NoteFullBars(new Header { Energy = e, EnergyMax = energyRegen.Max, Stamina = st, StaminaMax = staminaRegen.Max });
if (luck && !GlobalBossCuts("rerolls")) return null;
if (rerollCash >= 0) last.Cash = rerollCash;
var due = WhatIsDue(last, false);
return due.Count > 0 ? string.Join(", ", due) : null;
}
int SortCrew(int maxMoves, out bool done)
{
done = false;
sortWasStuck = false;
int moves = 0, noArrow = 0;
var movedUp = new List<string>();
bool autoTried = ByOwnStats || DateTime.UtcNow < autoSortOffUntil, restart = false, autoPressed = false;
try
{
for (int pass = 0; pass < 6 && moves < maxMoves; pass++)
{
int before = moves;
string lastSig = null;
Game.CrewRow lastMover = null;
bool top = false;
restart = false;
game.CrewListEnd(-1);
for (int look = 0; look < 120; look++)
{
Game.CrewPage p;
using (var f = game.Capture()) p = game.ReadCrew(f);
if (!p.Page) return moves;
var inSight = p.Rows.Where(r => !r.Empty && !r.Locked && !r.Cut && r.Rarity >= 0).ToList();
int missing;
var mover = Game.NextToMoveUp(inSight, out missing, ByOwnStats ? ownStats : null, SortSkipped);
noArrow += missing;
if (mover != null && !autoTried)
{
autoTried = true;
string why;
if (game.AutoSortCrew(out why)) { Log("Crew: AUTO SORT CREW pressed (the higher rarities on top)"); restart = autoPressed = true; break; }
if (firstSeen.Add("crew auto sort " + why)) Log("Crew: AUTO SORT CREW not pressed (" + why + ") - sorting with the arrows");
}
if (mover != null)
{
if (moves >= maxMoves) return moves;
if (lastMover != null && Game.SameHenchman(lastMover, mover) && Math.Abs(lastMover.Anchor.CenterY - mover.Anchor.CenterY) < 4)
{
sortStuck.Add(Tuple.Create(mover.Name, mover.Rarity, DateTime.UtcNow.AddHours(1)));
sortWasStuck = true;
Log("Crew: an up arrow didn't move " + mover + " - it stays where it is for an hour, the sort goes on without it");
SnapshotOnce("crew arrow no move");
lastMover = null;
continue;
}
game.CrewMoveUp(mover);
string who = (mover.Name.Length > 0 ? mover.Name : "a henchman") + " (" + mover.RarityName + ")";
if (!movedUp.Contains(who)) movedUp.Add(who);
lastMover = mover;
moves++;
continue;
}
string sig = string.Join(",", p.Rows.Select(r => r.Empty ? "empty" : r.Locked ? "locked" : r.Key));
if (sig == lastSig) { top = true; break; }
lastSig = sig;
game.ScrollPage(0.62, game.ListNotches(3));
}
if (restart) continue;
if (!top) break;
if (moves == before) { done = true; break; }
}
}
finally
{
if (autoPressed && moves > 0)
{
autoSortOffUntil = DateTime.UtcNow.AddHours(24);
Log("Crew: AUTO SORT CREW left the rarities out of order - only the arrows sort for the next 24 hours");
}
if (moves > 0)
Log("Crew: sorted by rarity" + (ByOwnStats && ownStats.Count > 0 ? " and own stats" : "") + ", " + moves + " move" + (moves == 1 ? "" : "s") + " up: " + string.Join(", ", movedUp.Take(5)) + (movedUp.Count > 5 ? " and " + (movedUp.Count - 5) + " more" : ""));
}
if (noArrow > 0 && moves == 0 && firstSeen.Add("crew arrows")) { Log("Crew: couldn't find the up arrows to sort the crew"); SnapshotOnce("crew arrows not found"); }
return moves;
}
bool ByOwnStats { get { return S.CrewReroll && S.CrewRerollStats > 0; } }
readonly List<Tuple<string, int, DateTime>> sortStuck = new List<Tuple<string, int, DateTime>>();
DateTime autoSortOffUntil;
bool sortWasStuck;
int sortStuckRounds;
bool SortSkipped(Game.CrewRow r)
{
var now = DateTime.UtcNow;
sortStuck.RemoveAll(x => x.Item3 <= now);
return sortStuck.Any(x => x.Item2 == r.Rarity && Game.WholeNameSame(x.Item1, r.Name));
}
DateTime nextBriefcases, nextEvents;
string badgeSig, eventsSig;
const int UnreadEventSeconds = 10 * 60;
readonly Dictionary<GameEvent, DateTime> eventsOn = new Dictionary<GameEvent, DateTime>();
string briefcasesSaid;
void NoteBadges(Frame f)
{
string sig = Game.BadgeSignature(Game.FindBadges(f));
noBadges = sig.Length == 0 ? noBadges + 1 : 0;
if (sig.Length == 0 && noBadges < 3) return;
badgeSig = sig;
if (badgeSig.Length == 0 && eventsOn.Count > 0) { eventsOn.Clear(); eventsSig = ""; Log("Events: none running now"); }
}
int noBadges;
bool EventOn(GameEvent e)
{
DateTime until;
return eventsOn.TryGetValue(e, out until) && DateTime.UtcNow < until;
}
void BadgesInRerolls()
{
List<Game.EventBadge> badges;
using (var f = game.Capture()) badges = Game.FindBadges(f);
if (badges.Count == 0)
{
game.Wait(1000);
using (var f = game.Capture()) badges = Game.FindBadges(f);
}
string sig = Game.BadgeSignature(badges);
if (sig == eventsSig) return;
badgeSig = sig;
eventsOn.Remove(GameEvent.RecruitLuck);
Log(Game.LuckBadge(badges) ? "Events: the badges changed - read again before more rerolls"
: "Events: recruitment luck's badge is gone - the other tasks go first again");
}
bool BossDamageHold()
{
DateTime until;
if (!S.Events || !S.Bosses || !Can(Tab.Bosses) || !eventsOn.TryGetValue(GameEvent.BossDamage, out until) || DateTime.UtcNow >= until) return false;
return bossesUp || bossBackAt < until || bossesReadAt == DateTime.MinValue;
}
bool CratesHeld(DateTime now)
{
if (!S.Events || EventOn(GameEvent.CrateLuck)) return false;
DateTime at;
if (!DateTime.TryParse(S.CratesOpenedAt, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out at)) return false;
return now < at.AddHours(24);
}
void DoEvents()
{
var now = DateTime.UtcNow;
nextEvents = now.AddMinutes(30);
List<Game.EventBadge> badges;
using (var f = game.Capture()) badges = Game.FindBadges(f);
string sig = Game.BadgeSignature(badges);
badgeSig = eventsSig = sig;
if (badges.Count == 0) { if (eventsOn.Count > 0) { eventsOn.Clear(); Log("Events: none running now"); } return; }
var read = game.ReadEvents(badges);
var before = new HashSet<GameEvent>(eventsOn.Where(kv => kv.Value > now).Select(kv => kv.Key));
eventsOn.Clear();
foreach (var e in read.Where(e => e.Kind != GameEvent.Other))
{
var until = now.AddSeconds(e.SecondsLeft > 0 ? e.SecondsLeft : UnreadEventSeconds);
DateTime had;
if (!eventsOn.TryGetValue(e.Kind, out had) || had < until) eventsOn[e.Kind] = until;
}
int soonest = read.Select(e => e.SecondsLeft > 0 ? e.SecondsLeft : UnreadEventSeconds).DefaultIfEmpty(30 * 60).Min();
nextEvents = now.AddSeconds(Math.Min(30 * 60, soonest + 5));
if (!before.SetEquals(eventsOn.Keys))
Log("Events: " + (read.Count > 0 ? string.Join(", ", read.Select(e => e + EventPlan(e.Kind))) : badges.Count + " badges, their words didn't read"));
if (eventsOn.ContainsKey(GameEvent.CrateLuck) && !before.Contains(GameEvent.CrateLuck)) nextCrates = now;
if (eventsOn.ContainsKey(GameEvent.BossDamage) && !before.Contains(GameEvent.BossDamage) && BossesDoneUntil(now) <= now) nextBosses = now;
if (eventsOn.ContainsKey(GameEvent.RecruitLuck) && !before.Contains(GameEvent.RecruitLuck)) nextReroll = now;
if (eventsOn.ContainsKey(GameEvent.OpsSpeed) && !before.Contains(GameEvent.OpsSpeed)) nextOps = now;
if (read.Count < badges.Count) SnapshotOnce("event badge words not read");
if (read.Any(e => e.Kind == GameEvent.Other && !e.Title.StartsWith("TRICK", StringComparison.OrdinalIgnoreCase))) SnapshotOnce("event title not known");
}
DateTime playtimeClaimed = DateTime.MinValue;
bool expHeld;
DateTime opsReadyAt = DateTime.MinValue;
bool opsKnown;
bool opsWaiting;
bool bossesNow;
readonly Dictionary<string, string> expSaid = new Dictionary<string, string>();
int expMissing;
int expClaimFails;
int expUnread;
DateTime expClaimAgain;
const int MostWantedFrom = 33;
const int ExpMinutes = 30;
static string UtcDay(DateTime utc) { return utc.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); }
static string UtcText(DateTime utc) { return utc.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture); }
static DateTime UtcParse(string s)
{
DateTime t;
return DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out t) ? t : DateTime.MinValue;
}
void ExpSay(string key, string line)
{
string was;
if (expSaid.TryGetValue(key, out was) && was == line) return;
expSaid[key] = line;
Log(line);
}
string ExpWhen(DateTime now)
{
DateTime at;
var route = ExpPlan(now, out at);
if (route == ExpRoute.Operations && at > now) return " (the operations finish at " + at.ToLocalTime().ToString("HH:mm") + ", the 2X then)";
return " (the 2X is claimed once health is almost full)";
}
void ResetExpState()
{
expHeld = opsKnown = opsWaiting = bossesNow = false;
opsReadyAt = DateTime.MinValue;
expSaid.Clear();
expMissing = expClaimFails = expUnread = 0;
expClaimAgain = DateTime.MinValue;
healthSeen = healthSeenMax = healthBase = -1;
healthSpentAt = DateTime.MinValue;
}
void ExpNewDay(DateTime now)
{
if (S.ExpDay == UtcDay(now)) return;
bool had = S.ExpDay.Length > 0;
S.ExpDay = UtcDay(now);
S.ExpCardAt = "";
S.ExpUsed = S.ExpMostWantedDone = false;
expHeld = opsWaiting = false;
expSaid.Clear();
expMissing = expClaimFails = expUnread = 0;
if (had) nextPlaytime = now;
SaveSettings();
}
bool ExpPending(DateTime now)
{
ExpNewDay(now);
if (!S.Playtime || !Can(Tab.Safehouse) || S.ExpUsed || S.ExpCardAt == "none") return false;
var at = UtcParse(S.ExpCardAt);
return at == DateTime.MinValue || at < now.Date.AddDays(1).AddMinutes(-10);
}
bool ExpRunning(DateTime now) { return now < UtcParse(S.ExpUntil); }
bool ExpCardReady(DateTime now)
{
var at = UtcParse(S.ExpCardAt);
return expHeld || (at != DateTime.MinValue && now >= at);
}
enum ExpRoute { Unknown, Operations, Bosses }
ExpRoute ExpPlan(DateTime now, out DateTime at)
{
at = DateTime.MinValue;
var card = expHeld ? now : UtcParse(S.ExpCardAt);
if (card == DateTime.MinValue) return ExpRoute.Unknown;
if (opsWaiting) { at = now; return ExpRoute.Operations; }
bool ops = S.Operations && Can(Tab.Operations);
if (ops && !opsKnown) return ExpRoute.Unknown;
if (ops && opsReadyAt != DateTime.MinValue && opsReadyAt < now.Date.AddDays(1))
{
if (opsReadyAt < card) return ExpRoute.Unknown;
at = opsReadyAt;
return ExpRoute.Operations;
}
at = card < now ? now : card;
return ExpRoute.Bosses;
}
int healthSeen = -1, healthSeenMax = -1, healthBase = -1;
DateTime healthSeenAt, healthBaseAt;
double HealthRate { get { return (S.HealthPerHour > 0 ? S.HealthPerHour : 360) / 3600.0; } }
int HealthMaxKnown { get { return healthSeenMax > 0 ? healthSeenMax : game != null ? game.BarMax(2) : -1; } }
void NoteHealth(int v, int max, DateTime now)
{
if (v < 0 || max <= 0 || v > max) return;
healthSeen = v; healthSeenMax = max; healthSeenAt = now;
if (v >= max || healthBase < 0 || v < healthBase) { healthBase = v >= max ? -1 : v; healthBaseAt = now; return; }
double s = (now - healthBaseAt).TotalSeconds;
if (s < 300 || v - healthBase < 10) return;
int perHour = (int)Math.Round((v - healthBase) * 3600 / s);
if (perHour <= 0 || Math.Abs(perHour - S.HealthPerHour) * 10 <= S.HealthPerHour) return;
S.HealthPerHour = perHour;
SaveSettings();
Log(string.Format(CultureInfo.InvariantCulture, "Health: it refills about {0:0.#} a minute (the bar full from empty in {1})", perHour / 60.0, SpanText((int)(max * 3600.0 / perHour))));
}
DateTime healthSpentAt = DateTime.MinValue;
void HealthSpent() { healthBase = -1; healthSpentAt = DateTime.UtcNow; }
int HealthNow(DateTime now)
{
if (healthSeen >= 0 && healthSeenAt >= healthSpentAt)
return (int)Math.Min(healthSeenMax, healthSeen + HealthRate * Math.Max(0, (now - healthSeenAt).TotalSeconds));
int max = HealthMaxKnown;
if (healthSpentAt == DateTime.MinValue || max <= 0) return -1;
return (int)Math.Min(max, HealthRate * Math.Max(0, (now - healthSpentAt).TotalSeconds));
}
bool ExpBarsReady(DateTime now, Header h, out string why)
{
why = null;
if (!ExpForBosses) return true;
int hp = HealthNow(now), max = HealthMaxKnown;
if (hp >= 0 && max > 0 && hp < max - HealthRate * 60) { why = "health " + hp + "/" + max; return false; }
if (h != null && h.Stamina >= 0 && h.StaminaMax > 0)
{
int cost = S.BossHitHealth > 0 ? S.BossHitHealth : 10;
int need = hp >= 0 && max > 0 ? Math.Min(h.StaminaMax - 10, (hp - max * LowHealthPct / 100) / cost) : h.StaminaMax - 10;
if (h.Stamina < need) { why = "stamina " + h.Stamina + "/" + h.StaminaMax; return false; }
}
return true;
}
bool ExpForBosses { get { return S.Bosses && Can(Tab.Bosses); } }
static bool ExpLastCall(DateTime now) { return now >= now.Date.AddDays(1).AddMinutes(-10); }
DateTime ExpSaveFrom(DateTime now)
{
if (!ExpPending(now) || !ExpForBosses) return DateTime.MaxValue;
DateTime at;
var route = ExpPlan(now, out at);
if (route == ExpRoute.Unknown) return DateTime.MaxValue;
int max = HealthMaxKnown;
double fill = (max > 0 ? max : 500) / HealthRate + 300;
if (route == ExpRoute.Bosses) { var late = now.Date.AddDays(1).AddMinutes(-10).AddSeconds(-fill); return at < late ? at : late; }
return at.AddSeconds(-fill);
}
bool ExpHoldsHealth(DateTime now)
{
if (ExpRunning(now)) return S.Bosses && Can(Tab.Bosses) && !crewUnequipped && BossesDoneUntil(now) <= now;
return now >= ExpSaveFrom(now);
}
bool ExpKeepsStamina(DateTime now) { return ExpRunning(now) || now >= ExpSaveFrom(now); }
int ExpHealthFloor(DateTime now)
{
if (!ExpPending(now)) return 0;
DateTime at;
var route = ExpPlan(now, out at);
int max = HealthMaxKnown;
if (route == ExpRoute.Unknown || max <= 0) return 0;
if (route == ExpRoute.Bosses && now >= at && S.ExpMostWantedDone) return max;
var by = route == ExpRoute.Bosses ? now.Date.AddDays(1).AddMinutes(-10) : at;
double floor = max - HealthRate * ((by - now).TotalSeconds - 60);
return floor <= 0 ? 0 : (int)Math.Min(max, floor);
}
bool ExpClaimDue(DateTime now, Header h)
{
if (!ExpPending(now) || !ExpCardReady(now)) return false;
DateTime at;
if (ExpPlan(now, out at) != ExpRoute.Bosses || now < at) return false;
bool mw = S.Bosses && Can(Tab.Bosses) && !crewUnequipped && BossesDoneUntil(now) <= now;
if (mw && !S.ExpMostWantedDone && !ExpLastCall(now)) return false;
string why;
if (ExpBarsReady(now, h, out why) || ExpLastCall(now)) return true;
string claimWait = "2X EXP: waiting to claim it until " + (why.StartsWith("health") ? "health is almost full" : "there's the stamina to spend it");
string claimWas;
if (!expSaid.TryGetValue("claim", out claimWas) || !claimWas.StartsWith(claimWait)) ExpSay("claim", claimWait + " (" + why.Split(' ')[0] + " now " + why.Split(' ')[1] + ")");
return false;
}
void NoteExpCard(DateTime now)
{
ExpNewDay(now);
int s = game.PlaytimeExpSeconds;
string card = S.ExpCardAt;
bool used = S.ExpUsed;
expMissing = s == -3 && game.PlaytimeNextSeconds != -1 ? expMissing + 1 : 0;
expUnread = s == -1 || s == -3 ? expUnread + 1 : 0;
if (s == -2) S.ExpUsed = true;
else if (s == 0) { var was = UtcParse(S.ExpCardAt); if (was == DateTime.MinValue || was > now) S.ExpCardAt = UtcText(now); }
else if (s > 0) S.ExpCardAt = UtcText(now.AddSeconds(s));
else if (expMissing >= 2 || (s == -3 && game.PlaytimeNextSeconds == -2)) S.ExpCardAt = "none";
else if (expUnread >= 3 && UtcParse(S.ExpCardAt) == DateTime.MinValue && !expHeld) S.ExpCardAt = "none";
if (card == S.ExpCardAt && used == S.ExpUsed) return;
SaveSettings();
if (S.ExpUsed && !used) { if (!ExpRunning(now)) Log("Playtime rewards: today's 2X EXP is claimed already - the bosses and fights as usual"); return; }
if (S.ExpCardAt == "none")
{
Log(expMissing >= 2 || s == -3 ? "Playtime rewards: no 2X EXP among today's rewards - the bosses and fights as usual"
: "Playtime rewards: couldn't read when the 2X EXP can be claimed (3 looks) - the bosses and fights as usual for now");
return;
}
if (s > 0 && (card.Length == 0 || Math.Abs((UtcParse(card) - UtcParse(S.ExpCardAt)).TotalMinutes) > 5))
Log("Playtime rewards: the 2X EXP can be claimed at " + UtcParse(S.ExpCardAt).ToLocalTime().ToString("HH:mm") + " - until it's used the bosses only hit the Most Wanted");
}
bool ClaimHeldExp(Tab back, string why)
{
var now = DateTime.UtcNow;
if (!S.Playtime || !ExpCardReady(now)) return false;
int got = game.ClaimPlaytimeRewards(true, true);
expHeld = game.PlaytimeExpHeld;
if (got > 0)
{
expHeld = opsWaiting = false;
playtimeClaimed = now;
Count.Rewards += got;
S.ExpUsed = true;
S.ExpUntil = UtcText(now.AddMinutes(ExpMinutes));
SaveSettings();
nextJobs = now;
int hp = HealthNow(now);
Log("Playtime rewards: claimed the 2X EXP one for " + why + (hp >= 0 ? " (health " + hp + "/" + HealthMaxKnown + ")" : "")
+ " - for " + ExpMinutes + " minutes stages 1-" + (MostWantedFrom - 1) + " first, no player fights or Takedown while a boss is left");
}
else
{
NoteExpCard(now);
if (!S.ExpUsed && game.PlaytimeExpSeconds <= 0 && ++expClaimFails >= 3)
{
S.ExpUsed = true;
SaveSettings();
Log("Playtime rewards: couldn't claim the 2X EXP 3 times - the bosses and fights as usual today");
game.Snapshot("2x exp claim not found", 60);
}
}
game.OpenTab(back);
return got > 0;
}
void BossesAfter(List<string> due, int i)
{
if (!bossesNow) return;
bossesNow = false;
if (S.GlobalBoss && globalBossUp) { nextBosses = DateTime.UtcNow; return; }
if (!S.Bosses || crewUnequipped || !Can(Tab.Bosses) || BossesDoneUntil(DateTime.UtcNow) > DateTime.UtcNow) return;
nextBosses = DateTime.UtcNow;
int at = due.IndexOf("bosses");
if (at >= 0 && at <= i) return;
if (at > i) due.RemoveAt(at);
due.Insert(i + 1, "bosses");
}
string EventPlan(GameEvent e)
{
switch (e)
{
case GameEvent.RecruitLuck: return (S.CrewReroll ? ": the rerolls go first while it runs" : "")
+ (S.CrewLuckRoll != LuckRoll.Same ? (S.CrewReroll ? ", with " : ": ") + LuckRollFor(S.CrewRollType, S.CrewLuckRoll, S.Events) + " hires" : "");
case GameEvent.CrateLuck: return S.Crates ? ": the crates are opened now" : "";
case GameEvent.BossDamage: return S.Bosses ? ": stamina goes to the bosses, no gifts while one is up" : "";
case GameEvent.OpsSpeed: return S.Operations ? ": operations looked at twice as often" : "";
default: return "";
}
}
void DoBriefcases()
{
nextBriefcases = DateTime.UtcNow.AddMinutes(20);
if (!game.OpenBriefcases())
{
nextBriefcases = DateTime.UtcNow.AddHours(1);
briefcasesStopped = false;
if (briefcasesSaid != "off")
{
Log(briefcasesSaid == "on" ? "Lucky Briefcases: no longer in the game's menu - the event is over. The bot looks again every hour"
: "Lucky Briefcases: not in the game's menu (no event right now). The bot looks again every hour");
briefcasesSaid = "off";
}
return;
}
if (briefcasesSaid != "on") { briefcasesSaid = "on"; Log("Lucky Briefcases: the event is on - the bot picks a briefcase with each free pick (one every 20 minutes online), never with keys"); }
try { PickBriefcases(); }
finally { game.FoldEvents(); }
}
void PickBriefcases()
{
if (briefcasesStopped) { nextBriefcases = DateTime.UtcNow.AddHours(6); return; }
for (int pick = 0; pick < 6; pick++)
{
var page = ReadBriefcasesSteady();
if (page == null || !page.Page)
{
if (page == null) SayBriefcases("the free picks didn't read the same twice - not picking now, looked at again in 10 minutes");
nextBriefcases = DateTime.UtcNow.AddMinutes(10);
break;
}
if (page.ButtonFree <= 0)
{
nextBriefcases = DateTime.UtcNow.AddSeconds(page.NextSeconds > 0 ? Math.Min(page.NextSeconds + 15, 3600) : 20 * 60);
if (page.ButtonFree < 0) SnapshotOnce("briefcase pick button unread");
SayBriefcases(page.ButtonFree < 0 ? "the pick button didn't read - looked at again in 20 minutes"
: "no free pick right now - the next one in " + (page.NextSeconds > 0 ? Parse.Short(page.NextSeconds) : "? (its time didn't read; looked at again in 20 minutes)"));
break;
}
if (page.Cases.Count == 0)
{
Log("Lucky Briefcases: a free pick, but no closed briefcase in sight - looked at again in an hour");
SnapshotOnce("briefcases none closed");
nextBriefcases = DateTime.UtcNow.AddHours(1);
break;
}
Game.BriefcasePage after;
string paid = game.PickBriefcase(page, out after);
if (page.Keys >= 0 && after != null && after.Page && after.Keys >= 0 && after.Keys < page.Keys)
{
briefcasesStopped = true;
Log("Lucky Briefcases: the keys went down (" + page.Keys + " -> " + after.Keys + ") after a pick - no more picks while this event runs");
game.Snapshot("briefcase keys went down");
nextBriefcases = DateTime.UtcNow.AddHours(6);
return;
}
if (paid == null) { nextBriefcases = DateTime.UtcNow.AddMinutes(10); break; }
Count.Rewards++;
Log("Lucky Briefcases: a free pick paid " + (paid.Length > 0 ? CultureInfo.InvariantCulture.TextInfo.ToTitleCase(paid.ToLowerInvariant()) : "something (its words didn't read)"));
}
}
bool briefcasesStopped;
DateTime briefcasesSaidAt = DateTime.MinValue;
void SayBriefcases(string what)
{
if (DateTime.UtcNow < briefcasesSaidAt) return;
briefcasesSaidAt = DateTime.UtcNow.AddHours(1);
Log("Lucky Briefcases: " + what);
}
Game.BriefcasePage ReadBriefcasesSteady()
{
Game.BriefcasePage a, b;
using (var f = game.Capture()) a = game.ReadBriefcases(f);
if (!a.Page) return a;
game.Wait(1000);
using (var f = game.Capture()) b = game.ReadBriefcases(f);
if (!b.Page) return b;
return a.ButtonFree == b.ButtonFree ? b : null;
}
DateTime nextGlobalBoss;
string globalBossSaid;
DateTime globalBossPinged = DateTime.MinValue;
bool globalBossUp;
bool globalBossWaits;
int globalBossFails;
int globalBossHits;
DateTime globalBossSaidHits = DateTime.MinValue;
int globalBossClaimFails;
DateTime globalBossClaimAfter = DateTime.MinValue;
const int GlobalBossMinutes = 30;
void ResetGlobalBoss() { globalBossSaid = null; globalBossUp = false; globalBossFails = globalBossHits = 0; }
bool GlobalBossHoldsExp(DateTime now)
{
if (!S.GlobalBoss || ExpLastCall(now)) return false;
return globalBossUp;
}
bool GlobalBossCuts(string task)
{
var now = DateTime.UtcNow;
if (!S.GlobalBoss || !globalBossUp || task == "global boss" || ExpLastCall(now)) return false;
return now >= nextGlobalBoss || task == "fights" || task == "bosses" || task == "takedown" || task == "give stamina";
}
void DoGlobalBoss()
{
var now = DateTime.UtcNow;
nextGlobalBoss = now.AddMinutes(GlobalBossMinutes);
if (!game.OpenGlobalBoss())
{
if (globalBossUp) { GlobalBossFailed("its page didn't open"); return; }
if (globalBossSaid != "off")
{
Log("Global Boss: not in the game's menu right now - the bot looks every " + GlobalBossMinutes + " minutes");
globalBossSaid = "off";
}
return;
}
try { ReadGlobalBoss(now); }
finally { if (!globalBossUp || ExpLastCall(DateTime.UtcNow)) game.FoldEvents(); }
}
void GlobalBossFailed(string why)
{
globalBossFails++;
if (globalBossFails < 3) { nextGlobalBoss = DateTime.UtcNow.AddMinutes(1); return; }
Log("Global Boss: " + why + " three times in a row - the rest plays again, the boss is looked at again in " + GlobalBossMinutes + " minutes");
game.Snapshot("global boss " + why, 60);
globalBossUp = false;
globalBossFails = 0;
nextGlobalBoss = DateTime.UtcNow.AddMinutes(GlobalBossMinutes);
}
void ReadGlobalBoss(DateTime now)
{
Game.GlobalBossPage a, b;
using (var f = game.Capture()) a = game.ReadGlobalBoss(f);
game.Wait(1000);
using (var f = game.Capture()) b = game.ReadGlobalBoss(f);
if (!a.Page || !b.Page || a.State != b.State)
{
if (globalBossUp) GlobalBossFailed("its page didn't read");
return;
}
string who = b.Boss != null ? CultureInfo.InvariantCulture.TextInfo.ToTitleCase(b.Boss.ToLowerInvariant()) : null;
if (b.State != Game.BossState.Live && globalBossUp)
{
globalBossUp = false;
Log("Global Boss: it's over" + (globalBossHits > 0 ? " - " + globalBossHits + " hits on it" : "") + " - the rest plays again");
}
if (b.State != Game.BossState.Live) globalBossHits = 0;
switch (b.State)
{
case Game.BossState.Settled:
if (globalBossSaid != "settled")
Log("Global Boss: " + (who != null ? who + " is" : "the last one was") + " beaten"
+ (b.Seconds > 0 ? " (its page closes in " + Parse.Short(b.Seconds) + ")" : "") + " - the bot looks for the next one every " + GlobalBossMinutes + " minutes");
globalBossSaid = "settled";
if (b.Claim != null) ClaimGlobalBoss(b, now);
break;
case Game.BossState.Upcoming:
if (b.Seconds > 0) nextGlobalBoss = now.AddSeconds(Math.Min(b.Seconds + 20, 6 * 3600));
if (globalBossSaid != "upcoming")
Log("Global Boss: the next one starts in " + (b.Seconds > 0 ? Parse.Short(b.Seconds) + " - the bot looks again then" : "? (its time didn't read; looked at every " + GlobalBossMinutes + " minutes)"));
globalBossSaid = "upcoming";
SnapshotOnce("global boss upcoming");
break;
case Game.BossState.Live:
if (!globalBossUp)
{
globalBossUp = true;
globalBossWaits = false;
globalBossHits = 0;
globalBossSaidHits = now;
Log("Global Boss: " + (who != null ? who : "a boss") + " is up! (\"" + b.Status + "\") - it comes first, every stamina point goes on it"
+ (ExpPending(now) ? "; the 2X EXP after it" : ""));
SnapshotOnce("global boss live");
}
globalBossSaid = "live";
if (now >= globalBossPinged.AddHours(12))
{
globalBossPinged = now;
Discord.Post(Ping.GlobalBoss, "\U0001F480 Global Boss is up", (who != null ? who + " is up" : "A Global Boss is up") + " in Idle Mafia. The bot is hitting it with all its stamina.",
Shot, Discord.Red, true);
}
HitGlobalBoss(b);
break;
}
}
void ClaimGlobalBoss(Game.GlobalBossPage p, DateTime now)
{
if (now < globalBossClaimAfter) return;
SnapshotOnce("global boss claim drops");
var after = game.ClaimGlobalBossDrops(p);
if (after == null)
{
if (++globalBossClaimFails < 3)
{
Log("Global Boss: pressed CLAIM DROPS, the button stayed - tried again at the next look");
return;
}
Log("Global Boss: CLAIM DROPS didn't take three times - left alone for 12 hours");
game.Snapshot("global boss claim didn't take", 60);
globalBossClaimFails = 0;
globalBossClaimAfter = now.AddHours(12);
return;
}
globalBossClaimFails = 0;
Log("Global Boss: claimed the drops - " + (after.Received.Count > 0 ? string.Join(", ", after.Received) : "what it gave didn't read"));
SnapshotOnce("global boss drops claimed");
}
void HitGlobalBoss(Game.GlobalBossPage p)
{
int hits;
string stop = GlobalBossHits(p, out hits);
globalBossHits += hits;
if (hits > 0) Count.BossHits += hits;
var now = DateTime.UtcNow;
if (hits > 0 || stop == "stamina" || stop == "health") globalBossFails = 0;
if (hits > 0 && now >= globalBossSaidHits.AddMinutes(15))
{
globalBossSaidHits = now;
Log("Global Boss: " + globalBossHits + " hits so far");
}
if ((stop == "stamina" || stop == "health") && S.GlobalBossConsumables && UseConsumable(stop == "stamina")) { nextGlobalBoss = DateTime.UtcNow; return; }
globalBossWaits = stop == "stamina" || stop == "health";
switch (stop)
{
case "stamina":
SayOnce("global boss stamina " + now.ToString("yyyyMMddHH"), "Global Boss: no stamina left - the rest plays until a point has refilled");
nextGlobalBoss = now.AddSeconds(20);
break;
case "health":
SayOnce("global boss health " + now.ToString("yyyyMMddHH"), "Global Boss: health is low - the rest plays until it has refilled");
nextGlobalBoss = now.AddMinutes(2);
break;
case "over": nextGlobalBoss = now; break;
case null: nextGlobalBoss = now; break;
case "bars": GlobalBossFailed("stamina didn't read"); break;
case "no attack": GlobalBossFailed("no ATTACK on its page"); break;
case "nothing": GlobalBossFailed("hits took no stamina"); break;
default: GlobalBossFailed(stop); break;
}
}
bool FightItemsReady(DateTime now) { return S.FightItems && now >= consumablesNoneUntil0; }
bool HealWithItem() { return S.FightItems && UseConsumable(false, "the fights"); }
DateTime consumablesNoneUntil0 = DateTime.MinValue, consumablesNoneUntil1 = DateTime.MinValue;
bool UseConsumable(bool stamina, string forWhat = "the Global Boss")
{
var now = DateTime.UtcNow;
if (now < (stamina ? consumablesNoneUntil0 : consumablesNoneUntil1)) return false;
string what = stamina ? "stamina" : "health";
Header before;
using (var f = game.Capture()) before = game.ReadHeader(f);
if (!stamina && forWhat == "the fights")
{
if (before.Health < 0) before = ReadBarsSettled(3000, x => x.Health >= 0);
if (before.Health >= 0 && before.HealthAtLeast(50))
{
SayOnce("consumables health fine " + now.ToString("yyyyMMddHH"), "Consumables: health is " + before.Health + " / " + before.HealthMax + " - no health item needed, the fights go on");
return true;
}
}
bool none;
string used;
try { used = game.UseConsumable(stamina, out none); }
finally { game.FoldEvents(); }
if (used == null)
{
if (stamina) consumablesNoneUntil0 = now.AddMinutes(30); else consumablesNoneUntil1 = now.AddMinutes(30);
SayOnce("consumables none " + what + " " + now.ToString("yyyyMMddHH"), "Consumables: " + (none ? "no " + what + " item left in the inventory" : "couldn't use a " + what + " item")
+ " - " + forWhat + " wait for the bar; looked for again in 30 minutes");
return false;
}
SnapshotOnce("consumable used");
var after = ReadBarsSettled(5000, x => stamina ? x.Stamina >= 0 : x.Health >= 0);
int b = stamina ? before.Stamina : before.Health, a = stamina ? after.Stamina : after.Health;
int max = stamina ? Math.Max(before.StaminaMax, after.StaminaMax) : Math.Max(before.HealthMax, after.HealthMax);
bool worked = Game.ItemWorked(b, a, max);
Log("Consumables: " + (worked ? "used a " : "pressed USE on a ") + used + " for " + forWhat + " (" + what + " " + (b >= 0 ? b.ToString() : "?") + " -> " + (a >= 0 ? a.ToString() : "?") + ")"
+ (worked ? "" : " - the bar didn't rise, so it wasn't used"));
if (!worked)
{
game.Snapshot("consumable used no change", 60);
if (stamina) consumablesNoneUntil0 = now.AddMinutes(30); else consumablesNoneUntil1 = now.AddMinutes(30);
return false;
}
return true;
}
string GlobalBossHits(Game.GlobalBossPage p, out int hits)
{
hits = 0;
var h = ReadBarsSettled(8000);
if (h.Stamina < 0) return "bars";
int stamina = h.Stamina, checkAt = h.Stamina, presses = 0, misses = 0, looks = 0;
for (int go = 0; go < 400; go++)
{
if (p.YourHealthMax > 0 && p.YourHealth >= 0 ? p.YourHealth * 100 < LowHealthPct * p.YourHealthMax
: h.Health >= 0 && !h.HealthAtLeast(LowHealthPct)) { hits += presses; return "health"; }
if (stamina <= 0) { hits += presses; return "stamina"; }
if (p.Attack == null)
{
if (looks++ >= 3) { hits += presses; return "no attack"; }
game.Wait(700);
using (var f = game.Capture()) p = game.ReadGlobalBoss(f);
if (!p.Page || p.State != Game.BossState.Live) { hits += presses; return "over"; }
continue;
}
looks = 0;
game.GlobalBossAttack(p);
presses++; stamina--;
game.Wait(500);
bool refused = false;
using (var f = game.Capture())
{
p = game.ReadGlobalBoss(f);
refused = p.YourHealthMax > 0 && p.YourHealth >= 0 && p.YourHealth * 2 < p.YourHealthMax && Game.TopRightYouHospitalized(f);
}
if (refused) { hits += presses - 1; return "health"; }
if (!p.Page || p.State != Game.BossState.Live)
{
game.Wait(800);
using (var f = game.Capture()) p = game.ReadGlobalBoss(f);
if (!p.Page || p.State != Game.BossState.Live) { hits += presses; return "over"; }
}
if (presses >= 10 || stamina <= 0)
{
h = ReadBarsSettled(5000);
if (h.Stamina < 0) { hits += presses; return "bars"; }
int spent = checkAt - h.Stamina;
if (spent <= 0)
{
if (++misses >= 2) { game.Snapshot("global boss hits take no stamina", 60); return "nothing"; }
}
else { misses = 0; hits += Math.Min(presses, spent); }
stamina = checkAt = h.Stamina;
presses = 0;
}
}
hits += presses;
return null;
}
DateTime nextEquip, nextShop, nextSafehouse;
bool saidBestGear, saidDeposit;
public event Action<SafehouseInfo> SafehouseRead;
public PlayerStats LastGameStats;
public event Action<PlayerStats> GameStatsRead;
void DoGear()
{
var now = DateTime.UtcNow;
bool gearDue = now >= nextEquip;
nextEquip = now.AddHours(6);
int bought = 0, opened = 0;
if (ShopOn() && now >= nextShop && Can(Tab.Shop)) bought = BuyFromShop();
if (S.Crates && now >= nextCrates && Can(Tab.Inventory) && !CratesHeld(now))
{
nextCrates = now.AddMinutes(20);
bool done;
game.CratesLeftText = S.CratesLeft;
opened = Report(game.OpenCrates(out done), "Opened {0} crate(s)" + (S.Events && EventOn(GameEvent.CrateLuck) ? " during crate luck" : ""));
if (game.CratesLeftText != S.CratesLeft) { S.CratesLeft = game.CratesLeftText; SaveSettings(); }
Count.Crates += opened;
if (done) S.CratesOpenedAt = now.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture);
}
if (bought > 0 || opened > 0 || gearDue) EquipThenDeposit();
}
string shopGoldSaid;
string shopRaritySaid;
string shopLimitSaid;
string shopKeepSaid;
bool ShopOn() { return S.ShopBuy; }
double ShopKeep(bool all)
{
if (TutorialOn) return 0;
double keep = PropertyKeep();
if (keep <= 0 && S.SafehouseUpgrade && upgradePrice > 0 && Can(Tab.Safehouse)) keep = upgradePrice;
if (keep <= 0 || all) return Math.Max(0, keep);
double banked = CanWithdraw() && last != null && last.Banked > 0 ? last.Banked : 0;
return Math.Max(0, keep - banked);
}
string ShopKeepLine(double keep)
{
string keptFor = keep > 0 ? (PropertyKeep() > 0 ? "the next property" : "the next safehouse upgrade") : null;
string say = keptFor == null ? null : keptFor + " " + M(ShopKeep(true));
string line = say != null && say != shopKeepSaid ? "Shop: keeps " + M(keep) + " of the cash on hand for " + keptFor : null;
shopKeepSaid = say;
return line;
}
int BuyFromShop()
{
int stock;
double keepAll = ShopKeep(true), keep = KeepFloor(keepAll, true);
string keepLine = ShopKeepLine(keep);
if (keepLine != null) Log(keepLine);
var dear = new List<Game.ShopItem>();
var goldLeft = new HashSet<string>();
var overLimit = new HashSet<string>();
Func<Game.ShopItem, bool> want = i =>
{
if (i.Gold)
{
if (i.Price <= S.ShopGoldMax) return true;
goldLeft.Add(i.ToString());
return false;
}
if (TutorialOn && tutorialItem != null && i.Name != null && Parse.Key(i.Name) == Parse.Key(tutorialItem)) return true;
if (!S.ShopCashFor(i.Price)) { overLimit.Add(i.ToString()); return false; }
return true;
};
var bought = game.BuyFromShop(out stock, keep, TutorialOn ? tutorialItem : null, dear, 0, want, S.ShopGoldMax > 0 && !TutorialOn);
if (S.Events && EventOn(GameEvent.ShopSale)) SnapshotOnce("shop sale page");
nextShop = DateTime.UtcNow.AddSeconds(stock >= 0 ? Math.Max(120, stock + 5) : 15 * 60);
bool bankPays = S.ShopWithdraw && S.Bank && BankFee() == 0;
var unread = bankPays ? dear.Where(i => i.Rarity < 0 && i.Price > 0).ToList() : new List<Game.ShopItem>();
string unreadSay = unread.Count == 0 ? null : "Shop: left " + string.Join(", ", unread.Select(i => i.ToString())) + " for the cash on hand - its rarity didn't read, and the bank's cash only pays for the rarities picked";
if (unreadSay != null && unreadSay != shopRaritySaid) { Log(unreadSay); SnapshotOnce("shop rarity not read"); }
if (unreadSay != null) shopRaritySaid = unreadSay;
dear.RemoveAll(i => !S.ShopBankFor(i.Rarity));
if (S.ShopWithdraw && dear.Count > 0)
{
double cheapest = dear.Min(i => i.Price), floor, spent = -1;
bool took = WithdrawKeeping(cheapest, false, dear.Where(i => i.Price > 0).Sum(i => i.Price), keepAll, out floor);
try
{
int again;
if (took)
{
var more = game.BuyFromShop(out again, floor, null, null, cheapest * 0.99, i => want(i) && S.ShopBankFor(i.Rarity));
bought.AddRange(more);
spent = more.Where(i => !i.Gold && i.Price > 0).Sum(i => i.Price);
}
}
finally { if (took) DepositBack(spent); }
}
string limitSay = overLimit.Count == 0 ? null : "Shop: left in the shop (over your price limit): " + string.Join(", ", overLimit.OrderBy(x => x));
if (limitSay != null && limitSay != shopLimitSaid) Log(limitSay);
shopLimitSaid = limitSay;
string goldSay = goldLeft.Count == 0 || S.ShopGoldMax <= 0 ? null : "Shop: left in the shop (over your " + S.ShopGoldMax + " gold limit): " + string.Join(", ", goldLeft.OrderBy(x => x));
if (goldSay != null && goldSay != shopGoldSaid) Log(goldSay);
shopGoldSaid = goldSay;
if (bought.Count == 0) return 0;
Count.Bought += bought.Count;
Log("Shop: bought " + string.Join(", ", bought.Select(i => i.ToString())));
SnapshotOnce("shop bought");
var good = bought.Where(i => S.ShopPingFor(i.Rarity)).OrderByDescending(i => i.Rarity).ToList();
Func<int, string> rarity = r => Game.Rarities[r].Substring(0, 1) + Game.Rarities[r].Substring(1).ToLowerInvariant();
if (good.Count > 0)
Discord.Post(Ping.Shop, "\U0001F6CD️ Bought " + (good.Count == 1 ? "a " + rarity(good[0].Rarity) + " item" : good.Count + " good items") + " in the shop",
string.Join("\n", good.Select(i => rarity(i.Rarity) + ": " + i)), Shot,
Discord.RarityColors[Math.Min(good[0].Rarity, Discord.RarityColors.Length - 1)], true);
return bought.Count;
}
void EquipThenDeposit()
{
if (!S.EquipBest || !Can(Tab.Crew)) return;
if (!DoEquip() || !Can(Tab.Collection)) return;
if (S.MarketPending.Length > 0 && !Settings.MarketRelistShown)
{
Log("Black Market: " + S.MarketPending.Split('|')[0] + " was taken out of your shop to relist and is in your inventory - the bot no longer relists (the game asks a captcha now), so put it back up yourself");
S.MarketPending = "";
SaveSettings();
}
if (S.MarketPending.Length > 0) { Log("Collection: no deposit while " + S.MarketPending.Split('|')[0] + " waits to go back up on the Black Market"); return; }
DepositCollection();
}
bool DoEquip()
{
nextEquip = DateTime.UtcNow.AddHours(6);
rerollCrew = null;
var r = game.EquipBest();
if (r == null) return false;
var a = r.Before;
var b = r.After ?? new Game.CrewTotals();
if (r.Window != null) { Log("Crew: a window came up after AUTO EQUIP BEST - left alone: \"" + r.Window + "\""); return false; }
if (!r.GearOn)
{
Log("Crew: AUTO EQUIP BEST pressed " + r.Presses + " time" + (r.Presses == 1 ? "" : "s") + ", but nothing showed it took (" + a + " before, " + b + " after"
+ (r.Bare ? ", no gear on the crew" : "") + ") - no deposit into the collection this time");
game.Snapshot("auto equip best not confirmed", 60);
return false;
}
CrewUnequipped = false;
if (!a.Read || !b.Read) Log("Crew: equipped the best gear (" + a + " before, " + b + " after)");
else if (a.Attack != b.Attack || a.Defense != b.Defense) Log(string.Format("Crew: best gear on. Attack +{0} -> +{1}, defense +{2} -> +{3}", a.Attack, b.Attack, a.Defense, b.Defense));
else if (!saidBestGear) Log("Crew: already wearing the best gear (" + b + ")");
saidBestGear = true;
SnapshotOnce("crew auto equip best");
return true;
}
void DepositCollection()
{
var r = game.DepositCollection();
if (r == null) return;
var a = r.Before;
var b = r.After ?? new Game.CollectionState();
if (r.Window != null) Log("Collection: a window came up after DEPOSIT ALL - left alone: \"" + r.Window + "\"");
bool more = a.Deposited >= 0 && b.Deposited > a.Deposited, stronger = a.Attack >= 0 && b.Attack > a.Attack;
if (more || stronger)
{
if (more) Count.Collected += b.Deposited - a.Deposited;
Log("Collection: " + (more ? string.Format("{0} new item(s) deposited, {1} of {2}", b.Deposited - a.Deposited, b.Deposited, b.Total) : "new items deposited")
+ (stronger ? string.Format(". Attack +{0} -> +{1}", a.Attack, b.Attack) : ""));
}
else if (!saidDeposit) Log("Collection: DEPOSIT ALL pressed (" + b + ")" + (!string.IsNullOrEmpty(r.Toast) ? " - \"" + r.Toast + "\"" : ""));
saidDeposit = true;
SnapshotOnce("collection deposit");
}
void ReadSafehouse()
{
nextSafehouse = DateTime.UtcNow.AddHours(24);
var info = game.ReadSafehouse();
if (info == null || info.Found == 0)
{
if (info != null) { Log("Couldn't read the Safehouse page"); game.Snapshot("safehouse not read", 60); }
nextSafehouse = DateTime.UtcNow.AddHours(1);
return;
}
if (info.AttackPower > 0 && S.LastAttackPower > 0 && info.AttackPower * 3 < S.LastAttackPower) info.AttackPower = -1;
if (info.DefensePower > 0 && S.LastDefensePower > 0 && info.DefensePower * 3 < S.LastDefensePower) info.DefensePower = -1;
if ((info.AttackPower > 0 && info.AttackPower != S.LastAttackPower) || (info.DefensePower > 0 && info.DefensePower != S.LastDefensePower))
{
if (info.AttackPower > 0) S.LastAttackPower = info.AttackPower;
if (info.DefensePower > 0) S.LastDefensePower = info.DefensePower;
SaveSettings();
}
if (info.Prestige >= 0 && info.Prestige != S.LastPrestige)
{
S.LastPrestige = info.Prestige;
SaveSettings();
Log("Safehouse: prestige " + info.Prestige + " of 6");
}
foreach (var line in info.Lines()) Log(line);
if (info.Hired >= 0) { hiredRead = info.Hired; hiredReadAt = DateTime.UtcNow; }
if (info.Level > 0 && info.MaxLevel > 0 && info.Level >= info.MaxLevel) upgradePrice = 0;
else if (info.UpgradeCost > 0) upgradePrice = info.UpgradeCost;
if (!info.Complete) SnapshotOnce("safehouse partly read");
var h = SafehouseRead;
if (h != null) h(info);
}
void DoGameStats()
{
nextGameStats = DateTime.UtcNow.AddHours(3);
var st = game.ReadPlayerStats();
if (st == null || st.Found == 0)
{
Log("Couldn't read YOUR STATS from the game's menu");
nextGameStats = DateTime.UtcNow.AddMinutes(30);
return;
}
LastGameStats = st;
if (st.Level > 0) { statsLevel = st.Level; statsAt = DateTime.UtcNow; }
if (st.Level > 0 && level > st.Level + 5)
{
Log("Level " + st.Level + " from the game's STATS (the bot had " + level + ", too high)");
level = st.Level;
S.LastLevel = level;
ReportTarget();
}
Log("Game stats (" + st.Found + " read):");
foreach (var line in st.Lines()) Log("Game stats: " + line);
if (st.Level > 0 && st.Level > level && (last == null || last.Level <= 0 || last.Level < st.Level))
{
Log("Level " + st.Level + " from the game's STATS (the bot had " + (level > 0 ? level.ToString() : "none") + ")");
level = st.Level;
S.LastLevel = level;
game.MenuChanged(); PropertiesLevelUp();
ReportTarget();
}
var g = GameStatsRead;
if (g != null) g(st);
}
void DoOperations()
{
nextOps = DateTime.UtcNow.AddMinutes(5);
if (!game.OpenTab(Tab.Operations)) { Visited(Tab.Operations, null, true); return; }
int collectedBefore = Count.OpsCollected, startedBefore = Count.OpsStarted;
List<OpSlot> slots;
using (var f = game.Capture()) slots = game.ReadOpSlots(f);
var now = DateTime.UtcNow;
bool hold = false;
var holdUntil = now;
var cardAt = UtcParse(S.ExpCardAt);
bool cardSoon = !ExpCardReady(now) && cardAt != DateTime.MinValue && cardAt - now <= TimeSpan.FromMinutes(5);
if (slots.Any(x => x.Collect != null) && ExpPending(now) && cardSoon)
{
hold = true;
holdUntil = cardAt;
}
else if (slots.Any(x => x.Collect != null) && ExpPending(now) && ExpCardReady(now) && GlobalBossHoldsExp(now))
{
hold = true;
holdUntil = now.AddMinutes(10);
ExpSay("ops gb", "2X EXP: waits for the Global Boss - the finished operations are kept for it");
}
else if (slots.Any(x => x.Collect != null) && ExpPending(now) && ExpCardReady(now))
{
string why;
if (ExpBarsReady(now, last, out why) || ExpLastCall(now))
{
if (ClaimHeldExp(Tab.Operations, "the finished operations, then the bosses"))
{
bossesNow = true;
using (var f = game.Capture()) slots = game.ReadOpSlots(f);
}
else if (game.PlaytimeExpSeconds > 0 && game.PlaytimeExpSeconds <= 300 && ExpPending(now))
{
hold = true;
holdUntil = UtcParse(S.ExpCardAt);
}
}
else
{
hold = true;
int hp = HealthNow(now), max = HealthMaxKnown;
double s = why.StartsWith("health") && hp >= 0 && max > 0 ? (max - HealthRate * 60 - hp) / HealthRate : 300;
holdUntil = now.AddSeconds(Math.Max(60, Math.Min(600, s)));
ExpSay("ops", "2X EXP: the operations are finished - collected inside the 2X once " + (why.StartsWith("health") ? "health is almost full" : "there's the stamina to spend it")
+ " (" + why.Split(' ')[0] + " now " + why.Split(' ')[1] + ")");
}
if (hold && holdUntil < now.AddMinutes(1)) holdUntil = now.AddMinutes(1);
}
opsWaiting = hold;
for (int i = 0; i < 4 && !hold; i++)
{
var s = slots.FirstOrDefault(x => x.Collect != null);
if (s == null) break;
game.CollectOp(s);
Count.OpsCollected++;
string toast = game.ReadToast(1500, t => Parse.Has(t, "COLLECTED"));
Log(toast != null ? "Operation done - " + Regex.Replace(toast, @"\s+[xX×]$", "")
: "Collected operation \"" + s.Name + "\"");
using (var f = game.Capture()) slots = game.ReadOpSlots(f);
}
int free = slots.Count(x => !x.Locked && !x.Running && !x.Done && x.Collect == null && x.Name.Length == 0);
if (slots.Any(x => x.Done && x.Collect == null) && firstSeen.Add("finished operation without collect"))
{
Log("An operation is finished but I can't find its collect button - leaving it for you");
game.Snapshot("operation finished without collect button");
}
if (slots.Any(x => !x.Locked && !x.Running && !x.Done && x.Collect == null && x.Name.Length > 0))
SnapshotOnce("operation slot in an unknown state");
if (free > 0)
{
StartOperations(free);
game.OpsTop();
}
using (var f = game.Capture()) slots = game.ReadOpSlots(f);
var ops = OpsRead;
if (ops != null) ops(slots);
bool allRunning = slots.Count > 0 && slots.Where(x => !x.Locked).All(x => x.Running);
var left = slots.Where(x => x.Running && x.SecondsLeft > 0).Select(x => x.SecondsLeft).ToList();
int soonest = -1;
opsReadyAt = DateTime.MinValue;
if (left.Count > 0)
{
soonest = left.Min();
if (S.Events && EventOn(GameEvent.OpsSpeed)) soonest /= 2;
opsReadyAt = DateTime.UtcNow.AddSeconds(soonest);
nextOps = DateTime.UtcNow.AddSeconds(Math.Min(soonest + 8, 30 * 60));
}
opsKnown = true;
if (hold)
{
nextOps = holdUntil;
Visited(Tab.Operations, false, true, holdUntil.AddSeconds(90));
return;
}
Visited(Tab.Operations, Count.OpsCollected > collectedBefore || Count.OpsStarted > startedBefore, !allRunning,
soonest >= 0 ? DateTime.UtcNow.AddSeconds(soonest + 90) : DateTime.MinValue);
}
bool opsNoneSaid;
void StartOperations(int free)
{
game.OpsTop();
var all = new List<OpRow>();
string lastSig = null;
int step = 1;
for (int page = 0; page < 30; page++)
{
List<OpRow> rows;
using (var f = game.Capture()) rows = game.ReadOpRows(f, page);
if (page == 0) step = Game.OpsStep(rows);
foreach (var r in rows)
if (!all.Any(a => Parse.Key(a.Name) == Parse.Key(r.Name))) all.Add(r);
string sig = string.Join(",", rows.Select(r => r.Name));
if (sig == lastSig) break;
lastSig = sig;
game.OpsDown(step);
}
string only = (S.OperationOnly ?? "").Trim();
OpRow pick = only.Length > 0 ? all.FirstOrDefault(r => r.CanStart && Game.SameOp(r.Name, only))
: all.Where(r => r.CanStart && r.Seconds > 0 && r.Xp > 0).OrderByDescending(r => (double)r.Xp / r.Seconds).FirstOrDefault();
if (pick == null)
{
if (!opsNoneSaid) Log(only.Length > 0 ? "A slot is free but \"" + only + "\" can't be started (still locked, or not enough cash on hand?) - looking again every 15 minutes"
: "A slot is free but no operation can be started (not enough cash on hand?) - looking again every 15 minutes");
opsNoneSaid = true;
if (only.Length == 0) SnapshotOnce("no operation to start");
nextOps = DateTime.UtcNow.AddMinutes(15);
return;
}
opsNoneSaid = false;
game.OpsTop();
for (int i = 0; i < pick.Page; i++) game.OpsDown(step);
for (int n = 0; n < free; n++)
{
OpRow row;
using (var f = game.Capture()) row = game.ReadOpRows(f, pick.Page).FirstOrDefault(r => Parse.Key(r.Name) == Parse.Key(pick.Name));
if (row == null || !row.CanStart) break;
game.StartOp(row);
Count.OpsStarted++;
Log("Started operation \"" + row.Name + "\"");
}
}
DateTime BossesDoneUntil(DateTime now)
{
DateTime until;
return DateTime.TryParse(S.BossesDoneUntil, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out until) && until > now ? until : now;
}
int stagesLocked = -1;
bool stagesWalked;
void BossesLevelUp(int newLevel)
{
var now = DateTime.UtcNow;
if (newLevel > 0 && newLevel < S.BossesOpenLevel && BossesDoneUntil(now) > now) return;
nextBosses = now;
S.BossesDoneUntil = "";
S.BossesOpenLevel = 0;
}
void BossesDone(DateTime until)
{
nextBosses = until;
S.BossesDoneUntil = until.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture);
S.BossesOpenLevel = stagesLocked > level ? stagesLocked : 0;
SaveSettings();
if (S.BossesOpenLevel > 0) SayOnce("bosses open " + S.BossesOpenLevel, "Bosses: nothing to fight now - the next stage opens at level " + S.BossesOpenLevel);
}
void DoBosses()
{
nextBosses = DateTime.UtcNow.AddMinutes(20);
stagesLocked = -1; stagesWalked = false;
var now = DateTime.UtcNow;
bool exp = ExpRunning(now), mwOnly = !exp && ExpPending(now);
if (softDay != UtcDay(now)) { softDay = UtcDay(now); softTop = MostWantedFrom - 1; softMw = MostWantedFrom; }
bool soften = mwOnly && StageHitsKnown(now);
int floor = mwOnly ? ExpHealthFloor(now) : 0, lowest = mwOnly ? MostWantedFrom : 1;
var softened = new SortedDictionary<int, string>();
int softSeeks = 0;
DateTime claimAt;
var route = ExpPlan(now, out claimAt);
if (mwOnly && floor > 0 && HealthNow(now) >= 0 && HealthNow(now) <= floor)
{
ExpSay("mw", "Bosses: the Most Wanted wait - the health left is saved for the 2X EXP");
nextBosses = claimAt > now ? claimAt : now.AddMinutes(10);
return;
}
if (!game.OpenTab(Tab.Bosses)) return;
var st = ReadStage();
if (st != null && st.Stage < 0 && st.StagesTab != null) { game.StagesTab(st); st = ReadStage(); }
if (st == null || st.Stage < 0)
{
Log("Bosses: the stages page didn't read" + (st != null && st.Said.Length > 0 ? " (\"" + Cut(st.Said, 160) + "\")" : ""));
game.Snapshot("boss stage page not read", 60);
nextBosses = DateTime.UtcNow.AddMinutes(15);
return;
}
int hits = 0, waits = 0, nexts = 0, stamina = -1, sinceBars = 99, stageHits = 0, stageAt = st.Stage, seeks = 0, lastBeaten = -1, idle = 0, calm = 0;
bool sought = !(S.BossWeakestFirst || exp || mwOnly), noneReady = false, rescued = false, reopened = false;
var hitCosts = new List<int>();
string stageBoss = st.Boss, stop = null;
var done = new List<string>();
int progressAt = Environment.TickCount;
for (int look = 0; look < 400 && stop == null; look++)
{
if (Environment.TickCount - progressAt > 180000)
{
stop = "the stages list went round 3 minutes without a hit";
game.Snapshot("boss stages went round", 60);
break;
}
if (st == null || st.Stage < 0)
{
if (waits == 8 && !reopened)
{
reopened = true;
SnapshotOnce("boss page left");
Log("Bosses: the stages page went away under the fights - opening Bosses again");
if (game.OpenTab(Tab.Bosses)) { waits = 0; st = ReadStage(); continue; }
}
if (++waits > 8) { stop = "the page stopped reading"; game.Snapshot("boss stage page not read", 60); break; }
game.Wait(500);
st = ReadStage();
continue;
}
if (st.Stage != stageAt)
{
if (!noneReady && lastBeaten > 0 && st.Stage != lastBeaten + 1 && seeks < 4) sought = false;
stageAt = st.Stage; stageBoss = st.Boss; stageHits = 0;
progressAt = Environment.TickCount;
}
if (st.Boss.Length > 0) stageBoss = st.Boss;
if (st.Attack == null)
{
if (st.Cooldown || ((st.DefeatedToday || st.Cleared) && waits < 10)) { waits++; game.Wait(500); st = ReadStage(); continue; }
if (!sought)
{
sought = true; seeks++;
if (mwOnly ? soften && SeekSoft(ref st) : SeekReady(ref st, Math.Max(lowest, lastBeaten > 0 ? lastBeaten + 1 : 1))) { waits = 0; continue; }
noneReady = true;
if (st == null || st.Stage < 0) continue;
}
if (!st.Cleared && !st.DefeatedToday && st.Next == null && !rescued && !stagesWalked)
{
rescued = true; sought = true; seeks++;
if (SeekReady(ref st, lowest, mwOnly)) { waits = 0; continue; }
noneReady = true;
if (st == null || st.Stage < 0) continue;
}
if ((st.Cleared || st.DefeatedToday) && nexts < 2 && !stagesWalked)
{
nexts++;
for (int roll = 0; roll < 6 && st != null && st.Next == null && !st.ListAt.IsEmpty; roll++)
{
string sig = st.ListSig;
game.StagesListScroll(st, -3); game.Wait(500); st = ReadStage();
if (ListStill(sig, st)) break;
}
if (st != null && st.Next != null)
{
game.StageNext(st);
waits = 0;
st = ReadStage();
continue;
}
if (st == null) continue;
}
else if (!st.Cleared && !st.DefeatedToday && st.Next != null && nexts < 2)
{
nexts++;
game.StageNext(st);
waits = 0;
st = ReadStage();
continue;
}
if (calm < 4 && !stagesWalked) { calm++; game.Wait(1500); st = ReadStage(); continue; }
stop = st.Cleared || st.DefeatedToday ? "cleared" : stageHits > 0 ? "no attack on a stage just hit" : "no attack";
break;
}
waits = 0; calm = 0;
if (sinceBars >= 5 || stamina <= 1)
{
bool cardHealth = st.YourHealth >= 0;
var h = ReadBarsSettled(15000, x => x.Stamina >= 0 && (x.Health >= 0 || cardHealth));
if (h.Stamina < 0)
{
var card = ReadStage();
int max = game.BarMax(1);
if (card != null && card.YourStamina >= 0 && max > 0 && card.YourStaminaMax == max) h.Stamina = card.YourStamina;
}
if (h.Stamina < 0) { Log("Couldn't read stamina (" + game.BarLook(1) + ") - bosses wait a couple of minutes"); game.Snapshot("stamina unreadable at bosses", 60); stop = "bars"; break; }
stamina = h.Stamina;
sinceBars = 0;
NoteHealth(h.Health, h.HealthMax, DateTime.UtcNow);
if (h.Health >= 0 && !h.HealthAtLeast(LowHealthPct)) { stop = "health"; break; }
}
if (st.YourHealthMax > 0 && st.YourHealth * 100 < LowHealthPct * st.YourHealthMax) { stop = "health"; break; }
if (stamina <= 0) { stop = "stamina"; break; }
if (mwOnly)
{
NoteHealth(st.YourHealth, st.YourHealthMax, DateTime.UtcNow);
floor = ExpHealthFloor(DateTime.UtcNow);
int hp = HealthNow(DateTime.UtcNow);
if (floor > 0 && hp >= 0 && hp - (S.BossHitHealth > 0 ? S.BossHitHealth : 10) < floor) { stop = "saving"; break; }
}
if (!sought)
{
sought = true; seeks++;
if (mwOnly ? soften && SeekSoft(ref st) : SeekReady(ref st, Math.Max(lowest, lastBeaten > 0 ? lastBeaten + 1 : 1))) { waits = 0; continue; }
noneReady = true;
if (st == null || st.Stage < 0 || st.Attack == null) continue;
}
bool softHit = false;
if (mwOnly)
{
string why = null;
bool target = soften && SoftTarget(st.Stage);
if (target && SoftenOk(ref st, out why)) softHit = true;
else
{
if (st == null || st.Stage < 0) continue;
if (target)
{
if (stageHits == 0 && why != null && saidOnce.Add("soften " + softDay + " " + st.Stage)) Log("Bosses: stage " + st.Stage + " not brought low before the 2X EXP - " + why);
SoftPassed(st.Stage);
}
if (soften && softSeeks++ < 60 && SeekSoft(ref st)) { waits = 0; continue; }
stop = "no most wanted";
break;
}
}
int bossBefore = st.Health, youBefore = st.YourHealth;
HealthSpent();
game.StageAttack(st);
progressAt = Environment.TickCount;
hits++; stageHits++; stamina--; sinceBars++;
Count.BossHits++;
game.Wait(1100);
st = ReadStage();
if (st != null) NoteHealth(st.YourHealth, st.YourHealthMax, DateTime.UtcNow);
if (st != null && youBefore > 0 && st.YourHealth >= 0 && st.YourHealth < youBefore) hitCosts.Add(youBefore - st.YourHealth);
if (st != null && st.Stage == stageAt && bossBefore > 0)
{
bool beaten = st.DefeatedToday || st.Health == 0;
int d = beaten ? (softHit ? Math.Max(bossBefore, st.Damage) : -1) : st.Health >= 0 && st.Health < bossBefore ? Math.Max(bossBefore - st.Health, st.Damage) : -1;
if (d > 0) NoteStageHit(stageAt, d, DateTime.UtcNow, softHit && beaten);
if (softHit && !beaten) softened[stageAt] = "stage " + stageAt + " at " + (st.Health >= 0 ? st.Health.ToString("N0", CultureInfo.InvariantCulture) : "?")
+ (st.HealthMax > 0 ? " / " + st.HealthMax.ToString("N0", CultureInfo.InvariantCulture) : "");
if (softHit && beaten)
{
Log("Bosses: stage " + stageAt + " was beaten by a hit meant to bring it low (" + bossBefore.ToString("N0", CultureInfo.InvariantCulture)
+ " health left, the biggest hit lately said less) - its hits count bigger from now on");
game.Snapshot("boss beaten before the 2X", 60);
}
}
if (st != null && st.Stage == stageAt && bossBefore >= 0 && st.Health == bossBefore && !st.DefeatedToday) idle++;
else idle = 0;
if (idle >= 1)
{
bool hospital;
using (var f = game.Capture()) hospital = Game.TopRightYouHospitalized(f);
if (hospital) { stop = "health"; break; }
}
if (idle >= 6) { stop = "hits change nothing (in the hospital?)"; game.Snapshot("boss hits change nothing", 60); break; }
if (st != null && st.Stage == stageAt && (st.DefeatedToday || st.Health == 0))
{
Count.BossesBeaten++;
lastBeaten = stageAt;
string who = "stage " + stageAt + (stageBoss.Length > 0 ? " " + stageBoss : "");
done.Add(who + (stageHits > 1 ? " (" + stageHits + " hits)" : ""));
Discord.Post(Ping.Boss, "\U0001F44A Boss beaten", who + " in " + stageHits + (stageHits == 1 ? " hit" : " hits"), Shot, Discord.Gold);
stageHits = 0;
}
}
if (softened.Count > 0) Log("Bosses: brought low for the 2X EXP (not beaten): " + string.Join(", ", softened.Values));
if (done.Count > 0) Log("Bosses: beat " + string.Join(", ", done) + (st != null && st.Tokens >= 0 ? " - " + st.Tokens.ToString("N0", CultureInfo.InvariantCulture) + " boss tokens" : ""));
int reset = st != null && st.ResetSeconds >= 0 ? st.ResetSeconds : SecondsToUtcMidnight();
string where = st != null && st.Stage > 0 ? "stage " + st.Stage + (st.Boss.Length > 0 ? " (" + st.Boss + ")" : "") : "the next stage";
if (hitCosts.Count >= 3)
{
int cost = hitCosts.OrderBy(x => x).ElementAt(hitCosts.Count / 2);
if (cost != S.BossHitHealth) { S.BossHitHealth = cost; SaveSettings(); }
}
if (mwOnly)
{
if (stop == "cleared" || stop == "no attack") stop = "no most wanted";
if (route == ExpRoute.Bosses && DateTime.UtcNow >= claimAt && stop != null && stop != "bars" && !S.ExpMostWantedDone)
{
S.ExpMostWantedDone = true;
SaveSettings();
Log("Bosses: the Most Wanted had their round - the 2X EXP next, once health is almost full");
}
if (stop == "saving" || stop == "no most wanted")
{
ExpSay("mw", stop == "saving" ? "Bosses: the Most Wanted stop - the health left is saved for the 2X EXP"
: "Bosses: nothing left to bring low safely - every boss waits for the 2X EXP");
nextBosses = claimAt > DateTime.UtcNow && claimAt < DateTime.UtcNow.AddHours(1) ? claimAt : DateTime.UtcNow.AddHours(1);
NoteBossRound(1, int.MaxValue);
return;
}
}
switch (stop)
{
case "cleared":
SayOnce("bosses cleared " + DateTime.UtcNow.ToString("yyyyMMdd"), "Bosses: every stage you can reach is beaten today (highest " + (st != null ? st.Highest : -1)
+ ") - back after the game's reset at 00:00 UTC");
BossesDone(DateTime.UtcNow.AddSeconds(reset + 60));
break;
case "no attack":
if (saidOnce.Add("bosses noattack " + DateTime.UtcNow.ToString("yyyyMMdd") + " " + (st != null ? st.Stage : -1)))
{
Log("Bosses: " + where + " shows no ATTACK" + (st != null && st.NeedLevel > 0 ? " - it needs level " + st.NeedLevel : " (a higher level needed?)")
+ (st != null ? " - highest stage " + st.Highest + ", " + (st.Next != null ? "the list's NEXT pressed " + nexts + "x" : "no NEXT in the list's sight") : "")
+ (level >= 180 ? " - the bot looks again in 15 minutes" : " - the bot looks again after a level-up or the game's reset") + (st != null && st.Said.Length > 0 ? " (\"" + Cut(st.Said, 160) + "\")" : ""));
SnapshotOnce("boss stage locked after the reset");
}
if (level >= 180) { nextBosses = DateTime.UtcNow.AddMinutes(15); break; }
BossesDone(DateTime.UtcNow.AddSeconds(reset + 60));
break;
case "stamina":
case "health":
if (hits == 0) Log("Bosses: " + where + " waits - " + (stop == "stamina" ? "no stamina left" : "health is low"));
nextBosses = DateTime.UtcNow.AddMinutes(exp ? 2 : 10);
break;
case "bars": nextBosses = DateTime.UtcNow.AddMinutes(2); break;
case null: nextBosses = DateTime.UtcNow.AddMinutes(1); break;
default:
Log("Bosses: stopped at " + where + " - " + stop);
nextBosses = DateTime.UtcNow.AddMinutes(15);
break;
}
bool up = stop != "cleared" && stop != "no attack";
Info("bossname", st != null && st.Stage > 0 ? "Stage " + st.Stage + (st.Boss.Length > 0 ? " - " + st.Boss : "") : "?");
if (stop == "cleared")
{
var t = TimeSpan.FromSeconds(Math.Max(60, reset));
Info("bosswhen", string.Format("all beaten - again in {0}h {1:00}m", (int)t.TotalHours, t.Minutes));
}
else Info("bosswhen", stop == "no attack" ? "needs a higher level" : "ready to fight");
NoteBossRound(up ? 1 : 0, up ? int.MaxValue : reset);
}
string softDay = "";
int softTop = MostWantedFrom - 1;
int softMw = MostWantedFrom;
const int SoftTimes = 3, SoftHitsNeeded = 3;
static readonly TimeSpan StageHitsKeep = TimeSpan.FromDays(3);
bool SoftTarget(int stage) { return stage > 0 && (stage < MostWantedFrom ? stage <= softTop : stage >= softMw); }
void SoftPassed(int stage)
{
if (stage <= 0) return;
if (stage < MostWantedFrom) softTop = Math.Min(softTop, stage - 1);
else softMw = Math.Max(softMw, stage + 1);
}
Dictionary<int, Tuple<int, DateTime, int>> StageHitsRead()
{
var d = new Dictionary<int, Tuple<int, DateTime, int>>();
foreach (var part in (S.StageHits ?? "").Split(';'))
{
var p = part.Split(':');
int stage, dmg, n;
DateTime at;
if (p.Length == 4 && int.TryParse(p[0], out stage) && int.TryParse(p[1], out dmg) && dmg > 0 && int.TryParse(p[3], out n) && n > 0
&& DateTime.TryParseExact(p[2], "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out at))
d[stage] = Tuple.Create(dmg, at, n);
}
return d;
}
void NoteStageHit(int stage, int dmg, DateTime now, bool trusted = false)
{
if (stage <= 0 || dmg <= 0) return;
var d = StageHitsRead();
Tuple<int, DateTime, int> was;
bool fresh = d.TryGetValue(stage, out was) && now - was.Item2 < StageHitsKeep;
int big = fresh ? Math.Max(was.Item1, dmg) : dmg, n = (fresh ? was.Item3 : 0) + 1;
if (trusted) n = Math.Max(n, SoftHitsNeeded);
d[stage] = Tuple.Create(big, now.Date, Math.Min(n, 999));
S.StageHits = string.Join(";", d.OrderBy(kv => kv.Key).Select(kv => kv.Key + ":" + kv.Value.Item1 + ":"
+ kv.Value.Item2.ToString("yyyyMMdd", CultureInfo.InvariantCulture) + ":" + kv.Value.Item3));
SaveSettings();
}
int StageHitOf(int stage, DateTime now)
{
Tuple<int, DateTime, int> was;
return StageHitsRead().TryGetValue(stage, out was) && now - was.Item2 < StageHitsKeep && was.Item3 >= SoftHitsNeeded ? was.Item1 : -1;
}
bool StageHitsKnown(DateTime now) { return StageHitsRead().Any(kv => now - kv.Value.Item2 < StageHitsKeep && kv.Value.Item3 >= SoftHitsNeeded); }
bool SoftenOk(ref BossStage st, out string why)
{
why = null;
if (st.Attack == null || st.DefeatedToday || st.Cleared) return false;
int big = StageHitOf(st.Stage, DateTime.UtcNow);
if (big <= 0) { why = "fewer than " + SoftHitsNeeded + " hits on it written down lately"; return false; }
if (st.Health <= 0 || st.HealthMax <= 0 || st.Health > st.HealthMax) { why = "its health didn't read"; return false; }
if (st.Health <= (long)SoftTimes * big) { why = "it's low already"; return false; }
game.Wait(300);
var again = ReadStage();
if (again == null || again.Stage != st.Stage || again.Health != st.Health || again.Attack == null)
{
if (again != null && again.Stage > 0) st = again;
why = "its health read differently twice";
return false;
}
st = again;
return true;
}
bool SeekSoft(ref BossStage st)
{
if (st == null || st.Stage < 0) return false;
if (softTop >= 1)
{
if (SeekReadyDown(ref st, softTop)) { if (st != null && st.Stage > 0 && st.Stage < MostWantedFrom) softTop = Math.Min(softTop, st.Stage); return true; }
if (st == null || st.Stage < 0) return false;
softTop = 0;
}
if (SeekReadyFrom(ref st, softMw)) return true;
if (st == null || st.Stage < 0 || st.Next == null || st.NextStage < softMw) return false;
game.StageNext(st);
var now = ReadStage();
if (now != null && now.Stage > 0) st = now;
return true;
}
bool SeekReadyDown(ref BossStage st, int top)
{
if (st == null || st.ListAt.IsEmpty) return false;
for (int roll = 0; roll < 30 && st.ListLast > 0 && st.ListLast < top; roll++)
{
int was = st.ListLast;
string wasSig = st.ListSig;
game.StagesListScroll(st, -3);
game.Wait(400);
var now = ReadStage();
if (now == null || now.Stage < 0 || now.ListAt.IsEmpty) return false;
st = now;
if (st.ListLast == was || ListStill(wasSig, st)) break;
}
for (int roll = 0; roll < 30; roll++)
{
var r = st.Readies.Where(kv => kv.Key > 0 && kv.Key <= top).OrderByDescending(kv => kv.Key).FirstOrDefault();
if (r.Value != null)
{
game.StageReady(r.Value);
var now = ReadStage();
if (now != null && now.Stage > 0) st = now;
if (st.Stage != r.Key && saidOnce.Add("bosses ready " + r.Key))
Log("Bosses: READY pressed on stage " + r.Key + " but the page shows stage " + st.Stage);
return true;
}
if (st.ListFirst == 1) return false;
int first = st.ListFirst;
string firstSig = st.ListSig;
game.StagesListScroll(st, 3);
game.Wait(400);
var then = ReadStage();
if (then == null || then.Stage < 0 || then.ListAt.IsEmpty) return false;
st = then;
if ((first > 0 && st.ListFirst == first) || ListStill(firstSig, st)) return false;
}
return false;
}
static bool ListStill(string before, BossStage now) { return now != null && !string.IsNullOrEmpty(before) && before == now.ListSig; }
BossStage ReadStage()
{
BossStage st = null;
for (int look = 0; look < 3; look++)
{
if (look > 0) game.Wait(400);
using (var f = game.Capture()) st = game.ReadBossStage(f);
if (st != null && st.Stage > 0) return st;
}
return st;
}
bool SeekReady(ref BossStage st, int from, bool strict = false)
{
if (st == null || st.ListAt.IsEmpty) return false;
if (strict) return SeekReadyFrom(ref st, from);
for (int roll = 0; roll < 30 && !(st.ListFirst > 0 && st.ListFirst <= from); roll++)
{
int was = st.ListFirst;
string wasSig = st.ListSig;
game.StagesListScroll(st, 10);
game.Wait(400);
var now = ReadStage();
if (now == null || now.Stage < 0 || now.ListAt.IsEmpty) break;
st = now;
if ((was > 0 && st.ListFirst == was) || ListStill(wasSig, st)) break;
}
for (int roll = 0; roll < 60; roll++)
{
if (st.Ready != null && (st.ReadyStage < 0 || st.ReadyStage >= from))
{
int at = st.ReadyStage, was = st.Stage;
game.StageReady(st);
var now = ReadStage();
if (now != null && now.Stage > 0) st = now;
SnapshotOnce("boss stage READY pressed");
if (at > 0 && st.Stage != at && saidOnce.Add("bosses ready " + at))
Log("Bosses: READY pressed on stage " + at + " but the page shows stage " + st.Stage + (was == st.Stage ? " still" : ""));
if (at < 0 && st.Stage == was && st.Stage > 0)
{
if (saidOnce.Add("bosses ready still " + UtcDay(DateTime.UtcNow))) Log("Bosses: READY pressed in the list but the page still shows stage " + st.Stage);
SnapshotOnce("boss READY did nothing");
return false;
}
return true;
}
if (st.LockedLevel > 0 && st.Readies.All(r => r.Key > 0 && r.Key < from)) { stagesLocked = st.LockedLevel; stagesWalked = st.Next == null; return false; }
if (st.Next != null || st.ListAt.IsEmpty) return false;
int last = st.ListLast;
string lastSig = st.ListSig;
game.StagesListScroll(st, -3);
game.Wait(400);
var then = ReadStage();
if (then == null || then.Stage < 0) return false;
st = then;
if (last > 0 && st.ListLast == last && st.Ready == null) { stagesWalked = st.Next == null; return false; }
if (ListStill(lastSig, st)) { stagesWalked = st.Next == null; return false; }
}
return false;
}
bool SeekReadyFrom(ref BossStage st, int from)
{
for (int roll = 0; roll < 30 && !(st.ListFirst > 0 && st.ListFirst <= from); roll++)
{
int was = st.ListFirst;
string wasSig = st.ListSig;
game.StagesListScroll(st, 10);
game.Wait(400);
var now = ReadStage();
if (now == null || now.Stage < 0 || now.ListAt.IsEmpty) break;
st = now;
if ((was > 0 && st.ListFirst == was) || ListStill(wasSig, st)) break;
}
for (int roll = 0; roll < 60; roll++)
{
var r = st.Readies.FirstOrDefault(kv => kv.Key >= from);
if (r.Value != null)
{
game.StageReady(r.Value);
var now = ReadStage();
if (now != null && now.Stage > 0) st = now;
if (st.Stage != r.Key && saidOnce.Add("bosses ready " + r.Key))
Log("Bosses: READY pressed on stage " + r.Key + " but the page shows stage " + st.Stage);
return true;
}
if (st.Next != null || st.ListAt.IsEmpty) return false;
int last = st.ListLast;
string lastSig = st.ListSig;
game.StagesListScroll(st, -3);
game.Wait(400);
var then = ReadStage();
if (then == null || then.Stage < 0) return false;
st = then;
if ((last > 0 && st.ListLast == last) || ListStill(lastSig, st)) return false;
}
return false;
}
static int SecondsToUtcMidnight() { var now = DateTime.UtcNow; return (int)(now.Date.AddDays(1) - now).TotalSeconds; }
static string Cut(string s, int n) { return s.Length <= n ? s : s.Substring(0, n) + "..."; }
Header ReadBarsSettled(int maxMs) { return ReadBarsSettled(maxMs, h => h.Stamina >= 0); }
Header ReadBarsSettled(int maxMs, Func<Header, bool> ok)
{
int end = Environment.TickCount + maxMs;
int closed = 0;
while (true)
{
Header h;
using (var f = game.Capture())
{
h = game.ReadHeader(f);
if (!ok(h) && closed < 10 && game.CloseTerritoryToast(f)) { closed++; end = Math.Max(end, Environment.TickCount + 1500); continue; }
}
if (ok(h) || end - Environment.TickCount <= 0) return h;
game.Wait(400);
}
}
const int LowHealthPct = 10;
Rejoiner rejoiner = new Rejoiner();
int[] barsSeen;
DateTime barsSameSince = DateTime.MinValue;
int dialogLookAt = Environment.TickCount - 60000;
bool dialogUp;
bool disconnectHint;
bool robloxUsed;
DateTime robloxUsedAt;
string dialogSeen;
uint stepInput;
IntPtr focusBack;
DateTime gameInput;
DateTime GameInput { get { return game.Win.LastInput > gameInput ? game.Win.LastInput : gameInput; } }
internal static int LookEvery(bool hint, bool tryUnderWay, bool suspect, bool upLastTime, bool rejoining)
{
return hint || tryUnderWay ? 0 : suspect || upLastTime || rejoining ? 10000 : 60000;
}
bool LookForDisconnect(Frame f, bool suspect, ref Game.Disconnect dialog)
{
int every = LookEvery(disconnectHint, rejoiner.Busy, suspect, dialogUp, rejoiner.Active);
if (Environment.TickCount - dialogLookAt < every) return false;
disconnectHint = false;
dialogLookAt = Environment.TickCount;
dialog = game.ReadDisconnect(f);
if (dialog == null && !rejoiner.Active) eventWindow = !Game.EventPopupX(f).IsEmpty;
if (dialog == null && !rejoiner.Active) inviteWindow = !Game.InviteIgnore(f).IsEmpty;
if (dialog == null && !rejoiner.Active && suspect) notifyWindow = Game.NotifyPromptCancel(f) != null;
return true;
}
bool eventWindow;
bool notifyWindow;
bool inviteWindow;
string defendName, defendCity;
int defendNo;
bool defendStationed;
DateTime defendHit = DateTime.MinValue, defendNext = DateTime.MinValue;
static readonly TimeSpan DefendAgain = TimeSpan.FromMinutes(3), DefendOver = TimeSpan.FromMinutes(2);
DateTime nextRulingLook = DateTime.MinValue;
void TerritoryCheck()
{
bool lost;
System.Drawing.Point x;
string read, by, text;
for (int toast = 0; toast < 4; toast++)
{
using (var f = game.Capture()) read = game.TerritoryToastDistrict(f, out lost, out x, out by, out text);
if (read == null) break;
string spared = S.DefendTerritory ? DefendSpared(S.NeverDefend, by, text) : null;
if (spared != null)
SayOnce("never defend " + Parse.Key(read) + DateTime.UtcNow.ToString("yyyyMMddHH"), "Territory: " + read + " is attacked by "
+ (by.Length > 0 ? by : spared) + " - not defended, \"" + spared + "\" is on your never-defend list");
bool ours = S.DefendTerritory && !Lowering && spared == null && NoteAttack(read, lost);
if (ours && DateTime.UtcNow >= defendNext && DateTime.UtcNow >= takedownHold) Defend(true);
using (var f = game.Capture()) game.CloseTerritoryToast(f);
}
if (DateTime.UtcNow >= nextRulingLook)
{
nextRulingLook = DateTime.UtcNow.AddMinutes(1);
using (var f = game.Capture()) game.CloseRulingFamily(f);
}
Shipment();
var now = DateTime.UtcNow;
if (defendName == null || Lowering) return;
if (now - defendHit >= DefendOver) EndDefence("no attack notification for 2 minutes");
else if (S.DefendTerritory && now >= defendNext && now >= takedownHold && defendByList) Defend();
}
void SaveDefence()
{
S.Defence = defendName == null ? "" : string.Join("|", defendName, defendCity, defendNo.ToString(CultureInfo.InvariantCulture),
defendStationed ? "1" : "0", defendHit.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture));
SaveSettings();
}
void LoadDefence()
{
defendName = null; defendStationed = false; defendByList = false; defendNext = DateTime.MinValue;
var p = (S.Defence ?? "").Split('|');
int no;
DateTime hit;
if (p.Length != 5 || p[0].Length == 0 || !int.TryParse(p[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out no)
|| !DateTime.TryParse(p[4], CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out hit)) return;
defendName = p[0]; defendCity = p[1]; defendNo = no; defendStationed = p[3] == "1"; defendHit = hit;
if (defendStationed) Log("Territory: henchmen are still stationed at " + defendName + " from before - recalled 2 minutes after its last attack");
}
bool defendByList;
internal static string DefendSpared(string list, string by, string text)
{
Func<string, string> key = s => Parse.Key(s).Replace("2", "II").Replace("3", "III");
string b = key(by), t = key(text);
foreach (var e in NeverAttackEntries(list))
{
string k = key(e);
if (k.Length < 3) continue;
if (b == k || (k.Length >= 6 && View.Distance(b, k) <= 1)) return e;
if (b.Length > 0) continue;
for (int i = 0; i + k.Length <= t.Length; i++)
{
if (i + k.Length < t.Length && t[i + k.Length] == 'I') continue;
string part = t.Substring(i, k.Length);
if (part == k || (k.Length >= 6 && View.Distance(part, k) <= 1)) return e;
}
}
return null;
}
readonly Dictionary<string, DateTime> shipmentsSaid = new Dictionary<string, DateTime>();
void Shipment()
{
string text;
System.Drawing.Point x;
using (var f = game.Capture()) text = game.ShipmentToast(f, out x);
if (text == null) return;
var now = DateTime.UtcNow;
string key = Parse.Key(text);
foreach (var old in shipmentsSaid.Where(kv => now - kv.Value > TimeSpan.FromMinutes(30)).Select(kv => kv.Key).ToList()) shipmentsSaid.Remove(old);
if (!shipmentsSaid.ContainsKey(key))
{
shipmentsSaid[key] = now;
Log("Territory: a shipment notification: \"" + Cut(text, 200) + "\"");
SnapshotOnce("territory shipment notification");
}
game.CloseShipmentToast(x);
}
bool NoteAttack(string read, bool lost)
{
string city, name;
int no;
if (!Game.MatchDistrict(read, out city, out no, out name))
{
SayOnce("district unknown " + Parse.Key(read), "Territory: \"" + read + "\" is attacked, but it isn't one of the game's districts as the bot knows them - not defended");
return false;
}
if (defendName != null && defendName != name)
{
SayOnce("defending elsewhere " + name, "Territory: " + name + " is attacked too - the crew stays at " + defendName + " while that attack lasts");
return false;
}
bool fresh = defendName == null;
defendName = name; defendCity = city; defendNo = no; defendHit = DateTime.UtcNow;
if (fresh) { defendNext = DateTime.MinValue; Log("Territory: " + name + " (" + city + ") is attacked - defending it"); }
else if (lost) defendNext = DateTime.MinValue;
SaveDefence();
return true;
}
void Defend(bool toast = false)
{
defendNext = DateTime.UtcNow + DefendAgain;
if (S.DefendHeal && defendStationed)
{
var h = ReadBarsSettled(3000, b => b.Stamina >= 0);
int keep = Kept(h.StaminaMax);
if (h.Stamina > keep && game.OpenTab(Tab.Crew))
{
int spent = game.HealRecovering(h.Stamina, keep);
if (spent > 0) Log("Territory: healed the beaten defenders with " + spent + " stamina");
}
}
bool viaToast = toast && game.OpenDistrictFromToast(defendName);
if (toast)
{
if (!viaToast) SayOnce("district by list", "Territory: pressing the notification didn't open " + defendName + " - through the Territory page instead");
else SnapshotOnce("district opened from the notification");
defendByList = !viaToast;
}
if (!viaToast) game.CloseDistrict();
if (!viaToast && !game.OpenDistrict(defendCity, defendNo, defendName))
{
if (game.LastDistrictGone) { Log("Territory: " + defendName + " isn't your family's any more - the defence ends"); EndDefence(null); return; }
game.Snapshot("territory district not opened", 60);
return;
}
DistrictWindow w;
using (var f = game.Capture()) w = game.ReadDistrictWindow(f);
string ours = last != null ? last.Family : null;
if (w != null && !string.IsNullOrEmpty(ours) && !Game.HeldByUs(w.HeldBy, ours))
{
game.CloseDistrict(w);
Log("Territory: " + defendName + " is held by " + w.HeldBy + " now - the defence ends");
EndDefence(null);
return;
}
int n = game.StationAllHere();
if (n == -2) SnapshotOnce("territory station crew none free");
else if (n < 0) game.Snapshot("territory station all not read", 60);
game.CloseDistrict();
if (n > 0) { defendStationed = true; SaveDefence(); Log("Territory: stationed " + n + " henchmen at " + defendName); }
else if (n == -2) SayOnce("station crew none free", "Territory: STATION CREW showed no free henchmen at " + defendName + " - none stationed (all away or busy)");
}
void EndDefence(string why)
{
string name = defendName;
bool stationed = defendStationed;
defendName = null; defendByList = false; defendStationed = false;
SaveDefence();
if (!stationed || name == null) { if (why != null && name != null) Log("Territory: the attack on " + name + " is over (" + why + ")"); return; }
int n = game.OpenTab(Tab.Crew) ? game.RecallFrom(name) : 0;
Log("Territory: the attack on " + name + " is over" + (why != null ? " (" + why + ")" : "") + " - " + (n > 0 ? n + " henchmen recalled" : "none of yours were still there"));
}
void CloseEventWindow()
{
TerritoryCheck();
if (notifyWindow)
{
notifyWindow = false;
using (var f = game.Capture()) game.CloseNotifyPrompt(f);
}
if (inviteWindow)
{
inviteWindow = false;
using (var f = game.Capture()) game.CloseInvite(f);
}
if (!eventWindow) return;
eventWindow = false;
using (var f = game.Capture()) game.CloseEventPopup(f);
}
DateTime robloxBarSince = DateTime.MinValue, robloxBarSaid = DateTime.MinValue;
void NoteRobloxBar(bool showing)
{
var now = DateTime.UtcNow;
if (showing)
{
if (robloxBarSince != DateTime.MinValue) return;
robloxBarSince = now;
if (now - robloxBarSaid < TimeSpan.FromMinutes(30)) return;
robloxBarSaid = now;
Log("Roblox's own title bar is over the game's top bar - the bot reads nothing up there (bars, cash, name) until it goes");
SnapshotOnce("roblox title bar over the top bar");
}
else if (robloxBarSince != DateTime.MinValue)
{
if (robloxBarSaid >= robloxBarSince && now - robloxBarSince >= TimeSpan.FromMinutes(1))
Log("Roblox's title bar is gone after " + (int)Math.Round((now - robloxBarSince).TotalMinutes) + " min - the top bar reads again");
robloxBarSince = DateTime.MinValue;
barSweeps = 0;
barFixTries = 0;
nextBarSweep = DateTime.MinValue;
bankHiddenSaid = false;
}
}
DateTime nextBarSweep = DateTime.MinValue;
int barSweeps;
int barFixTries;
bool SweepRobloxBar(bool eagerly = false)
{
var now = DateTime.UtcNow;
if (!game.RobloxBar || robloxBarSince == DateTime.MinValue || now - robloxBarSince < TimeSpan.FromSeconds(20) && !eagerly || now < nextBarSweep) return false;
if (!eagerly && S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000)) return false;
barSweeps++;
nextBarSweep = now.AddMinutes(barSweeps < 3 ? 2 : 15);
bool gone = false;
State("Moving the mouse over Roblox's title bar so it goes away");
if (!Act(delegate { gone = game.ClearRobloxBar(); })) return true;
if (gone) { Log("Roblox's title bar went away after the bot moved the mouse up to it and back down"); return true; }
bool f11 = false;
State("Pressing F11 so Roblox's title bar goes away");
if (!Act(delegate { f11 = game.ToggleFullscreenClearBar(); })) return true;
if (f11) { Log("Roblox's title bar went away after the bot pressed F11 - the top bar reads again"); barSweeps = 0; }
else if (eagerly || barSweeps >= 3) SayOnce("roblox bar stays", "Roblox's title bar still stays after the mouse and F11 - the bot can't read the top bar until it goes "
+ "(moving the mouse to the top of the screen and away, or playing Roblox in a window instead of fullscreen, clears it)");
else Log("Roblox's title bar stays after the mouse and F11 - trying again in 2 minutes");
return true;
}
bool FixRobloxBarNow()
{
if (!game.RobloxBar) return false;
if (robloxBarSince == DateTime.MinValue) robloxBarSince = DateTime.UtcNow;
nextBarSweep = DateTime.MinValue;
return SweepRobloxBar(true);
}
int emptySaid, copiesSaid;
DateTime emptySaidAt = DateTime.MinValue;
int halfSaid;
DateTime halfSaidAt = DateTime.MinValue;
readonly HashSet<string> coverSaid = new HashSet<string>();
void NoteEmptyPictures()
{
NoteScreenFirst();
string cover = RobloxWindow.CoveredBy;
if (cover != null && coverSaid.Add(cover))
Log(cover + " lies over Roblox on this PC, so no game pictures go to Discord while it does (they could show it)"
+ (cover.EndsWith(" overlay") ? " - turn that overlay off to get them back" : ""));
int half = RobloxWindow.HalfDrawn;
if (half > halfSaid && DateTime.UtcNow - halfSaidAt >= TimeSpan.FromHours(1))
{
Log("Roblox's picture came half drawn " + (half - halfSaid) + " time" + (half - halfSaid == 1 ? "" : "s") + " - the bot copied it again from the screen");
halfSaid = half; halfSaidAt = DateTime.UtcNow;
}
int empty = RobloxWindow.EmptyPictures, copies = RobloxWindow.ScreenCopies;
int n = (empty - emptySaid) - (copies - copiesSaid);
if (n <= 0) { emptySaid = empty; copiesSaid = copies; return; }
if (DateTime.UtcNow - emptySaidAt < TimeSpan.FromMinutes(30)) return;
emptySaid = empty; copiesSaid = copies; emptySaidAt = DateTime.UtcNow;
Log("Windows gave the bot an empty picture of Roblox " + (n == 1 ? "once" : n + " times") + " (a graphics problem on this PC) - "
+ "it reads the game only with nothing over Roblox. If it goes on: restart the PC, update the graphics driver");
}
readonly object screenFirstLock = new object();
void NoteScreenFirst()
{
if (!RobloxWindow.ScreenFirst || S.ScreenFirst) return;
lock (screenFirstLock)
{
if (S.ScreenFirst) return;
S.ScreenFirst = true;
SaveSettings();
}
Log("Windows gave the bot an empty picture of Roblox while the screen showed the game. On this PC that can make Roblox "
+ "flash white, so from now on the bot copies Roblox from the screen while nothing is over it");
}
bool DisconnectBeforeRound()
{
if (LeaveDeadGame()) return true;
if (Environment.TickCount - dialogLookAt < 5000) return false;
using (var f = game.Capture()) return DisconnectBeforeRound(f);
}
bool LeaveDeadGame()
{
var step = rejoiner.DeadAfterTeleport(DateTime.UtcNow, game.TabFails, game.TabFailsSince, Rejoins, barsSameSince);
if (step == Rejoiner.Step.Wait) return false;
game.Snapshot("game dead after teleport failed", 60);
return TakeRejoinStep(step);
}
void NoteBarsMoving(Header h, DateTime now)
{
var bars = new[] { h.Energy, h.EnergyMax, h.Stamina, h.StaminaMax, h.Health, h.HealthMax };
if (h.Energy < 0 || h.Stamina < 0 || h.Health < 0 || h.EnergyMax <= 0 || h.StaminaMax <= 0 || h.HealthMax <= 0) return;
if (h.Energy >= h.EnergyMax && h.Stamina >= h.StaminaMax && h.Health >= h.HealthMax) { barsSeen = null; barsSameSince = DateTime.MinValue; return; }
if (barsSeen == null || !bars.SequenceEqual(barsSeen)) { barsSeen = bars; barsSameSince = now; }
}
bool DisconnectBeforeRound(Frame f)
{
var d = game.ReadDisconnect(f);
dialogLookAt = Environment.TickCount;
if (d == null && !rejoiner.Active) eventWindow = !Game.EventPopupX(f).IsEmpty;
if (d == null) return false;
dialogUp = true;
if (!Offline(d)) { State(rejoiner.Status ?? Game.Disconnected); WatchRoblox(4000); }
return true;
}
void WatchRoblox(int ms)
{
int end = Environment.TickCount + ms;
do
{
if (game.Win.IsForeground && game.Win.UserActiveWithin(1000)) { robloxUsed = true; robloxUsedAt = DateTime.UtcNow; }
Sleep(Math.Max(1, Math.Min(250, end - Environment.TickCount)));
} while (!stop && end - Environment.TickCount > 0);
}
IntPtr readHandle;
bool readOk;
bool rejoinIdleSaid;
void GameReadable()
{
var now = DateTime.UtcNow;
dialogSeen = null;
robloxUsed = false;
disconnectSaid = false;
rejoinIdleSaid = false;
readHandle = game.Win.Handle;
readOk = true;
rejoiner.Readable(now);
RejoinNote();
if (focusBack != IntPtr.Zero)
{
if (Native.LastInputTick() == stepInput) game.Win.GiveFocusBack(focusBack);
focusBack = IntPtr.Zero;
}
if (game.Win.IsForeground && game.Win.UserActiveWithin(5000)) gameInput = now;
}
bool Offline(Game.Disconnect d)
{
var now = DateTime.UtcNow;
Rejoiner.Step step;
if (d != null)
{
string text = d.ToString();
if (text != dialogSeen)
{
Log((d.Teleport ? "Roblox shows its Teleport Failed window: " : "Roblox disconnected the game: ") + (text.Length > 0 ? text : "(its message didn't read)"));
SnapshotOnce((d.Teleport ? "teleport failed " : "disconnected ") + (d.Code >= 0 ? d.Code.ToString() : "no code"));
dialogSeen = text;
}
step = rejoiner.OnDialog(now, d, Rejoins, robloxUsed, TimeSpan.FromMinutes((int)S.OtherDeviceRejoin));
if (!disconnectSaid && !robloxUsed && !(d.Teleport && (step == Rejoiner.Step.Dismiss || rejoiner.Busy)))
{
Discord.Problem("disconnected", d.Teleport ? "⚠️ Roblox shows its Teleport Failed window" : "⚠️ Roblox disconnected the game", (text.Length > 0 ? text + "\n" : "") + DisconnectPlan(d), Shot, false, 10);
disconnectSaid = offlineSaid = true;
}
}
else
{
dialogSeen = null;
step = rejoiner.OnUnreadable(now, Rejoins, robloxUsed);
}
return TakeRejoinStep(step);
}
bool TakeRejoinStep(Rejoiner.Step step)
{
RejoinNote();
if (step == Rejoiner.Step.Wait) return false;
if (S.WaitWhileBusy && game.Win.UserActiveWithin(S.IdleSeconds * 1000))
{
State("Roblox disconnected the game - getting back in once you stop using the mouse/keyboard");
if (!rejoinIdleSaid) { Log("Roblox disconnected the game - getting back in once you've been idle for " + S.IdleSeconds + " s (you're using the PC)"); rejoinIdleSaid = true; }
int end = Environment.TickCount + 30000;
do WatchRoblox(500);
while (!stop && !robloxUsed && game.Win.UserActiveWithin(S.IdleSeconds * 1000) && end - Environment.TickCount > 0);
return true;
}
var now = DateTime.UtcNow;
if (step == Rejoiner.Step.Launch)
{
IntPtr front = Native.GetForegroundWindow();
rejoiner.Took(step, now);
robloxUsed = false;
try { RobloxWindow.StartGame(); }
catch (Exception e)
{
Log("Couldn't start Idle Mafia Game: " + e.Message);
rejoiner.Failed(now);
RejoinNote();
Sleep(4000);
return true;
}
Log("Starting Idle Mafia Game again");
stepInput = Native.LastInputTick();
focusBack = S.GiveFocusBack ? front : IntPtr.Zero;
game.ForgetView();
State("Starting Idle Mafia Game in Roblox...");
WatchRoblox(5000);
return true;
}
if (step == Rejoiner.Step.MenuLeave)
{
State("The game stopped answering - leaving it through Roblox's menu, to start it again");
if (Act(delegate { game.LeaveByMenu(); }))
{
rejoiner.Took(step, now);
robloxUsed = false;
stepInput = Native.LastInputTick();
game.ForgetView();
Log("Pressed Esc, L and Enter in Roblox (its menu's Leave) - the game is started again once Roblox has closed");
}
WatchRoblox(3000);
return true;
}
bool reconnect = step == Rejoiner.Step.Reconnect, dismiss = step == Rejoiner.Step.Dismiss, pressed = false;
State(dismiss ? "Roblox's Teleport Failed window - pressing Ok" : reconnect ? "Roblox disconnected the game - pressing Reconnect" : "Roblox disconnected the game - pressing Leave");
Act(delegate { pressed = dismiss ? game.PressTeleportOk() : game.PressDisconnect(reconnect, Rejoins && S.OtherDeviceRejoin != OtherDeviceWait.Off); });
if (pressed)
{
rejoiner.Took(step, now);
robloxUsed = false;
stepInput = Native.LastInputTick();
Log(dismiss ? "Pressed Ok in Roblox's Teleport Failed window (it only closes it)" : reconnect ? "Pressed Reconnect" : "Pressed Leave - the game is started again once Roblox has closed");
}
WatchRoblox(3000);
return true;
}
volatile string stopWhy;
DateTime awaySince = DateTime.MinValue, nextSummary, nextLevelPing;
string awaySaid;
volatile bool offlineSaid;
volatile bool awayOut;
bool disconnectSaid, awayYours;
public bool OutOfGame { get { return offlineSaid || awayOut; } }
public bool GameLeftToYou { get { return rejoiner.LeftToYou || robloxCheckUp; } }
volatile bool robloxCheckUp;
Counters summaryFrom;
int summaryLevel = -1, levelSaid = -1, levelSeen = -1, levelSaidXp = -1, rerollsSaid;
void DiscordStart()
{
var now = DateTime.UtcNow;
stopWhy = null;
awaySince = DateTime.MinValue; awaySaid = null; offlineSaid = disconnectSaid = awayOut = false;
nextSummary = now.AddHours(3); summaryFrom = Count.Copy(); summaryLevel = level;
levelSaid = levelSeen = levelSaidXp = -1; nextLevelPing = now;
rerollsSaid = Count.Rerolls;
}
string DisconnectPlan(Game.Disconnect d)
{
if (!rejoiner.Active) return "It was up before the bot started (or in another game), so the bot leaves it to you.";
if (d.OtherDevice && Rejoins && S.OtherDeviceRejoin != OtherDeviceWait.Off)
return "Your account joined the game on another device. The bot waits " + (int)S.OtherDeviceRejoin + " minutes, then gets back in (that throws the other device out).";
if (d.OtherDevice) return "Your account joined the game on another device, so the bot leaves Roblox alone.";
if (d.Kicked) return "Roblox says you were kicked. The bot doesn't rejoin after a kick, so start the game yourself.";
if (!Rejoins) return turns != null ? "Rejoining is off while the bot plays several accounts in turns, so it waits." : "Rejoining is off in the bot's settings, so it waits.";
if (d.Foreign) return "Roblox's window isn't in English, so the bot can't rejoin by itself. Set Roblox to English.";
if (d.Teleport) return "The bot closes it with its Ok and plays on.";
return "The bot gets back in by itself.";
}
byte[] Shot()
{
using (var f = game.Win.Capture()) return Discord.Jpg(f.Bitmap);
}
void Away(string key, string title, string text)
{
var now = DateTime.UtcNow;
if (awaySince == DateTime.MinValue || awaySaid != null && awaySaid != key || awayLogged != null && awayLogged != key)
{
awaySince = now; awaySaid = null; awayLogged = null;
awayYours = key == "closed" && now - robloxUsedAt < TimeSpan.FromMinutes(1);
}
if (now - awaySince >= TimeSpan.FromMinutes(5)) awayOut = true;
if (robloxUsed) return;
if (awayLogged == null && now - awaySince >= TimeSpan.FromMinutes(2))
{
awayLogged = key;
string since = awaySince.ToLocalTime().ToString("HH:mm");
Log(key == "closed" ? "Roblox isn't open since " + since + " - " + text : "Can't read the game since " + since + ": " + text);
if (key != "closed") game.Snapshot("game unreadable", 60);
}
if (awaySaid != null || now - awaySince < TimeSpan.FromMinutes(robloxCheckUp && key == "unreadable" ? 0 : 5)) return;
awaySaid = key;
if (awayYours) return;
offlineSaid = true;
Discord.Problem("away " + key, title, text, key == "closed" ? null : (Func<byte[]>)Shot, false, 60);
}
string awayLogged;
bool rejoinBackSaid;
void BackIn()
{
if (awayLogged != null && !rejoinBackSaid) Log("The game reads again, after " + Math.Max(1, (int)Math.Round((DateTime.UtcNow - awaySince).TotalMinutes)) + " minutes");
rejoinBackSaid = false;
if (awaySaid != null && offlineSaid) Discord.Problem("back", "✅ The bot can read the game again", "It plays on.", Shot, false, 0, Discord.Green);
if (awaySaid != null) offlineSaid = false;
awayOut = false;
awaySaid = awayLogged = null;
awaySince = DateTime.MinValue;
}
void RejoinNote()
{
string n = rejoiner.Note;
rejoiner.Note = null;
if (n == null) return;
Log(n);
if (n.StartsWith("Back in the game"))
{
rejoinBackSaid = true;
if (offlineSaid) Discord.Problem("back", "✅ " + n, "The bot plays on.", Shot, false, 0, Discord.Green);
offlineSaid = false;
}
else if (n.StartsWith("Couldn't get back")) Discord.Problem("rejoin failed", "⚠️ " + n, null, null, false, 30);
}
void LevelPing(int lv, int xpNext)
{
if (lv != levelSeen) { levelSeen = lv; return; }
if (levelSaid < 0) { levelSaid = lv; levelSaidXp = xpNext; return; }
if (lv == levelSaid && levelSaidXp <= 0) levelSaidXp = xpNext;
if (lv < levelSaid - 50) { levelSaid = lv; levelSaidXp = xpNext; return; }
if (lv <= levelSaid) return;
if (lv - levelSaid > 3 && !(xpNext > 0 && levelSaidXp > 0 && xpNext > levelSaidXp)) return;
if (!Discord.On(Ping.Level)) { levelSaid = lv; levelSaidXp = xpNext; return; }
if (DateTime.UtcNow < nextLevelPing) return;
Discord.Post(Ping.Level, "⬆️ Level " + lv, lv - levelSaid == 1 ? "Up from " + levelSaid + "." : "Up " + (lv - levelSaid) + " levels from " + levelSaid + ".", Shot, Discord.Purple);
levelSaid = lv;
levelSaidXp = xpNext;
nextLevelPing = DateTime.UtcNow.AddMinutes(10);
}
void PingHenchman(Game.CrewRow got, int replaced, bool perfect, IEnumerable<Game.CrewRow> crew)
{
if (got == null || got.Rarity >= Discord.RarityColors.Length) return;
if (!HenchmanNews(got, replaced, perfect, S.CrewReroll || RerollOnly, Game.BestRoll(RollNow()), crew)) return;
int took = Count.Rerolls - rerollsSaid;
rerollsSaid = Count.Rerolls;
Discord.Post(Ping.Rarity, "\U0001F3B2 " + (perfect ? "Perfect " : "") + got.RarityName + " henchman!",
got + (took > 1 ? " after " + took + " rerolls" : ""), Shot, Discord.RarityColors[got.Rarity]);
}
internal static bool HenchmanNews(Game.CrewRow got, int replaced, bool perfect, bool rerolls, int best, IEnumerable<Game.CrewRow> crew)
{
if (got == null || got.Rarity < (int)Rarity.Mythic) return false;
if (perfect) return true;
if (got.Rarity <= replaced) return false;
return !rerolls || got.Rarity >= best || (crew ?? Enumerable.Empty<Game.CrewRow>()).Any(r => !ReferenceEquals(r, got) && r.Rarity >= 0 && r.Rarity < got.Rarity);
}
void DiscordSummary()
{
var now = DateTime.UtcNow;
if (now < nextSummary) return;
nextSummary = now.AddHours(3);
var from = summaryFrom;
int fromLevel = summaryLevel;
summaryFrom = Count.Copy();
summaryLevel = level;
if (from == null || !Discord.On(Ping.Summary)) return;
var c = Count;
var lines = new List<string>();
if (level > 0) lines.Add("Level " + level + (fromLevel > 0 && level > fromLevel ? " (+" + (level - fromLevel) + ")" : ""));
if (last != null && last.Cash >= 0) lines.Add("Cash " + M(last.Cash) + (last.Banked >= 0 ? ", " + M(last.Banked) + " in the bank" : ""));
var did = new List<string>();
Action<int, string, string> add = (n, one, many) => { if (n > 0) did.Add(n + " " + (n == 1 ? one : many)); };
add(c.Jobs - from.Jobs, "job", "jobs");
add(c.Helps - from.Helps, "heist help", "heist helps");
int fights = c.Fights - from.Fights;
if (fights > 0) did.Add(fights + (fights == 1 ? " fight" : " fights") + " (" + (c.Wins - from.Wins) + " won)");
add(c.BossesBeaten - from.BossesBeaten, "boss beaten", "bosses beaten");
add(c.Properties - from.Properties, "property built", "properties built");
add(c.Hired - from.Hired, "hire", "hires");
add(c.Rerolls - from.Rerolls, "reroll", "rerolls");
add(c.Bought - from.Bought, "shop item", "shop items");
add(c.Crates - from.Crates, "crate", "crates");
add(c.OpsCollected - from.OpsCollected, "operation collected", "operations collected");
add(c.Rewards - from.Rewards, "reward", "rewards");
add(c.StaminaGiven - from.StaminaGiven, "stamina given to the family", "stamina given to the family");
lines.Add(did.Count > 0 ? "Last 3 hours: " + string.Join(", ", did) + "." : "Nothing done in the last 3 hours.");
Discord.Post(Ping.Summary, "\U0001F4CA 3-hour update", string.Join("\n", lines), Shot, Discord.Gold);
}
public event Action<FamilyInfo> FamilyRead;
FamilyInfo family = new FamilyInfo();
DateTime nextTakedown, nextGive, nextFamily;
DateTime freeGoneAt = DateTime.MinValue;
bool familyMissingSaid;
int takedownHealthCost = 25;
int takedownNoAttack;
int takedownNotTaken;
DateTime takedownHold = DateTime.MinValue;
DateTime takedownAwaySince = DateTime.MinValue;
int familyFails;
int familyShots;
string familyLogDay;
readonly HashSet<string> saidOnce = new HashSet<string>();
DateTime bossesReadAt = DateTime.MinValue, bossBackAt = DateTime.MaxValue;
bool bossesUp;
readonly RegenGuess energyRegen = new RegenGuess(), staminaRegen = new RegenGuess();
int takedownWants;
int givenSinceFights;
DateTime giveBrokenUntil = DateTime.MinValue;
int giveMissed;
void GiveMissed(string what)
{
int wait = ++giveMissed == 1 ? 10 : 60;
Log(what + " - tries again in " + wait + " minutes");
game.NoteFailure(what);
nextGive = giveBrokenUntil = DateTime.UtcNow.AddMinutes(wait);
}
void ResetFamily(DateTime now)
{
nextTakedown = nextGive = nextFamily = now;
familyMissingSaid = false;
familyFails = 0;
bossesReadAt = DateTime.MinValue;
bossBackAt = DateTime.MaxValue;
bossesUp = false;
takedownWants = givenSinceFights = giveMissed = 0;
giveBrokenUntil = DateTime.MinValue;
saidOnce.Clear();
}
void SayOnce(string key, string msg) { if (saidOnce.Add(key)) Log(msg); }
void FamilyChanged() { var h = FamilyRead; if (h != null) h(family.Copy()); }
void NoteBossRound(int stillUp, int soonest)
{
bossesReadAt = DateTime.UtcNow;
bossesUp = stillUp > 0;
bossBackAt = soonest < int.MaxValue ? DateTime.UtcNow.AddSeconds(soonest) : DateTime.MaxValue;
}
void NoteTicks(Header h)
{
if (h == null) return;
var now = DateTime.UtcNow;
energyRegen.Note(h.Energy, h.EnergyMax, h.EnergyTick, now);
staminaRegen.Note(h.Stamina, h.StaminaMax, h.StaminaTick, now);
}
internal static bool GuessBar(RegenGuess regen, DateTime now, ref int value, ref int max)
{
if (value >= 0) return true;
int g = regen.Guess(now);
if (g < 0) return false;
value = g; max = regen.Max;
return true;
}
int Kept(int max)
{
int k = Math.Max(0, S.FightStaminaReserve);
if (max <= 0 || k <= max / 2) return k;
SayOnce("kept half " + max, "Your stamina bar holds " + max + ", so the bot keeps half of it (" + max / 2 + ") for bosses instead of " + k);
return max / 2;
}
static int FightSpare(int max, int kept) { return max > 0 ? Math.Max(1, Math.Min(5, max - kept)) : 5; }
bool FamilyTakes()
{
return S.GiveStamina && Can(Tab.Family) && !family.NotInFamily && DateTime.UtcNow >= giveBrokenUntil;
}
bool FightsMayUse(Header h)
{
if (h == null || h.Stamina < 0) return false;
int kept = Kept(h.StaminaMax);
if (!FamilyTakes())
{
int need = FightSpare(h.StaminaMax, kept);
if (h.Stamina - kept >= need) return true;
SayOnce("fights wait " + kept + "/" + h.StaminaMax, "Fights wait until the stamina is " + need + " above the " + kept + " kept for bosses"
+ (h.StaminaMax > 0 ? " (your bar holds " + h.StaminaMax + ")" : ""));
return false;
}
return kept > 0 && h.Stamina >= kept && h.StaminaMax > 0 && givenSinceFights >= Math.Max(5, h.StaminaMax - kept);
}
bool familyOpened;
DateTime familyDoubtAt = DateTime.MinValue;
bool FamilyOpen(string sub, ref DateTime retry)
{
var nav = game.OpenFamily(sub);
if (nav == FamilyNav.Open)
{
familyFails = 0;
familyOpened = true;
familyDoubtAt = DateTime.MinValue;
if (family.NotInFamily) { family.NotInFamily = false; familyMissingSaid = false; FamilyChanged(); }
return true;
}
if (nav == FamilyNav.NotInFamily && familyOpened)
{
if (familyDoubtAt == DateTime.MinValue)
{
familyDoubtAt = DateTime.UtcNow;
Log("FAMILY shows no family of yours, but it did before - looking again in 30 minutes");
}
if (DateTime.UtcNow - familyDoubtAt < TimeSpan.FromMinutes(20))
{
retry = DateTime.UtcNow.AddMinutes(30);
return false;
}
familyOpened = false;
familyDoubtAt = DateTime.MinValue;
}
if (nav == FamilyNav.Failed && game.NotInMenu(Tab.Family)) { retry = DateTime.UtcNow.AddMinutes(30); return false; }
if (nav == FamilyNav.Failed && familyFails++ == 0) familyShots = game.Shots;
if (nav == FamilyNav.Failed && familyFails >= 5)
{
nextTakedown = nextGive = nextFamily = DateTime.UtcNow.AddHours(3);
familyFails = 0;
Log("The Family tab didn't open as expected 5 times in a row - the family tasks wait 3 hours" + (game.Shots > familyShots ? " (pictures are in the problems folder)" : ""));
return false;
}
if (nav == FamilyNav.NotInFamily)
{
nextTakedown = nextGive = nextFamily = DateTime.UtcNow.AddHours(6);
if (!familyMissingSaid)
{
familyMissingSaid = true;
Log("You're not in a family, so the family tasks (Takedown attacks, stamina perks, the family watch) wait - the bot looks again every 6 hours");
SnapshotOnce("family page without a family");
}
family.NotInFamily = true;
FamilyChanged();
return false;
}
retry = DateTime.UtcNow.AddMinutes(30);
return false;
}
void NoteTakedown(TakedownState t)
{
if (t == null || !t.Page) return;
family.Takedown = t;
family.TakedownAt = DateTime.Now;
FamilyChanged();
}
void NotePerks(List<PerkRow> rows)
{
if (rows == null || rows.Count == 0) return;
family.Perks = rows;
family.PerksAt = DateTime.Now;
FamilyChanged();
}
static string TakedownBlock(TakedownState t)
{
if (t == null || !t.Page) return "The Takedown page isn't showing (or didn't read) - no attacks this time";
if (t.Free < 0) return "Couldn't read the Takedown's free attacks - not attacking (without free ones ATTACK uses tickets or Robux)";
if (t.Free == 0) return "";
if (t.Segments <= 0) return string.Format("The Takedown's free attacks read as {0}/{1}, but their bar shows none - not attacking", t.Free, t.FreeMax);
if (t.Attack == null && !t.AttackBelow) return "Couldn't find the Takedown's ATTACK button - not attacking";
return null;
}
int takedownEvery = 60;
bool TakedownCrewHome()
{
var now = DateTime.UtcNow;
if (!Can(Tab.Crew) || !game.OpenTab(Tab.Crew)) { SayOnce("takedown crew not checked", "Takedown: the Crew page didn't open - attacking without the check for henchmen at a district"); return true; }
int recalled = game.RecallEveryone();
int away = recalled > 0 ? 1 : game.CrewAway();
if (away > 0)
{
if (takedownAwaySince == DateTime.MinValue) takedownAwaySince = now;
if (now - takedownAwaySince < TimeSpan.FromMinutes(30))
{
int wait = recalled > 0 ? 11 : 3;
nextTakedown = now.AddMinutes(wait);
takedownHold = nextTakedown.AddMinutes(10);
if (recalled > 0) SnapshotOnce("takedown crew recalled");
if (recalled > 0) Log("Takedown: recalled " + recalled + " henchm" + (recalled == 1 ? "an" : "en") + " from the districts (their gear adds nothing there) - the attacks wait until they're back");
else SayOnce("takedown crew away", "Takedown: henchmen are still away at a district or on their way back - the attacks wait for them");
return false;
}
Log("Takedown: henchmen still read as away after 30 minutes - attacking anyway");
game.Snapshot("takedown crew still away", 60);
}
takedownAwaySince = DateTime.MinValue;
saidOnce.Remove("takedown crew away");
if (S.EquipBest) DoEquip();
return true;
}
void DoTakedown()
{
takedownWants = 0;
nextTakedown = DateTime.UtcNow.AddMinutes(takedownEvery);
if (!FamilyOpen("TAKEDOWN", ref nextTakedown)) return;
var t = game.ReadTakedownSteady();
NoteTakedown(t);
if (t.Page && t.Free > 0 && freeGoneAt != DateTime.MinValue)
{
Log(string.Format("{0} free Takedown attack(s) came back in {1}", t.Free, SpanText((int)(DateTime.UtcNow - freeGoneAt).TotalSeconds)));
if (t.FreeMax > 0 && t.Free >= t.FreeMax && takedownEvery > 15)
{
takedownEvery = Math.Max(15, takedownEvery / 2);
Log("(They were all back, so the bot now looks every " + takedownEvery + " minutes)");
}
}
if (t.Page && t.Free > 0) freeGoneAt = DateTime.MinValue;
if (t.Page && t.Free >= 0 && t.Segments >= 0 && t.Segments != t.Free && saidOnce.Add("takedown bar"))
{
Log(string.Format("(The Takedown's free attacks read {0}/{1} while their bar shows {2} gold - keeping a picture)", t.Free, t.FreeMax, t.Segments));
SnapshotOnce("takedown count and bar differ");
}
var start = t;
int done = 0, hpBefore = -1, toastTries = 0;
bool waitShort = false, crewChecked = false, crewWait = false;
for (int i = 0; i < 20; i++)
{
string why = TakedownBlock(t);
if (why != null)
{
bool noAttack = t.Page && t.Free > 0 && t.Attack == null && !t.AttackBelow;
takedownNoAttack = noAttack ? takedownNoAttack + 1 : 0;
if (why.Length > 0) SayOnce("takedown: " + why, why);
if (noAttack && takedownNoAttack >= 3 && t.NewSeconds > 0)
{
nextTakedown = DateTime.UtcNow.AddSeconds(t.NewSeconds + 120);
takedownNoAttack = 0;
SayOnce("takedown finished", "Takedown: still no ATTACK after three looks - taken as finished, looked at again when the new one starts (in " + SpanText(t.NewSeconds) + ")");
}
else if (why.Length > 0 && (!noAttack || takedownNoAttack == 1)) game.Snapshot("takedown " + why, 60);
break;
}
takedownNoAttack = 0;
var bars = ReadBarsSettled(3000, x => x.Health >= 0 && x.Stamina >= 0);
int hp = t.Health >= 0 ? t.Health : bars.Health, hpMax = t.HealthMax > 0 ? t.HealthMax : bars.HealthMax;
if (hpBefore >= 0 && hp >= 0 && hpBefore > hp && hpMax > 0 && (hpBefore - hp) * 10 <= hpMax * 4) takedownHealthCost = hpBefore - hp;
bool costly = hp >= 0 && hpMax > 0 && (hp - takedownHealthCost) * 100 < LowHealthPct * hpMax;
if (hp >= 0 && hpMax > 0 && (hp * 100 < LowHealthPct * hpMax || costly))
{
Log("Health is low" + (costly ? " (" + hp + ", an attack takes about " + takedownHealthCost + ")" : "") + " - Takedown attacks wait until it comes back");
nextTakedown = DateTime.UtcNow.AddMinutes(20);
waitShort = true;
break;
}
if (bars.Stamina < 0 || bars.Stamina <= Kept(bars.StaminaMax))
{
if (bars.Stamina >= 0) takedownWants = Math.Max(1, t.Free);
if (bars.Stamina < 0 && game.RobloxBar && barFixTries < 2)
{
barFixTries++;
Log("Takedown: the stamina didn't read and Roblox's own title bar is over the top bar - the bot clears it first");
if (FixRobloxBarNow()) continue;
}
if (bars.Stamina < 0 && toastTries < 2 && game.CloseTerritoryToasts()) { toastTries++; continue; }
if (bars.Stamina < 0) { Log("Couldn't read stamina (" + game.BarLook(1) + ") - " + t.Free + " free Takedown attack(s) wait"); game.Snapshot("stamina unreadable at the takedown", 60); }
else SayOnce("takedown waits for stamina", "Takedown: " + t.Free + " free attack(s) wait for stamina above the " + Kept(bars.StaminaMax) + " kept for bosses and fights - they go before the stamina gifts");
nextTakedown = DateTime.UtcNow.AddMinutes(20);
waitShort = true;
break;
}
if (!crewChecked)
{
crewChecked = true;
if (!TakedownCrewHome()) { crewWait = waitShort = true; break; }
if (!FamilyOpen("TAKEDOWN", ref nextTakedown)) break;
t = game.ReadTakedownSteady();
NoteTakedown(t);
continue;
}
hpBefore = hp;
int free = t.Free;
if (!game.TakedownAttack(t))
{
const string notFound = "Couldn't find the Takedown's ATTACK button below the window's edge - not attacking";
SayOnce("takedown: " + notFound, notFound);
game.Snapshot("takedown attack not found below", 60);
break;
}
done++;
Count.TakedownAttacks++;
family.Attacks++;
if (done == 1) { game.Wait(600); SnapshotOnce("takedown right after an attack"); }
string said;
var after = game.WaitTakedownResult(free, text => { Log("A window came up over the Takedown: " + text); SnapshotOnce("takedown window"); }, out said);
if (!after.Page || after.Free < 0 || after.Free >= free)
{
Log(string.Format("The Takedown's free attacks didn't go down after ATTACK ({0} before, {1} now){2} - no more attacks this time",
free, !after.Page ? "page not read" : after.Free < 0 ? "unreadable" : after.Free.ToString(), said != null ? " - the game said \"" + Cut(said, 160) + "\"" : ""));
if (said != null && Parse.Has(said, "ALREADY FOUGHT FOR"))
{
done--; Count.TakedownAttacks--; family.Attacks--;
NoteTakedown(after);
nextTakedown = DateTime.UtcNow.AddSeconds(after.NewSeconds > 0 ? after.NewSeconds + 120 : 6 * 3600);
SayOnce("takedown other family", "Takedown: the game keeps you out until the new Takedown (you fought for your old family this week) - looking again "
+ (after.NewSeconds > 0 ? "when it starts (in " + SpanText(after.NewSeconds) + ")" : "in 6 hours"));
waitShort = true;
break;
}
game.Snapshot("takedown attack not counted", 60);
done--; Count.TakedownAttacks--; family.Attacks--;
NoteTakedown(after);
t = after;
if (after.Page && after.Free >= free && ++takedownNotTaken >= 2)
{
nextTakedown = DateTime.UtcNow.AddHours(6);
SayOnce("takedown not taken", "Takedown: the game took no ATTACK twice in a row (a family joined during this week's Takedown?) - looking again every 6 hours"
+ (after.NewSeconds > 0 ? " and when the new one starts (in " + SpanText(after.NewSeconds) + ")" : ""));
waitShort = true;
if (after.NewSeconds > 0 && DateTime.UtcNow.AddSeconds(after.NewSeconds + 120) < nextTakedown) nextTakedown = DateTime.UtcNow.AddSeconds(after.NewSeconds + 120);
}
break;
}
takedownNotTaken = 0;
t = game.ReadTakedownSteady();
if (t.Page && t.Free > 0 && t.Attack == null && !t.AttackBelow && game.CloseTakedownWindow(text => { Log("A window stayed over the Takedown: " + text); SnapshotOnce("takedown window"); }))
t = game.ReadTakedownSteady();
NoteTakedown(t);
}
if (done > 0)
{
var parts = new List<string> { string.Format("{0} free attack{1} used", done, done == 1 ? "" : "s") };
if (t.Page && t.Free >= 0) parts.Add(t.Free == 0 ? "none left" : t.Free + " of " + t.FreeMax + " left");
if (start.Damage >= 0 && t.Damage > start.Damage)
parts.Add(string.Format(CultureInfo.InvariantCulture, "+{0:N0} damage ({1:N0} this week{2})", t.Damage - start.Damage, t.Damage, t.Place.Length > 0 ? ", " + t.Place : ""));
if (start.Stage > 0 && t.Stage > start.Stage) parts.Add("now at stage " + t.Stage + (t.StageName.Length > 0 ? " (" + t.StageName + ")" : ""));
if (start.Health >= 0 && t.Health >= 0) parts.Add("health " + start.Health + " -> " + t.Health);
Log("Takedown: " + string.Join(", ", parts));
if (start.Damage >= 0 && t.Damage > start.Damage && t.Damage - start.Damage <= done) game.Snapshot("takedown one damage", 60, failure: false);
if (t.Page && t.Free == 0) freeGoneAt = DateTime.UtcNow;
}
if (!crewWait && takedownHold != DateTime.MinValue)
{
takedownHold = DateTime.MinValue;
if (defendName != null && S.DefendTerritory) { Log("Territory: the Takedown is done - back to defending " + defendName); Defend(); }
}
if (!t.Page || waitShort) return;
var now = DateTime.UtcNow;
if (t.Free == 0 && t.NextFreeSeconds > 0) nextTakedown = now.AddSeconds(Math.Max(120, Math.Min(3 * 3600, t.NextFreeSeconds + 30)));
if (t.Free == 0 && t.NextFreeSeconds < 0 && saidOnce.Add("takedown refill text"))
{
Log("(No free Takedown attacks left. Under the bar it says \"" + t.Under + "\". Looking again in an hour.)");
SnapshotOnce("takedown without free attacks");
}
if (t.NewSeconds > 0 && now.AddSeconds(t.NewSeconds + 120) < nextTakedown) nextTakedown = now.AddSeconds(t.NewSeconds + 120);
}
static string SpanText(int seconds)
{
var t = TimeSpan.FromSeconds(Math.Max(0, seconds));
return t.TotalHours >= 1 ? string.Format("{0}h {1:00}m", (int)t.TotalHours, t.Minutes) : string.Format("{0} min", Math.Max(1, t.Minutes));
}
int GiveReserve(int max)
{
if (max > 10 && ExpKeepsStamina(DateTime.UtcNow)) return Math.Max(Kept(max), max - 10);
return Kept(max);
}
internal static int GiveAtLeast(int max, int kept) { return Math.Max(5, Math.Min(15, (max - kept - 5) / 5 * 5)); }
string WhyNotGive(Header h)
{
if (h == null || h.Stamina < 0 || h.StaminaMax <= 0) return "stamina unreadable";
int kept = GiveReserve(h.StaminaMax);
int least = GiveAtLeast(h.StaminaMax, kept);
if (h.Stamina - kept < least) return "gives once stamina is " + least + " above the " + kept + " kept for bosses and fights (" + h.Stamina + " now)";
if (S.Takedown && takedownWants > 0 && Can(Tab.Family) && !ExpHoldsHealth(DateTime.UtcNow)) return "the Takedown's free attacks come first";
return null;
}
string giveWhySaid;
void NoteGiveWhy(Header h)
{
string why = !S.GiveStamina ? "" : family.NotInFamily ? "you're not in a family"
: DateTime.UtcNow < giveBrokenUntil ? "giving didn't work, tries again at " + giveBrokenUntil.ToLocalTime().ToString("HH:mm")
: WhyNotGive(h) ?? (BossDamageHold() ? "kept for the bosses during 2X boss damage"
: DateTime.UtcNow < nextGive ? "gave just now, gives again at " + nextGive.ToLocalTime().ToString("HH:mm") : "gives this round");
if (why == giveWhySaid) return;
giveWhySaid = why;
Info("givewhy", why);
}
PerkRow PickPerk(List<PerkRow> rows)
{
if (S.StaminaPerk.Trim().Length > 0)
{
var r = rows.FirstOrDefault(x => Game.SamePerk(x.Name, S.StaminaPerk));
if (r == null) SayOnce("perk missing", "The stamina perk \"" + S.StaminaPerk + "\" isn't on the Perks page - no stamina given (pick another on the Stamina page)");
else if (r.Maxed) { SayOnce("perk maxed", r.Name + " is at its top level - pick another stamina perk on the Stamina page"); r = null; }
return r;
}
var pick = rows.Where(x => x.Left > 0 && !x.Maxed && x.Level >= 0 && x.Name.Length > 0).OrderByDescending(x => x.GiveReady).ThenBy(x => x.Left).FirstOrDefault();
if (pick == null) SayOnce("perk none", "Couldn't read how far any stamina perk is from its next level - no stamina given");
return pick;
}
static string Progress(PerkRow r)
{
return r == null || r.Have < 0 ? "?" : string.Format(CultureInfo.InvariantCulture, "{0:N0} of {1:N0}", r.Have, r.Need);
}
void DoGiveStamina()
{
try { GiveStaminaHere(); }
finally
{
if (nextGive > DateTime.UtcNow.AddMinutes(20)) giveBrokenUntil = nextGive;
}
}
void GiveStaminaHere()
{
nextGive = DateTime.UtcNow.AddMinutes(5);
var h = ReadBarsSettled(3000);
bool open = false;
if (h.Stamina < 0)
{
if (!FamilyOpen("PERKS", ref nextGive)) return;
open = true;
h = ReadBarsSettled(6000);
if (h.Stamina < 0)
{
SayOnce("give stamina covered", "Give stamina: the stamina bar stayed covered by notifications - it tries again in 2 minutes");
SnapshotOnce("stamina bar covered at give stamina");
nextGive = DateTime.UtcNow.AddMinutes(2);
return;
}
}
if (WhyNotGive(h) != null) { Neutral(); return; }
if (!open && !FamilyOpen("PERKS", ref nextGive)) return;
var rows = game.WalkStaminaPerks();
if (rows.Count == 0)
{
game.Wait(1500);
if (!FamilyOpen("PERKS", ref nextGive)) return;
rows = game.WalkStaminaPerks();
}
NotePerks(rows);
if (rows.Count == 0)
{
game.Snapshot("stamina perks unreadable", 60);
GiveMissed("Couldn't read the stamina perks - no stamina given");
return;
}
var pick = PickPerk(rows);
if (pick == null) { nextGive = DateTime.UtcNow.AddMinutes(60); return; }
int reserve = GiveReserve(h.StaminaMax), stamina = h.Stamina, given = 0;
PerkRow row = null;
while (stamina - 5 >= reserve && given < 500)
{
row = (row != null ? game.PerkRowInSight(pick.Name) : null) ?? game.FindPerkRow(pick.Name);
if (row == null)
{
game.Snapshot("stamina perk button not found", 60);
GiveMissed("Couldn't find the GIVE 5 button of " + pick.Name + " - no more stamina given this time");
break;
}
if (row.Maxed) break;
long haveBefore = row.Have;
var pressedAt = DateTime.UtcNow;
game.GiveStamina(row);
PerkRow seen = null;
bool counted = false;
for (int look = 0; look < 3 && !counted; look++)
{
if (look > 0) game.Wait(500);
var r = game.PerkRowInSight(pick.Name, true);
if (r == null) continue;
seen = r;
counted = haveBefore >= 0 && r.Have >= haveBefore + 4 && r.Have <= haveBefore + 6;
if (!counted && ((row.Level > 0 && r.Level > row.Level) || (row.Need > 0 && r.Need > row.Need))) counted = true;
}
if (!counted)
{
string toast = game.ReadToast(800, t => Parse.Has(t, "GAVE") && Parse.Has(t, "STAMINA"));
counted = toast != null;
}
int left = stamina - 5;
if (!counted)
{
var after = ReadBarsSettled(6000, x => x.Stamina >= 0 && x.Stamina <= stamina - 4);
if (after.Stamina < 0)
{
Log("Gave stamina to " + pick.Name + " but couldn't see the 5 go (the stamina bar was covered) - giving again in 5 minutes");
break;
}
if (after.Stamina > stamina - 4)
{
if (game.ToastClosedSince(pressedAt, "SERVICES ARE BUSY") || game.ReadToast(300, t => Parse.Has(t, "SERVICES ARE BUSY")) != null)
{
Log("Gave stamina to " + pick.Name + ", but the game said its family services are busy - giving again in 5 minutes");
nextGive = DateTime.UtcNow.AddMinutes(5);
break;
}
game.Snapshot("give stamina not counted", 60);
GiveMissed(string.Format("GIVE 5 on {0} didn't take stamina ({1} before, {2} after)", pick.Name, stamina, after.Stamina));
break;
}
left = after.Stamina;
}
if (seen != null) row = seen;
if (given == 0) SnapshotOnce("stamina perk after GIVE 5");
giveMissed = 0;
given += 5;
stamina = left;
Count.StaminaGiven += 5;
family.StaminaGiven += 5;
givenSinceFights += 5;
}
if (given == 0) { FamilyChanged(); return; }
var now = game.PerkRowInSight(pick.Name) ?? row;
Log(string.Format("Gave {0} stamina to {1} ({2} -> {3}), stamina now {4}/{5}", given, pick.Name, Progress(pick), Progress(now), stamina, h.StaminaMax));
if (now != null) NotePerks(rows.Select(x => Game.SamePerk(x.Name, pick.Name) ? now : x).ToList());
if (nextGive < DateTime.UtcNow.AddMinutes(30)) { nextGive = DateTime.UtcNow.AddMinutes(5); giveBrokenUntil = DateTime.MinValue; }
}
void DoFamilyWatch()
{
nextFamily = DateTime.UtcNow.AddHours(3);
if (!FamilyOpen("OVERVIEW", ref nextFamily)) return;
var o = game.ReadFamilyOverviewAll();
if (o.Level < 0 && o.Name.Length == 0 && o.Xp < 0) { Log("Couldn't read the family's overview"); game.Snapshot("family overview unreadable", 60); }
else
{
var old = family.Overview;
if (old != null && old.Xp >= 0 && o.Xp >= 0 && old.XpNext == o.XpNext && o.Xp != old.Xp)
Log(string.Format(CultureInfo.InvariantCulture, "Family XP {0:+#,0;-#,0} since {1:HH:mm} ({2:N0} of {3:N0})", o.Xp - old.Xp, family.OverviewAt, o.Xp, o.XpNext));
family.Overview = o;
family.OverviewAt = DateTime.Now;
FamilyChanged();
}
var stale = DateTime.Now.AddHours(-6);
DateTime ignore = DateTime.MinValue;
if (family.TakedownAt < stale && FamilyOpen("TAKEDOWN", ref ignore)) NoteTakedown(game.ReadTakedownSteady());
if (family.PerksAt < stale && FamilyOpen("PERKS", ref ignore)) NotePerks(game.WalkStaminaPerks());
string day = DateTime.Now.ToString("yyyy-MM-dd");
if (familyLogDay != day) { familyLogDay = day; Log(FamilyLine()); }
}
string FamilyLine()
{
var ic = CultureInfo.InvariantCulture;
var parts = new List<string>();
var o = family.Overview;
if (o != null)
{
var p = new List<string> { (o.Name.Length > 0 ? o.Name : "your family") + (o.Tag.Length > 0 ? " [" + o.Tag + "]" : "") };
if (o.Level > 0) p.Add("level " + o.Level);
if (o.Xp >= 0) p.Add(string.Format(ic, "family XP {0:N0} of {1:N0}", o.Xp, o.XpNext));
if (o.Members >= 0) p.Add(o.Members + " of " + o.MembersMax + " members");
if (o.Respect >= 0) p.Add(o.Respect.ToString("N0", ic) + " respect");
if (o.PerkLevels >= 0) p.Add(o.PerkLevels + " of " + o.PerkLevelsMax + " perk levels");
if (o.Territories >= 0) p.Add(o.Territories + " of " + o.TerritoriesMax + " territories");
if (o.Vault >= 0) p.Add("vault " + CashText(o.Vault) + (o.Gold >= 0 ? " and " + o.Gold + " gold bars" : ""));
parts.Add(string.Join(", ", p));
}
var t = family.Takedown;
if (t != null)
{
var p = new List<string> { "Takedown" + (t.Name.Length > 0 ? " " + t.Name : "") };
if (t.Damage >= 0) p.Add(string.Format(ic, "your damage this week {0:N0}{1}", t.Damage, t.Place.Length > 0 ? " (" + t.Place + ")" : ""));
if (t.Free >= 0) p.Add(t.Free + " of " + t.FreeMax + " free attacks");
if (t.Stage > 0) p.Add(string.Format(ic, "stage {0}{1}", t.Stage, t.StageHpMax > 0 ? string.Format(ic, " ({0:N0} of {1:N0} health left)", t.StageHp, t.StageHpMax) : ""));
parts.Add(string.Join(", ", p));
}
if (family.Perks != null && family.Perks.Count > 0)
parts.Add("stamina perks: " + string.Join(", ", family.Perks.Select(r => r.Name + (r.Level > 0 ? " " + r.Level : "") + (r.Have >= 0 ? " (" + Progress(r) + ")" : ""))));
if (family.Attacks > 0 || family.StaminaGiven > 0) parts.Add(string.Format("this session: {0} Takedown attacks, {1} stamina given", family.Attacks, family.StaminaGiven));
return "Family: " + (parts.Count > 0 ? string.Join(" | ", parts) : "nothing read yet");
}
static string CashText(double v)
{
var ic = CultureInfo.InvariantCulture;
if (v >= 1e12) return "$" + (v / 1e12).ToString("0.##", ic) + "T";
if (v >= 1e9) return "$" + (v / 1e9).ToString("0.##", ic) + "B";
if (v >= 1e6) return "$" + (v / 1e6).ToString("0.##", ic) + "M";
return "$" + v.ToString("N0", ic);
}
void ScanFamily()
{
DateTime ignore = DateTime.MinValue;
if (!FamilyOpen("OVERVIEW", ref ignore)) { if (family.NotInFamily) Log("Family: you're not in a family"); return; }
family.Overview = game.ReadFamilyOverviewAll();
family.OverviewAt = DateTime.Now;
FamilyChanged();
if (FamilyOpen("TAKEDOWN", ref ignore))
{
var t = game.ReadTakedownSteady();
NoteTakedown(t);
string why = TakedownBlock(t);
Log(string.Format(" Takedown: free attacks {0} of {1} (bar {2} gold, under it \"{3}\"), tickets {4}, health {5} of {6} - {7}", t.Free, t.FreeMax, t.Segments, t.Under,
t.Tickets, t.Health, t.HealthMax, why == null ? (S.Takedown ? "would attack" : "would attack, but that's switched off") : why.Length == 0 ? "none left" : why));
}
if (FamilyOpen("PERKS", ref ignore))
{
var rows = game.WalkStaminaPerks();
NotePerks(rows);
foreach (var r in rows)
Log(string.Format(" stamina perk {0}: level {1} of {2}, {3}{4}", r.Name, r.Level, r.LevelMax, Progress(r), r.Left > 0 ? ", " + r.Left + " to go" : ""));
var pick = rows.Count > 0 ? PickPerk(rows) : null;
string why = WhyNotGive(last);
Log(" spare stamina: " + (pick == null ? "no perk to give to" : "would go to " + pick.Name) + " - " + (!S.GiveStamina ? "switched off" : why == null ? "would give now" : "not now: " + why));
}
Log(FamilyLine());
}
public event Action<Profile> ProfileRead;
public event Action<Profile> ProfileChanged;
void CheckGame()
{
Log("--- Checking your game: opening its tabs and reading them (it only opens tabs and rolls lists) ---");
State("Checking your game - about a minute");
var p = new Profile { Checked = DateTime.Now, Version = Program.Version };
var h = ReadBarsSettled(4000, x => x.Level > 0 && !string.IsNullOrWhiteSpace(x.Name) && x.Stamina >= 0);
p.Name = (h.Name ?? "").Trim();
if (S.Account.Length > 0 && (p.Name.Length == 0 || Accounts.Same(p.Name, S.Account))) p.Name = S.Account;
p.Level = h.Level > 0 ? h.Level : level;
p.Xp = h.Xp; p.XpNext = h.XpNext; p.SkillPoints = h.SkillPoints;
p.EnergyMax = h.EnergyMax; p.StaminaMax = h.StaminaMax; p.HealthMax = h.HealthMax;
p.Cash = h.Cash; p.Banked = h.Banked;
using (var f = game.Capture()) p.Window = f.Width + "x" + f.Height;
if (p.Name.Length == 0) p.Note("your name (the top bar)");
game.OnlyNavigate = true;
try
{
CheckPart(p, "the game's menu", () =>
{
var m = game.ReadWholeMenu();
if (m == null) { p.Note("the game's menu"); return; }
p.Tabs = string.Join(",", m.Seen);
p.MenuWhole = m.Whole && m.Unread == 0 ? 1 : 0;
if (m.Unread > 0) p.Note(m.Unread + " entries of the game's menu");
});
CheckPart(p, "the Safehouse", () =>
{
var s = game.ReadSafehouse();
if (s == null || s.Found == 0) { p.Note("the Safehouse"); return; }
p.SafeLevel = s.Level; p.SafeMax = s.MaxLevel; p.SafeBonus = s.Bonus;
p.Respect = s.Respect; p.AttackPower = s.AttackPower; p.DefensePower = s.DefensePower; p.Income = s.Income;
p.Hired = s.Hired; p.HireMax = s.HireMax; p.Gear = s.Gear; p.GearMax = s.GearMax; p.Capacity = s.Capacity;
if (s.Family != null) { p.Family = s.Family; p.InFamily = 1; }
p.Rank = Profile.NormalRank(s.Rank);
if (s.Members >= 0) { p.Members = s.Members; p.MembersMax = s.MembersMax; }
var sh = SafehouseRead;
if (sh != null) sh(s);
});
if (Open(p, Tab.Bank, "the Bank"))
CheckPart(p, "the Bank's fee", () =>
{
string line;
using (var f = game.Capture()) p.BankFee = game.ReadBankFee(f, out line);
p.BankLine = line;
if (p.BankFee >= 0) p.BankFeeFrom = "the Bank page";
});
if (Open(p, Tab.Operations, "Operations"))
CheckPart(p, "Operations", () =>
{
List<OpSlot> slots;
using (var f = game.Capture()) { slots = game.ReadOpSlots(f); p.OfflineOpsOffer = game.OffersOfflineOps(f) ? 1 : 0; }
p.OpsSlots = slots.Count;
p.OpsOpen = slots.Count(s => !s.Locked);
p.OpsPass = slots.Count(s => s.Pass);
p.OpsRunning = slots.Count(s => s.Running || s.Done || s.Collect != null);
var ops = OpsRead;
if (ops != null) ops(slots);
});
if (Open(p, Tab.Jobs, "Jobs"))
CheckPart(p, "the Jobs list", () =>
{
var j = game.ReadJobsList(Catalog);
p.CitiesSeen = string.Join(",", j.Cities.Select(i => GameData.Cities[i]));
p.CitiesFolded = string.Join(",", j.Folded.Select(i => GameData.Cities[i]));
p.JobCash = j.Cash;
if (game.LearnJobs(Catalog, 0, false) > 0) { GameData.Renumber(Catalog); SaveCatalog(); }
});
if (Open(p, Tab.Crew, "the Crew"))
CheckPart(p, "the Crew", () =>
{
Game.CrewTotals c;
using (var f = game.Capture()) c = game.ReadCrewTotals(f);
p.Henchmen = c.Henchmen; p.HenchSlots = c.Slots; p.CrewAttack = c.Attack; p.CrewDefense = c.Defense;
if (p.Henchmen < 0 && p.Hired >= 0) { p.Henchmen = p.Hired; p.HenchSlots = p.HireMax; }
});
if (p.Lacks(Tab.Family)) { if (p.InFamily < 0) p.InFamily = 0; }
else
CheckPart(p, "your family", () =>
{
var nav = game.OpenFamily("OVERVIEW");
if (nav == FamilyNav.NotInFamily || (nav == FamilyNav.Failed && game.NotInMenu(Tab.Family) && p.InFamily != 1))
{
p.InFamily = 0; p.Family = ""; p.Rank = "";
return;
}
if (nav != FamilyNav.Open) { p.Note("your family"); return; }
var o = game.ReadFamilyOverviewAll();
p.InFamily = 1;
if (o.Name.Length > 0) p.Family = o.Name + (o.Tag.Length > 0 ? " [" + o.Tag + "]" : "");
p.FamilyLevel = o.Level;
if (o.Members >= 0) { p.Members = o.Members; p.MembersMax = o.MembersMax; }
family.Overview = o;
family.OverviewAt = DateTime.Now;
FamilyChanged();
});
if (Open(p, Tab.Bosses, "Bosses"))
CheckPart(p, "the Bosses page", () =>
{
var b = game.ReadStageLook(p.Level);
if (b == null) { p.Note("the Bosses page"); return; }
p.BossStage = b.Stage; p.BossHighest = b.Highest; p.BossNeedLevel = b.NeedLevel; p.BossReady = b.Ready ? 1 : b.Cleared ? 0 : -1;
});
if (Open(p, Tab.Heists, "Heists"))
CheckPart(p, "Heists", () =>
{
game.HeistsTop();
using (var f = game.Capture()) { p.Favors = game.ReadFavors(f); p.FamilyHeists = game.ReadHeists(f).Count(r => r.YourFamily); }
if (p.Favors >= 0) Info("favors", p.Favors.ToString());
});
}
finally { game.OnlyNavigate = false; }
string file = Profile.FileFor(dir, p.Name);
try { p.Save(file); }
catch (Exception e) { Log("Couldn't save what the check found: " + e.Message); }
feeNotedFor = null;
foreach (var row in Setup.Found(p)) Log("Check - " + row[0] + ": " + row[1]);
#if LINUX
Log("--- Check finished (saved in " + System.IO.Path.GetFileName(file) + "). The Tasks page shows what it suggests. Nothing changes until you say so ---");
#else
Log("--- Check finished (saved in " + System.IO.Path.GetFileName(file) + "). The window shows what it suggests. Nothing changes until you say so ---");
#endif
var ev = ProfileRead;
if (ev != null) ev(p);
}
bool Open(Profile p, Tab t, string what)
{
if (p.Lacks(t)) return false;
bool ok = false;
CheckPart(p, what, () => { ok = game.OpenTab(t); if (!ok && !game.NotInMenu(t)) p.Note(what + " (the tab didn't open)"); });
return ok;
}
void CheckPart(Profile p, string what, Action read)
{
try { read(); }
catch (StopException) { throw; }
catch (UserBusyException) { throw; }
catch (WindowChangedException) { throw; }
catch (NeedUserException) { throw; }
catch (Exception e)
{
p.Note(what + " (" + e.Message + ")");
if (MemoryShort(e)) { Log("Checking your game: the PC ran out of memory (" + what + ")"); throw new OutOfMemoryPause(e); }
SnapshotOnce("check " + what);
}
}
void Scan()
{
Log("--- Scan: reading every screen, no actions ---");
if (last != null)
Log(string.Format("Level {0}, XP {1}/{2}, energy {3}/{4}, stamina {5}/{6}, health {7}/{8}, skill points {9}",
last.Level, last.Xp, last.XpNext, last.Energy, last.EnergyMax, last.Stamina, last.StaminaMax, last.Health, last.HealthMax, last.SkillPoints));
ReportTarget();
var job = PickJob(last);
if (job != null) Log("Job it would do now: " + job.Name + " (" + job.Cost + " energy)");
if (game.OpenTab(Tab.Heists))
{
game.HeistsTop();
using (var f = game.Capture())
{
Log("Heist favors left: " + game.ReadFavors(f));
foreach (var r in game.ReadHeists(f))
Log(string.Format(" heist \"{0}\": {1}, +{2} XP per help{3}{4}", r.Name, r.HelpText, r.XpPerHelp, r.YourFamily ? " (your family)" : "",
r.CanHelp && S.WouldHelp(r.YourFamily, r.XpPerHelp, JobXpPerEnergy() * HelpCost(r)) ? " <- would help" : ""));
}
}
if (game.OpenTab(Tab.Fight))
using (var f = game.Capture())
{
bool war;
var rows = game.ReadFightRows(f, out war);
Log("Fight list" + (war ? " (at war)" : "") + ": " + string.Join(" ", rows.Select(r => string.Format("#{0} {1} steal {2}% {3} stam{4}", r.Index + 1,
r.War ? "war target" : "lvl " + r.Level, r.Steal, r.StaminaCost, r.Ready ? "" : " (not ready)"))));
}
if (game.OpenTab(Tab.Operations))
using (var f = game.Capture())
{
var slotsSeen = game.ReadOpSlots(f);
var ops = OpsRead;
if (ops != null) ops(slotsSeen);
foreach (var s in slotsSeen)
Log(string.Format(" operation slot {0}: {1}", s.Index + 1, s.Locked ? "locked" : s.Collect != null || s.Done ? "READY to collect: " + s.Name
: s.Running ? s.Name + ", " + (s.SecondsLeft >= 0 ? TimeSpan.FromSeconds(s.SecondsLeft).ToString() + " left" : "running") : "free"));
}
if (game.OpenTab(Tab.Bosses))
{
var b = game.ReadStageLook(level);
Log(b == null ? "Bosses: the stages page didn't read"
: string.Format("Bosses: stage {0}{1}, highest stage {2}{3}{4}", b.Stage, b.Ready ? " (ATTACK shows)" : b.Cleared ? " (beaten today)" : "", b.Highest,
b.ResetSeconds >= 0 ? ", resets in " + Parse.Short(b.ResetSeconds) : "", b.NeedLevel > 0 ? ", the next locked stage needs level " + b.NeedLevel : ""));
}
ScanFamily();
if (game.OpenTab(Tab.Crew))
using (var f = game.Capture())
{
System.Drawing.Rectangle box;
string why;
var auto = game.FindAutoEquip(f, out box, out why);
Log("Crew: " + game.ReadCrewTotals(f) + "; AUTO EQUIP BEST " + (auto != null ? "found at " + (box.X + box.Width / 2) + "," + (box.Y + box.Height / 2) : "not pressable: " + why));
}
var info = game.ReadSafehouse();
if (info != null)
{
foreach (var line in info.Lines()) Log(line);
var h = SafehouseRead;
if (h != null && info.Found > 0) h(info);
}
Log("--- Scan finished ---");
}
}
}