Skip to content

Commit c481fa5

Browse files
committed
Added booster^ command
#17
1 parent f95ed53 commit c481fa5

File tree

9 files changed

+210
-13
lines changed

9 files changed

+210
-13
lines changed

BoosterManager/Boosters/BoosterJob.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,5 +335,11 @@ internal int GetNumUnqueuedBoosters(uint gameID) {
335335
return GameIDsToBooster.Where(x => x == gameID).Count();
336336
}
337337
}
338+
339+
internal int GetNumBoosters(uint gameID) {
340+
lock(LockObject) {
341+
return (GetBooster(gameID) == null ? 0 : 1) + GetNumUnqueuedBoosters(gameID);
342+
}
343+
}
338344
}
339345
}

BoosterManager/Boosters/BoosterJobUtilities.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ internal static int GetNumUnqueuedBoosters(this IEnumerable<BoosterJob> jobs, ui
116116
return jobs.ToList().Sum(job => job.GetNumUnqueuedBoosters(gameID));
117117
}
118118

119+
internal static int GetNumBoosters(this IEnumerable<BoosterJob> jobs, uint gameID) {
120+
return jobs.ToList().Sum(job => job.GetNumBoosters(gameID));
121+
}
122+
119123
internal static DateTime? MaxDateTime(DateTime? a, DateTime? b) {
120124
if (a == null || b == null) {
121125
if (a == null && b == null) {

BoosterManager/Commands.cs

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,11 @@ internal static class Commands {
218218
case "BOOSTER":
219219
return ResponseBooster(bot, access, steamID, new StatusReporter(bot, steamID), args[1]);
220220

221+
case "BOOSTER^" when args.Length > 3:
222+
return ResponseSmartBooster(access, steamID, new StatusReporter(bot, steamID), args[1], args[2], Utilities.GetArgsAsText(args, 3, ","));
223+
case "BOOSTER^" when args.Length > 2:
224+
return ResponseSmartBooster(access, steamID, new StatusReporter(bot, steamID), bot.BotName, args[1], args[2]);
225+
221226
case "BOOSTERS" or "MBOOSTERS":
222227
return await ResponseCountItems(access, steamID, Utilities.GetArgsAsText(args, 1, ","), ItemIdentifier.BoosterIdentifier, marketable: true).ConfigureAwait(false);
223228
case "UBOOSTERS":
@@ -523,19 +528,19 @@ internal static class Commands {
523528
}
524529

525530
if (minutes == 0) {
526-
if ((acceptedType == Confirmation.EConfirmationType.Market && await MarketHandler.StopMarketRepeatTimer(bot).ConfigureAwait(false))
527-
|| (acceptedType == Confirmation.EConfirmationType.Trade && await InventoryHandler.StopTradeRepeatTimer(bot).ConfigureAwait(false))
531+
if ((acceptedType == Confirmation.EConfirmationType.Market && MarketHandler.StopMarketRepeatTimer(bot))
532+
|| (acceptedType == Confirmation.EConfirmationType.Trade && InventoryHandler.StopTradeRepeatTimer(bot))
528533
) {
529534
return FormatBotResponse(bot, Strings.RepetitionCancelled);
530535
} else {
531536
return FormatBotResponse(bot, Strings.RepetitionNotActive);
532537
}
533538
} else {
534539
if (acceptedType == Confirmation.EConfirmationType.Market) {
535-
await MarketHandler.StartMarketRepeatTimer(bot, minutes, statusReporter).ConfigureAwait(false);
540+
MarketHandler.StartMarketRepeatTimer(bot, minutes, statusReporter);
536541
repeatMessage = String.Format(Strings.RepetitionNotice, minutes, String.Format("!m2faok {0} 0", bot.BotName));
537542
} else if (acceptedType == Confirmation.EConfirmationType.Trade) {
538-
await InventoryHandler.StartTradeRepeatTimer(bot, minutes, statusReporter).ConfigureAwait(false);
543+
InventoryHandler.StartTradeRepeatTimer(bot, minutes, statusReporter);
539544
repeatMessage = String.Format(Strings.RepetitionNotice, minutes, String.Format("!t2faok {0} 0", bot.BotName));
540545
}
541546
}
@@ -1686,6 +1691,77 @@ internal static class Commands {
16861691
return await ResponseSendMultipleItemsToMultipleBots(sender, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(sender, access, steamID), recieverBotNames, amountsAsText, appIDAsText, contextIDAsText, itemIdentifiersAsText, marketable).ConfigureAwait(false);
16871692
}
16881693

1694+
private static string? ResponseSmartBooster(EAccess access, ulong steamID, StatusReporter craftingReporter, string botNames, string gameIDsAsText, string amountsAsText) {
1695+
if (String.IsNullOrEmpty(botNames)) {
1696+
throw new ArgumentNullException(nameof(botNames));
1697+
}
1698+
1699+
if (String.IsNullOrEmpty(gameIDsAsText)) {
1700+
throw new ArgumentNullException(nameof(gameIDsAsText));
1701+
}
1702+
1703+
if (String.IsNullOrEmpty(amountsAsText)) {
1704+
throw new ArgumentNullException(nameof(amountsAsText));
1705+
}
1706+
1707+
HashSet<Bot>? bots = Bot.GetBots(botNames);
1708+
1709+
if ((bots == null) || (bots.Count == 0)) {
1710+
return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
1711+
}
1712+
1713+
if(bots.Any(bot => ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID) < EAccess.Master)) {
1714+
return null;
1715+
}
1716+
1717+
// Parse GameIDs
1718+
string[] gameIDs = gameIDsAsText.Split(",", StringSplitOptions.RemoveEmptyEntries);
1719+
1720+
if (gameIDs.Length == 0) {
1721+
return FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(gameIDs)));
1722+
}
1723+
1724+
List<uint> gamesToBooster = new List<uint>();
1725+
1726+
foreach (string game in gameIDs) {
1727+
if (!uint.TryParse(game, out uint gameID) || (gameID == 0)) {
1728+
return FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.ErrorParsingObject, nameof(gameID)));
1729+
}
1730+
1731+
gamesToBooster.Add(gameID);
1732+
}
1733+
1734+
// Parse Amounts
1735+
string[] amountStrings = amountsAsText.Split(",", StringSplitOptions.RemoveEmptyEntries);
1736+
1737+
if (amountStrings.Length == 0) {
1738+
return FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(amountStrings)));
1739+
}
1740+
1741+
if (amountStrings.Length == 1 && gamesToBooster.Count > 1) {
1742+
amountStrings = Enumerable.Repeat(amountStrings[0], gamesToBooster.Count).ToArray();
1743+
}
1744+
1745+
if (amountStrings.Length != gamesToBooster.Count) {
1746+
return FormatStaticResponse(String.Format(Strings.AppIDCountDoesNotEqualAmountCount, gamesToBooster.Count, amountStrings.Length));
1747+
}
1748+
1749+
List<uint> amounts = new List<uint>();
1750+
foreach (string amount in amountStrings) {
1751+
if (!uint.TryParse(amount, out uint amountNum)) {
1752+
return FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.ErrorParsingObject, nameof(amountNum)));
1753+
}
1754+
1755+
amounts.Add(amountNum);
1756+
}
1757+
1758+
// Try to craft boosters
1759+
List<(uint, uint)> gameIDsWithAmounts = Zip(gamesToBooster, amounts).ToList();
1760+
BoosterHandler.GetBoosterInfos(bots, (boosterInfos) => BoosterHandler.SmartScheduleBoosters(BoosterJobType.Limited, bots, boosterInfos, gameIDsWithAmounts, craftingReporter));
1761+
1762+
return FormatStaticResponse(String.Format(Strings.BoosterAssignmentStarting, gameIDsWithAmounts.Sum(gameIDWithAmount => amounts.Sum(amount => amount))));
1763+
}
1764+
16891765
private static string? ResponseTradeCheck(Bot bot, EAccess access) {
16901766
if (access < EAccess.Master) {
16911767
return null;

BoosterManager/Handlers/BoosterHandler.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,86 @@ internal string ScheduleBoosters(BoosterJobType jobType, List<uint> gameIDs, Sta
4646
return Commands.FormatBotResponse(Bot, String.Format(Strings.BoosterCreationStarting, gameIDs.Count));
4747
}
4848

49+
internal static void SmartScheduleBoosters(BoosterJobType jobType, HashSet<Bot> bots, Dictionary<Bot, Dictionary<uint, Steam.BoosterInfo>> botBoosterInfos, List<(uint gameID, uint amount)> gameIDsWithAmounts, StatusReporter craftingReporter) {
50+
Dictionary<Bot, List<uint>> gameIDsToQueue = new();
51+
DateTime now = DateTime.Now;
52+
53+
// Figure out the most efficient way to queue the given boosters and amounts using the given bots
54+
foreach (var gameIDWithAmount in gameIDsWithAmounts) {
55+
uint gameID = gameIDWithAmount.gameID;
56+
uint amount = gameIDWithAmount.amount;
57+
58+
// Get all the data we need to determine which is the best bot for this booster
59+
Dictionary<Bot, (int numQueued, DateTime nextCraftTime)> botStates = new();
60+
foreach(Bot bot in bots) {
61+
if (!botBoosterInfos.TryGetValue(bot, out Dictionary<uint, Steam.BoosterInfo>? boosterInfos)) {
62+
continue;
63+
}
64+
65+
if (!boosterInfos.TryGetValue(gameID, out Steam.BoosterInfo? boosterInfo)) {
66+
continue;
67+
}
68+
69+
botStates.Add(bot, (BoosterHandlers[bot.BotName].Jobs.GetNumBoosters(gameID), boosterInfo.AvailableAtTime ?? now));
70+
}
71+
72+
if (botStates.Count == 0) {
73+
continue;
74+
}
75+
76+
for (int i = 0; i < amount; i++) {
77+
// Find the best bot for this booster
78+
Bot? bestBot = null;
79+
foreach(var botState in botStates) {
80+
if (bestBot == null) {
81+
bestBot = botState.Key;
82+
83+
continue;
84+
}
85+
86+
Bot bot = botState.Key;
87+
int numQueued = botState.Value.numQueued;
88+
DateTime nextCraftTime = botState.Value.nextCraftTime;
89+
90+
if (botStates[bestBot].nextCraftTime.AddDays(botStates[bestBot].numQueued) > nextCraftTime.AddDays(numQueued)) {
91+
bestBot = bot;
92+
}
93+
}
94+
95+
if (bestBot == null) {
96+
break;
97+
}
98+
99+
// Assign the booster to the best bot
100+
gameIDsToQueue.TryAdd(bestBot, new List<uint>());
101+
gameIDsToQueue[bestBot].Add(gameID);
102+
var bestBotState = botStates[bestBot];
103+
botStates[bestBot] = (bestBotState.numQueued + 1, bestBotState.nextCraftTime);
104+
}
105+
}
106+
107+
if (gameIDsToQueue.Count == 0) {
108+
foreach(Bot bot in bots) {
109+
craftingReporter.Report(bot, Strings.BoostersUncraftable);
110+
}
111+
112+
craftingReporter.ForceSend();
113+
114+
return;
115+
}
116+
117+
// Queue the boosters
118+
foreach (var item in gameIDsToQueue) {
119+
Bot bot = item.Key;
120+
List<uint> gameIDs = item.Value;
121+
122+
BoosterHandlers[bot.BotName].Jobs.Add(new BoosterJob(bot, jobType, gameIDs, craftingReporter));
123+
craftingReporter.Report(bot, String.Format(Strings.BoosterCreationStarting, gameIDs.Count));
124+
}
125+
126+
craftingReporter.ForceSend();
127+
}
128+
49129
internal string UnscheduleBoosters(HashSet<uint>? gameIDs = null, int? timeLimitHours = null) {
50130
List<uint> removedGameIDs = new List<uint>();
51131

@@ -213,5 +293,23 @@ internal void OnGemsRecieved() {
213293
BoosterQueue.OnBoosterInfosUpdated += BoosterQueue.ForceUpdateBoosterInfos;
214294
BoosterQueue.Start();
215295
}
296+
297+
internal static void GetBoosterInfos(HashSet<Bot> bots, Action<Dictionary<Bot, Dictionary<uint, Steam.BoosterInfo>>> callback) {
298+
Dictionary<Bot, Dictionary<uint, Steam.BoosterInfo>> boosterInfos = new();
299+
300+
foreach (Bot bot in bots) {
301+
void OnBoosterInfosUpdated(Dictionary<uint, Steam.BoosterInfo> boosterInfo) {
302+
BoosterHandlers[bot.BotName].BoosterQueue.OnBoosterInfosUpdated -= OnBoosterInfosUpdated;
303+
boosterInfos.Add(bot, boosterInfo);
304+
305+
if (boosterInfos.Count == bots.Count) {
306+
callback(boosterInfos);
307+
}
308+
}
309+
310+
BoosterHandlers[bot.BotName].BoosterQueue.OnBoosterInfosUpdated += OnBoosterInfosUpdated;
311+
BoosterHandlers[bot.BotName].BoosterQueue.Start();
312+
}
313+
}
216314
}
217315
}

BoosterManager/Handlers/InventoryHandler.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ internal static async Task<string> GetItemCount(Bot bot, uint appID, ulong conte
232232
return Commands.FormatBotResponse(bot, response);
233233
}
234234

235-
internal static async Task<bool> StopTradeRepeatTimer(Bot bot) {
235+
internal static bool StopTradeRepeatTimer(Bot bot) {
236236
if (!TradeRepeatTimers.ContainsKey(bot)) {
237237
return false;
238238
}
@@ -246,15 +246,15 @@ internal static async Task<bool> StopTradeRepeatTimer(Bot bot) {
246246
}
247247

248248
if (statusReporter != null) {
249-
await statusReporter.Send().ConfigureAwait(false);
249+
statusReporter.ForceSend();
250250
}
251251
}
252252

253253
return true;
254254
}
255255

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

259259
Timer newTimer = new Timer(async _ => await InventoryHandler.AcceptTradeConfirmations(bot, statusReporter).ConfigureAwait(false), null, Timeout.Infinite, Timeout.Infinite);
260260
if (TradeRepeatTimers.TryAdd(bot, (newTimer, statusReporter))) {

BoosterManager/Handlers/MarketHandler.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ internal static async Task<string> GetBuyLimit(Bot bot) {
279279
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()));
280280
}
281281

282-
internal static async Task<bool> StopMarketRepeatTimer(Bot bot) {
282+
internal static bool StopMarketRepeatTimer(Bot bot) {
283283
if (!MarketRepeatTimers.ContainsKey(bot)) {
284284
return false;
285285
}
@@ -293,15 +293,15 @@ internal static async Task<bool> StopMarketRepeatTimer(Bot bot) {
293293
}
294294

295295
if (statusReporter != null) {
296-
await statusReporter.Send().ConfigureAwait(false);
296+
statusReporter.ForceSend();
297297
}
298298
}
299299

300300
return true;
301301
}
302302

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

306306
Timer newTimer = new Timer(async _ => await MarketHandler.AcceptMarketConfirmations(bot, statusReporter).ConfigureAwait(false), null, Timeout.Infinite, Timeout.Infinite);
307307
if (MarketRepeatTimers.TryAdd(bot, (newTimer, statusReporter))) {

BoosterManager/Handlers/StatusReporter.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,11 @@ internal void Report(Bot reportingBot, string report, bool suppressDuplicateMess
9797
}
9898
}
9999

100-
internal async Task Send() {
100+
internal void ForceSend() {
101+
Utilities.InBackground(async() => await Send().ConfigureAwait(false));
102+
}
103+
104+
private async Task Send() {
101105
await ReportSemaphore.WaitAsync().ConfigureAwait(false);
102106
try {
103107
ReportTimer?.Dispose();

BoosterManager/Localization/Strings.resx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,4 +541,12 @@
541541
<value>Crafted {0}/{1} boosters. Crafting will finish on {2} at ~{3}, and will use {4} gems.</value>
542542
<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>
543543
</data>
544+
<data name="AppIDCountDoesNotEqualAmountCount" xml:space="preserve">
545+
<value>Number of appIDs ({0}) does not match number of item amounts ({1})</value>
546+
<comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
547+
</data>
548+
<data name="BoosterAssignmentStarting" xml:space="preserve">
549+
<value>Attempting to assign {0} boosters...</value>
550+
<comment>{0} will be replaced by a number of boosters</comment>
551+
</data>
544552
</root>

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Parameters in square brackets are sometimes `[Optional]`, parameters in angle br
2525
Command | Access | Description
2626
--- | --- | ---
2727
`booster [Bots] <AppIDs>`|`Master`|Adds `AppIDs` to the given bot's booster queue.
28+
`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.
2829
`bstatus [Bots]`|`Master`|Prints the status of the given bot's booster queue.
2930
`bstatus^ [Bots]`|`Master`|Prints a shortened status of the given bot's booster queue.
3031
`bstop [Bots] <AppIDs>`|`Master`|Removes `AppIDs` from the given bot's booster queue.

0 commit comments

Comments
 (0)