Skip to content

Commit

Permalink
Improve stability
Browse files Browse the repository at this point in the history
  • Loading branch information
Citrinate committed May 24, 2024
1 parent 4da60ef commit cc29619
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 102 deletions.
3 changes: 2 additions & 1 deletion BoosterManager/Boosters/BoosterDequeueReason.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ internal enum BoosterDequeueReason {
RemovedByUser,
Uncraftable,
UnexpectedlyUncraftable,
Unmarketable
Unmarketable,
JobStopped
}
}
80 changes: 51 additions & 29 deletions BoosterManager/Boosters/BoosterJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Linq;
using ArchiSteamFarm.Steam;
using BoosterManager.Localization;
using SteamKit2.GC.Dota.Internal;

// Represents the state of a !booster command

Expand All @@ -15,6 +14,7 @@ internal sealed class BoosterJob {
internal StatusReporter StatusReporter;
private bool CreatedFromSaveState = false;
private readonly object LockObject = new();
private bool JobStopped = false;

private BoosterHandler BoosterHandler => BoosterHandler.BoosterHandlers[Bot.BotName];
private BoosterQueue BoosterQueue => BoosterHandler.BoosterQueue;
Expand Down Expand Up @@ -128,29 +128,6 @@ private void Start() {
BoosterQueue.AddBooster(gameID, this);
}

void OnBoosterInfosUpdated(Dictionary<uint, Steam.BoosterInfo> boosterInfos) {
try {
// At this point, all boosters that can be added to the queue have been
DateTime? lastBoosterCraftTime = LastBoosterCraftTime;
if (lastBoosterCraftTime == null) {
StatusReporter.Report(Bot, Strings.BoostersUncraftable, log: CreatedFromSaveState);
Finish();

return;
}

BoosterHandler.UpdateBoosterJobs();

if (lastBoosterCraftTime.Value.Date == DateTime.Today) {
StatusReporter.Report(Bot, String.Format(Strings.QueueStatusShort, NumBoosters, String.Format("{0:N0}", GemsNeeded), String.Format("{0:t}", lastBoosterCraftTime)), log: CreatedFromSaveState);
} else {
StatusReporter.Report(Bot, String.Format(Strings.QueueStatusShortWithDate, NumBoosters, String.Format("{0:N0}", GemsNeeded), String.Format("{0:d}", lastBoosterCraftTime), String.Format("{0:t}", lastBoosterCraftTime)), log: CreatedFromSaveState);
}
} finally {
BoosterQueue.OnBoosterInfosUpdated -= OnBoosterInfosUpdated;
}
}

BoosterQueue.OnBoosterInfosUpdated += OnBoosterInfosUpdated;
BoosterQueue.OnBoosterRemoved += OnBoosterRemoved;
BoosterQueue.Start();
Expand All @@ -164,6 +141,50 @@ internal void Finish() {
}
}

internal void Stop() {
JobStopped = true;
BoosterQueue.OnBoosterInfosUpdated -= OnBoosterInfosUpdated;
BoosterQueue.OnBoosterRemoved -= OnBoosterRemoved;

lock (LockObject) {
foreach (Booster booster in Boosters.Where(booster => !booster.WasCrafted)) {
BoosterQueue.RemoveBooster(booster.GameID, BoosterDequeueReason.JobStopped);
}
}
}

private void Update() {
if (JobStopped) {
return;
}

// Save the current state of this job
BoosterHandler.UpdateBoosterJobs();
}

void OnBoosterInfosUpdated(Dictionary<uint, Steam.BoosterInfo> boosterInfos) {
try {
// At this point, all boosters that can be added to the queue have been
DateTime? lastBoosterCraftTime = LastBoosterCraftTime;
if (lastBoosterCraftTime == null) {
StatusReporter.Report(Bot, Strings.BoostersUncraftable, log: CreatedFromSaveState);
Finish();

return;
}

Update();

if (lastBoosterCraftTime.Value.Date == DateTime.Today) {
StatusReporter.Report(Bot, String.Format(Strings.QueueStatusShort, NumBoosters, String.Format("{0:N0}", GemsNeeded), String.Format("{0:t}", lastBoosterCraftTime)), log: CreatedFromSaveState);
} else {
StatusReporter.Report(Bot, String.Format(Strings.QueueStatusShortWithDate, NumBoosters, String.Format("{0:N0}", GemsNeeded), String.Format("{0:d}", lastBoosterCraftTime), String.Format("{0:t}", lastBoosterCraftTime)), log: CreatedFromSaveState);
}
} finally {
BoosterQueue.OnBoosterInfosUpdated -= OnBoosterInfosUpdated;
}
}

internal void OnBoosterRemoved(Booster booster, BoosterDequeueReason reason) {
if (!(reason == BoosterDequeueReason.Crafted
// Currently we don't prevent user from queing a booster that already exists in the permanent booster job
Expand Down Expand Up @@ -203,7 +224,8 @@ internal void OnBoosterUnqueueable (uint gameID, BoosterDequeueReason reason) {
lock(LockObject) {
GameIDsToBooster.RemoveAll(x => x == gameID);
}
BoosterHandler.UpdateBoosterJobs();

Update();
}

internal void OnBoosterDequeued(Booster booster, BoosterDequeueReason reason) {
Expand All @@ -213,7 +235,7 @@ internal void OnBoosterDequeued(Booster booster, BoosterDequeueReason reason) {
Boosters.Remove(booster);
}
StatusReporter.Report(Bot, String.Format(Strings.BoosterUnexpectedlyUncraftable, booster.Info.Name, booster.GameID));
BoosterHandler.UpdateBoosterJobs();
Update();

return;
}
Expand All @@ -238,7 +260,7 @@ internal void OnBoosterDequeued(Booster booster, BoosterDequeueReason reason) {
}
}

BoosterHandler.UpdateBoosterJobs();
Update();

return;
}
Expand Down Expand Up @@ -283,7 +305,7 @@ internal bool RemoveUnqueuedBooster(uint gameID) {
removed = GameIDsToBooster.Remove(gameID);
}

BoosterHandler.UpdateBoosterJobs();
Update();

return removed;
}
Expand Down Expand Up @@ -319,7 +341,7 @@ internal List<uint> RemoveAllBoosters() {
}
}

BoosterHandler.UpdateBoosterJobs();
Update();

return gameIDsRemoved;
}
Expand Down
115 changes: 59 additions & 56 deletions BoosterManager/Boosters/BoosterQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Collections;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using BoosterManager.Localization;

namespace BoosterManager {
internal sealed class BoosterQueue : IDisposable {
internal sealed class BoosterQueue {
private readonly Bot Bot;
private readonly Timer Timer;
private readonly ConcurrentHashSet<Booster> Boosters = new(new BoosterComparer());
Expand All @@ -24,6 +25,7 @@ internal sealed class BoosterQueue : IDisposable {
private const int BoosterInfosUpdateBackOffMinMinutes = 1;
private const int BoosterInfosUpdateBackOffMaxMinutes = 15;
private float BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault;
private SemaphoreSlim RunSemaphore = new SemaphoreSlim(1, 1);

internal BoosterQueue(Bot bot) {
Bot = bot;
Expand All @@ -35,82 +37,83 @@ internal BoosterQueue(Bot bot) {
);
}

public void Dispose() {
Timer.Dispose();
}

internal void Start() {
UpdateTimer(DateTime.Now);
Utilities.InBackground(async() => await Run().ConfigureAwait(false));
}

private async Task Run() {
if (!Bot.IsConnectedAndLoggedOn) {
UpdateTimer(DateTime.Now.AddSeconds(1));

return;
}

// Reload the booster creator page
if (!await UpdateBoosterInfos().ConfigureAwait(false)) {
// Reload failed, try again later
Bot.ArchiLogger.LogGenericError(Strings.BoosterInfoUpdateFailed);
UpdateTimer(DateTime.Now.AddMinutes(Math.Min(BoosterInfosUpdateBackOffMaxMinutes, BoosterInfosUpdateBackOffMinMinutes * BoosterInfosUpdateBackOffMultiplier)));
BoosterInfosUpdateBackOffMultiplier += BoosterInfosUpdateBackOffMultiplierStep;
await RunSemaphore.WaitAsync().ConfigureAwait(false);
try {
if (!Bot.IsConnectedAndLoggedOn) {
UpdateTimer(DateTime.Now.AddSeconds(1));

return;
}

Booster? booster = GetNextCraftableBooster();
if (booster == null) {
// Booster queue is empty
BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault;
return;
}

return;
}

if (DateTime.Now >= booster.GetAvailableAtTime()) {
// Attempt to craft the next booster in the queue
if (booster.Info.Price > AvailableGems) {
// Not enough gems, wait until we get more gems
booster.BoosterJob.OnInsufficientGems(booster);
OnBoosterInfosUpdated += ForceUpdateBoosterInfos;
// Reload the booster creator page
if (!await UpdateBoosterInfos().ConfigureAwait(false)) {
// Reload failed, try again later
Bot.ArchiLogger.LogGenericError(Strings.BoosterInfoUpdateFailed);
UpdateTimer(DateTime.Now.AddMinutes(Math.Min(BoosterInfosUpdateBackOffMaxMinutes, BoosterInfosUpdateBackOffMinMinutes * BoosterInfosUpdateBackOffMultiplier)));
BoosterInfosUpdateBackOffMultiplier += BoosterInfosUpdateBackOffMultiplierStep;

return;
}

BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault;

if (!await CraftBooster(booster).ConfigureAwait(false)) {
// Craft failed, decide whether or not to remove this booster from the queue
Bot.ArchiLogger.LogGenericError(String.Format(Strings.BoosterCreationFailed, booster.GameID));
VerifyCraftBoosterError(booster);
UpdateTimer(DateTime.Now);
Booster? booster = GetNextCraftableBooster();
if (booster == null) {
// Booster queue is empty
BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault;

return;
}

if (DateTime.Now >= booster.GetAvailableAtTime()) {
// Attempt to craft the next booster in the queue
if (booster.Info.Price > AvailableGems) {
// Not enough gems, wait until we get more gems
booster.BoosterJob.OnInsufficientGems(booster);
OnBoosterInfosUpdated += ForceUpdateBoosterInfos;
UpdateTimer(DateTime.Now.AddMinutes(Math.Min(BoosterInfosUpdateBackOffMaxMinutes, BoosterInfosUpdateBackOffMinMinutes * BoosterInfosUpdateBackOffMultiplier)));
BoosterInfosUpdateBackOffMultiplier += BoosterInfosUpdateBackOffMultiplierStep;

Bot.ArchiLogger.LogGenericInfo(String.Format(Strings.BoosterCreationSuccess, booster.GameID));
RemoveBooster(booster.GameID, BoosterDequeueReason.Crafted);
return;
}

booster = GetNextCraftableBooster();
if (booster == null) {
// Queue has no more boosters in it
return;
BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault;

if (!await CraftBooster(booster).ConfigureAwait(false)) {
// Craft failed, decide whether or not to remove this booster from the queue
Bot.ArchiLogger.LogGenericError(String.Format(Strings.BoosterCreationFailed, booster.GameID));
VerifyCraftBoosterError(booster);
UpdateTimer(DateTime.Now);

return;
}

Bot.ArchiLogger.LogGenericInfo(String.Format(Strings.BoosterCreationSuccess, booster.GameID));
RemoveBooster(booster.GameID, BoosterDequeueReason.Crafted);

booster = GetNextCraftableBooster();
if (booster == null) {
// Queue has no more boosters in it
return;
}
}
}

BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault;
BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault;

// Wait until the next booster is ready to craft
DateTime nextBoosterTime = booster.GetAvailableAtTime();
if (nextBoosterTime < DateTime.Now.AddSeconds(MinDelayBetweenBoosters)) {
nextBoosterTime = DateTime.Now.AddSeconds(MinDelayBetweenBoosters);
}
// Wait until the next booster is ready to craft
DateTime nextBoosterTime = booster.GetAvailableAtTime();
if (nextBoosterTime < DateTime.Now.AddSeconds(MinDelayBetweenBoosters)) {
nextBoosterTime = DateTime.Now.AddSeconds(MinDelayBetweenBoosters);
}

UpdateTimer(nextBoosterTime);
Bot.ArchiLogger.LogGenericInfo(String.Format(Strings.NextBoosterCraft, String.Format("{0:T}", nextBoosterTime)));
UpdateTimer(nextBoosterTime);
Bot.ArchiLogger.LogGenericInfo(String.Format(Strings.NextBoosterCraft, String.Format("{0:T}", nextBoosterTime)));
} finally {
RunSemaphore.Release();
}
}

internal void AddBooster(uint gameID, BoosterJob boosterJob) {
Expand Down
9 changes: 7 additions & 2 deletions BoosterManager/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ internal static class Commands {
string twofacMessage = success ? message : String.Format(ArchiSteamFarm.Localization.Strings.WarningFailedWithError, message);

if (repeatMessage != null) {
return FormatBotResponse(bot, String.Format("{0}. {1}", twofacMessage, repeatMessage));
return FormatBotResponse(bot, String.Format("{0} {1}", twofacMessage, repeatMessage));
}

return FormatBotResponse(bot, twofacMessage);
Expand Down Expand Up @@ -1710,10 +1710,15 @@ internal static class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
}

if(bots.Any(bot => ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID) < EAccess.Master)) {
if (bots.Any(bot => ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID) < EAccess.Master)) {
return null;
}

Bot? offlineBot = bots.FirstOrDefault(bot => !bot.IsConnectedAndLoggedOn);
if (offlineBot != null) {
return FormatBotResponse(offlineBot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
}

// Parse GameIDs
string[] gameIDs = gameIDsAsText.Split(",", StringSplitOptions.RemoveEmptyEntries);

Expand Down
Loading

0 comments on commit cc29619

Please sign in to comment.