Skip to content

Commit

Permalink
Added booster^ command
Browse files Browse the repository at this point in the history
  • Loading branch information
Citrinate committed May 19, 2024
1 parent f95ed53 commit c481fa5
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 13 deletions.
6 changes: 6 additions & 0 deletions BoosterManager/Boosters/BoosterJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -335,5 +335,11 @@ internal int GetNumUnqueuedBoosters(uint gameID) {
return GameIDsToBooster.Where(x => x == gameID).Count();
}
}

internal int GetNumBoosters(uint gameID) {
lock(LockObject) {
return (GetBooster(gameID) == null ? 0 : 1) + GetNumUnqueuedBoosters(gameID);
}
}
}
}
4 changes: 4 additions & 0 deletions BoosterManager/Boosters/BoosterJobUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ internal static int GetNumUnqueuedBoosters(this IEnumerable<BoosterJob> jobs, ui
return jobs.ToList().Sum(job => job.GetNumUnqueuedBoosters(gameID));
}

internal static int GetNumBoosters(this IEnumerable<BoosterJob> jobs, uint gameID) {
return jobs.ToList().Sum(job => job.GetNumBoosters(gameID));
}

internal static DateTime? MaxDateTime(DateTime? a, DateTime? b) {
if (a == null || b == null) {
if (a == null && b == null) {
Expand Down
84 changes: 80 additions & 4 deletions BoosterManager/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ internal static class Commands {
case "BOOSTER":
return ResponseBooster(bot, access, steamID, new StatusReporter(bot, steamID), args[1]);

case "BOOSTER^" when args.Length > 3:
return ResponseSmartBooster(access, steamID, new StatusReporter(bot, steamID), args[1], args[2], Utilities.GetArgsAsText(args, 3, ","));
case "BOOSTER^" when args.Length > 2:
return ResponseSmartBooster(access, steamID, new StatusReporter(bot, steamID), bot.BotName, args[1], args[2]);

case "BOOSTERS" or "MBOOSTERS":
return await ResponseCountItems(access, steamID, Utilities.GetArgsAsText(args, 1, ","), ItemIdentifier.BoosterIdentifier, marketable: true).ConfigureAwait(false);
case "UBOOSTERS":
Expand Down Expand Up @@ -523,19 +528,19 @@ internal static class Commands {
}

if (minutes == 0) {
if ((acceptedType == Confirmation.EConfirmationType.Market && await MarketHandler.StopMarketRepeatTimer(bot).ConfigureAwait(false))
|| (acceptedType == Confirmation.EConfirmationType.Trade && await InventoryHandler.StopTradeRepeatTimer(bot).ConfigureAwait(false))
if ((acceptedType == Confirmation.EConfirmationType.Market && MarketHandler.StopMarketRepeatTimer(bot))
|| (acceptedType == Confirmation.EConfirmationType.Trade && InventoryHandler.StopTradeRepeatTimer(bot))
) {
return FormatBotResponse(bot, Strings.RepetitionCancelled);
} else {
return FormatBotResponse(bot, Strings.RepetitionNotActive);
}
} else {
if (acceptedType == Confirmation.EConfirmationType.Market) {
await MarketHandler.StartMarketRepeatTimer(bot, minutes, statusReporter).ConfigureAwait(false);
MarketHandler.StartMarketRepeatTimer(bot, minutes, statusReporter);
repeatMessage = String.Format(Strings.RepetitionNotice, minutes, String.Format("!m2faok {0} 0", bot.BotName));
} else if (acceptedType == Confirmation.EConfirmationType.Trade) {
await InventoryHandler.StartTradeRepeatTimer(bot, minutes, statusReporter).ConfigureAwait(false);
InventoryHandler.StartTradeRepeatTimer(bot, minutes, statusReporter);
repeatMessage = String.Format(Strings.RepetitionNotice, minutes, String.Format("!t2faok {0} 0", bot.BotName));
}
}
Expand Down Expand Up @@ -1686,6 +1691,77 @@ internal static class Commands {
return await ResponseSendMultipleItemsToMultipleBots(sender, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(sender, access, steamID), recieverBotNames, amountsAsText, appIDAsText, contextIDAsText, itemIdentifiersAsText, marketable).ConfigureAwait(false);
}

private static string? ResponseSmartBooster(EAccess access, ulong steamID, StatusReporter craftingReporter, string botNames, string gameIDsAsText, string amountsAsText) {
if (String.IsNullOrEmpty(botNames)) {
throw new ArgumentNullException(nameof(botNames));
}

if (String.IsNullOrEmpty(gameIDsAsText)) {
throw new ArgumentNullException(nameof(gameIDsAsText));
}

if (String.IsNullOrEmpty(amountsAsText)) {
throw new ArgumentNullException(nameof(amountsAsText));
}

HashSet<Bot>? bots = Bot.GetBots(botNames);

if ((bots == null) || (bots.Count == 0)) {
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)) {
return null;
}

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

if (gameIDs.Length == 0) {
return FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(gameIDs)));
}

List<uint> gamesToBooster = new List<uint>();

foreach (string game in gameIDs) {
if (!uint.TryParse(game, out uint gameID) || (gameID == 0)) {
return FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.ErrorParsingObject, nameof(gameID)));
}

gamesToBooster.Add(gameID);
}

// Parse Amounts
string[] amountStrings = amountsAsText.Split(",", StringSplitOptions.RemoveEmptyEntries);

if (amountStrings.Length == 0) {
return FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(amountStrings)));
}

if (amountStrings.Length == 1 && gamesToBooster.Count > 1) {
amountStrings = Enumerable.Repeat(amountStrings[0], gamesToBooster.Count).ToArray();
}

if (amountStrings.Length != gamesToBooster.Count) {
return FormatStaticResponse(String.Format(Strings.AppIDCountDoesNotEqualAmountCount, gamesToBooster.Count, amountStrings.Length));
}

List<uint> amounts = new List<uint>();
foreach (string amount in amountStrings) {
if (!uint.TryParse(amount, out uint amountNum)) {
return FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.ErrorParsingObject, nameof(amountNum)));
}

amounts.Add(amountNum);
}

// Try to craft boosters
List<(uint, uint)> gameIDsWithAmounts = Zip(gamesToBooster, amounts).ToList();
BoosterHandler.GetBoosterInfos(bots, (boosterInfos) => BoosterHandler.SmartScheduleBoosters(BoosterJobType.Limited, bots, boosterInfos, gameIDsWithAmounts, craftingReporter));

return FormatStaticResponse(String.Format(Strings.BoosterAssignmentStarting, gameIDsWithAmounts.Sum(gameIDWithAmount => amounts.Sum(amount => amount))));
}

private static string? ResponseTradeCheck(Bot bot, EAccess access) {
if (access < EAccess.Master) {
return null;
Expand Down
98 changes: 98 additions & 0 deletions BoosterManager/Handlers/BoosterHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,86 @@ internal string ScheduleBoosters(BoosterJobType jobType, List<uint> gameIDs, Sta
return Commands.FormatBotResponse(Bot, String.Format(Strings.BoosterCreationStarting, gameIDs.Count));
}

internal static void SmartScheduleBoosters(BoosterJobType jobType, HashSet<Bot> bots, Dictionary<Bot, Dictionary<uint, Steam.BoosterInfo>> botBoosterInfos, List<(uint gameID, uint amount)> gameIDsWithAmounts, StatusReporter craftingReporter) {
Dictionary<Bot, List<uint>> gameIDsToQueue = new();
DateTime now = DateTime.Now;

// Figure out the most efficient way to queue the given boosters and amounts using the given bots
foreach (var gameIDWithAmount in gameIDsWithAmounts) {
uint gameID = gameIDWithAmount.gameID;
uint amount = gameIDWithAmount.amount;

// Get all the data we need to determine which is the best bot for this booster
Dictionary<Bot, (int numQueued, DateTime nextCraftTime)> botStates = new();
foreach(Bot bot in bots) {
if (!botBoosterInfos.TryGetValue(bot, out Dictionary<uint, Steam.BoosterInfo>? boosterInfos)) {
continue;
}

if (!boosterInfos.TryGetValue(gameID, out Steam.BoosterInfo? boosterInfo)) {
continue;
}

botStates.Add(bot, (BoosterHandlers[bot.BotName].Jobs.GetNumBoosters(gameID), boosterInfo.AvailableAtTime ?? now));
}

if (botStates.Count == 0) {
continue;
}

for (int i = 0; i < amount; i++) {
// Find the best bot for this booster
Bot? bestBot = null;
foreach(var botState in botStates) {
if (bestBot == null) {
bestBot = botState.Key;

continue;
}

Bot bot = botState.Key;
int numQueued = botState.Value.numQueued;
DateTime nextCraftTime = botState.Value.nextCraftTime;

if (botStates[bestBot].nextCraftTime.AddDays(botStates[bestBot].numQueued) > nextCraftTime.AddDays(numQueued)) {
bestBot = bot;
}
}

if (bestBot == null) {
break;
}

// Assign the booster to the best bot
gameIDsToQueue.TryAdd(bestBot, new List<uint>());
gameIDsToQueue[bestBot].Add(gameID);
var bestBotState = botStates[bestBot];
botStates[bestBot] = (bestBotState.numQueued + 1, bestBotState.nextCraftTime);
}
}

if (gameIDsToQueue.Count == 0) {
foreach(Bot bot in bots) {
craftingReporter.Report(bot, Strings.BoostersUncraftable);
}

craftingReporter.ForceSend();

return;
}

// Queue the boosters
foreach (var item in gameIDsToQueue) {
Bot bot = item.Key;
List<uint> gameIDs = item.Value;

BoosterHandlers[bot.BotName].Jobs.Add(new BoosterJob(bot, jobType, gameIDs, craftingReporter));
craftingReporter.Report(bot, String.Format(Strings.BoosterCreationStarting, gameIDs.Count));
}

craftingReporter.ForceSend();
}

internal string UnscheduleBoosters(HashSet<uint>? gameIDs = null, int? timeLimitHours = null) {
List<uint> removedGameIDs = new List<uint>();

Expand Down Expand Up @@ -213,5 +293,23 @@ internal void OnGemsRecieved() {
BoosterQueue.OnBoosterInfosUpdated += BoosterQueue.ForceUpdateBoosterInfos;
BoosterQueue.Start();
}

internal static void GetBoosterInfos(HashSet<Bot> bots, Action<Dictionary<Bot, Dictionary<uint, Steam.BoosterInfo>>> callback) {
Dictionary<Bot, Dictionary<uint, Steam.BoosterInfo>> boosterInfos = new();

foreach (Bot bot in bots) {
void OnBoosterInfosUpdated(Dictionary<uint, Steam.BoosterInfo> boosterInfo) {
BoosterHandlers[bot.BotName].BoosterQueue.OnBoosterInfosUpdated -= OnBoosterInfosUpdated;
boosterInfos.Add(bot, boosterInfo);

if (boosterInfos.Count == bots.Count) {
callback(boosterInfos);
}
}

BoosterHandlers[bot.BotName].BoosterQueue.OnBoosterInfosUpdated += OnBoosterInfosUpdated;
BoosterHandlers[bot.BotName].BoosterQueue.Start();
}
}
}
}
8 changes: 4 additions & 4 deletions BoosterManager/Handlers/InventoryHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ internal static async Task<string> GetItemCount(Bot bot, uint appID, ulong conte
return Commands.FormatBotResponse(bot, response);
}

internal static async Task<bool> StopTradeRepeatTimer(Bot bot) {
internal static bool StopTradeRepeatTimer(Bot bot) {
if (!TradeRepeatTimers.ContainsKey(bot)) {
return false;
}
Expand All @@ -246,15 +246,15 @@ internal static async Task<bool> StopTradeRepeatTimer(Bot bot) {
}

if (statusReporter != null) {
await statusReporter.Send().ConfigureAwait(false);
statusReporter.ForceSend();
}
}

return true;
}

internal static async Task StartTradeRepeatTimer(Bot bot, uint minutes, StatusReporter? statusReporter) {
await StopTradeRepeatTimer(bot).ConfigureAwait(false);
internal static void StartTradeRepeatTimer(Bot bot, uint minutes, StatusReporter? statusReporter) {
StopTradeRepeatTimer(bot);

Timer newTimer = new Timer(async _ => await InventoryHandler.AcceptTradeConfirmations(bot, statusReporter).ConfigureAwait(false), null, Timeout.Infinite, Timeout.Infinite);
if (TradeRepeatTimers.TryAdd(bot, (newTimer, statusReporter))) {
Expand Down
8 changes: 4 additions & 4 deletions BoosterManager/Handlers/MarketHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ internal static async Task<string> GetBuyLimit(Bot bot) {
return Commands.FormatBotResponse(bot, String.Format(Strings.MarketBuyLimit, String.Format("{0:#,#0.00}", buyOrderValue / 100.0), String.Format("{0:#,#0.00}", buyOrderLimit / 100.0), String.Format("{0:0%}", buyOrderUsagePercent), String.Format("{0:#,#0.00}", remainingBuyOrderLimit / 100.0), bot.WalletCurrency.ToString()));
}

internal static async Task<bool> StopMarketRepeatTimer(Bot bot) {
internal static bool StopMarketRepeatTimer(Bot bot) {
if (!MarketRepeatTimers.ContainsKey(bot)) {
return false;
}
Expand All @@ -293,15 +293,15 @@ internal static async Task<bool> StopMarketRepeatTimer(Bot bot) {
}

if (statusReporter != null) {
await statusReporter.Send().ConfigureAwait(false);
statusReporter.ForceSend();
}
}

return true;
}

internal static async Task StartMarketRepeatTimer(Bot bot, uint minutes, StatusReporter? statusReporter) {
await StopMarketRepeatTimer(bot).ConfigureAwait(false);
internal static void StartMarketRepeatTimer(Bot bot, uint minutes, StatusReporter? statusReporter) {
StopMarketRepeatTimer(bot);

Timer newTimer = new Timer(async _ => await MarketHandler.AcceptMarketConfirmations(bot, statusReporter).ConfigureAwait(false), null, Timeout.Infinite, Timeout.Infinite);
if (MarketRepeatTimers.TryAdd(bot, (newTimer, statusReporter))) {
Expand Down
6 changes: 5 additions & 1 deletion BoosterManager/Handlers/StatusReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ internal void Report(Bot reportingBot, string report, bool suppressDuplicateMess
}
}

internal async Task Send() {
internal void ForceSend() {
Utilities.InBackground(async() => await Send().ConfigureAwait(false));
}

private async Task Send() {
await ReportSemaphore.WaitAsync().ConfigureAwait(false);
try {
ReportTimer?.Dispose();
Expand Down
8 changes: 8 additions & 0 deletions BoosterManager/Localization/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -541,4 +541,12 @@
<value>Crafted {0}/{1} boosters. Crafting will finish on {2} at ~{3}, and will use {4} gems.</value>
<comment>{0} will be replaced by a number of boosters, {1} will be replaced by a number of boosters, {2} will be replaced by a date, {3} will be replaced by a time, {4} will be replaced by a number of gems</comment>
</data>
<data name="AppIDCountDoesNotEqualAmountCount" xml:space="preserve">
<value>Number of appIDs ({0}) does not match number of item amounts ({1})</value>
<comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
</data>
<data name="BoosterAssignmentStarting" xml:space="preserve">
<value>Attempting to assign {0} boosters...</value>
<comment>{0} will be replaced by a number of boosters</comment>
</data>
</root>
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Parameters in square brackets are sometimes `[Optional]`, parameters in angle br
Command | Access | Description
--- | --- | ---
`booster [Bots] <AppIDs>`|`Master`|Adds `AppIDs` to the given bot's booster queue.
`booster^ [Bots] <AppIDs> <Amounts>`|`Master`|Adds `AppIDs` to some or all of given bot's booster queues, selected in a way to minimize the time it takes to craft a total `Amount` of boosters. The `Amounts` specified may be a single amount for all `AppIDs`, or multiple amounts for each `AppID` respectively.
`bstatus [Bots]`|`Master`|Prints the status of the given bot's booster queue.
`bstatus^ [Bots]`|`Master`|Prints a shortened status of the given bot's booster queue.
`bstop [Bots] <AppIDs>`|`Master`|Removes `AppIDs` from the given bot's booster queue.
Expand Down

0 comments on commit c481fa5

Please sign in to comment.