diff --git a/ArchiSteamFarm/Steam/Data/EAssetRarity.cs b/ArchiSteamFarm/Steam/Data/EAssetRarity.cs index 3b97ebf43d1e1..6427492bd4ba7 100644 --- a/ArchiSteamFarm/Steam/Data/EAssetRarity.cs +++ b/ArchiSteamFarm/Steam/Data/EAssetRarity.cs @@ -27,5 +27,11 @@ public enum EAssetRarity : byte { Unknown, Common, Uncommon, - Rare + Rare, + Mythical, + Legendary, + Ancient, + Immortal, + Arcana, + Unusual } diff --git a/ArchiSteamFarm/Steam/Data/InventoryDescription.cs b/ArchiSteamFarm/Steam/Data/InventoryDescription.cs index 243f75220dad2..7bfc15ad8ad3b 100644 --- a/ArchiSteamFarm/Steam/Data/InventoryDescription.cs +++ b/ArchiSteamFarm/Steam/Data/InventoryDescription.cs @@ -191,8 +191,8 @@ public EAssetRarity Rarity { } foreach (CEconItem_Tag? tag in Body.tags) { - switch (tag.category) { - case "droprate": + switch (tag.category.ToUpperInvariant()) { + case "DROPRATE": switch (tag.internal_name) { case "droprate_0": CachedRarity = EAssetRarity.Common; @@ -211,6 +211,51 @@ public EAssetRarity Rarity { CachedRarity = EAssetRarity.Unknown; + return CachedRarity.Value; + } + case "RARITY": + switch (tag.internal_name) { + case "common" or "Rarity_common" or "Rarity_Common_Weapon" or "Rarity_Common_Character": + CachedRarity = EAssetRarity.Common; + + return CachedRarity.Value; + case "uncommon" or "Rarity_uncommon" or "Rarity_Uncommon_Weapon" or "Rarity_Uncommon_Character": + CachedRarity = EAssetRarity.Uncommon; + + return CachedRarity.Value; + case "rare" or "Rarity_rare" or "Rarity_Rare_Weapon" or "Rarity_Rare_Character": + CachedRarity = EAssetRarity.Rare; + + return CachedRarity.Value; + case "epic" or "ultra_rare" or "Rarity_mythical" or "Rarity_Mythical_Weapon" or "Rarity_Mythical_Character": + CachedRarity = EAssetRarity.Mythical; + + return CachedRarity.Value; + case "exotic" or "legendary" or "Rarity_legendary" or "Rarity_Legendary_Weapon" or "Rarity_Legendary_Character": + CachedRarity = EAssetRarity.Legendary; + + return CachedRarity.Value; + case "extraordinary" or "Rarity_ancient" or "Rarity_Ancient_Weapon" or "Rarity_Ancient_Character": + CachedRarity = EAssetRarity.Ancient; + + return CachedRarity.Value; + case "Rarity_immortal" or "Rarity_Contraband_Weapon" or "Rarity_Contraband_Character": + CachedRarity = EAssetRarity.Immortal; + + return CachedRarity.Value; + case "Rarity_arcana": + CachedRarity = EAssetRarity.Arcana; + + return CachedRarity.Value; + case "Rarity_unusual": + CachedRarity = EAssetRarity.Unusual; + + return CachedRarity.Value; + default: + ASF.ArchiLogger.LogGenericError(Strings.FormatWarningUnknownValuePleaseReport(nameof(tag.internal_name), tag.internal_name)); + + CachedRarity = EAssetRarity.Unknown; + return CachedRarity.Value; } } diff --git a/ArchiSteamFarm/Steam/Interaction/Commands.cs b/ArchiSteamFarm/Steam/Interaction/Commands.cs index dca4418a36759..1cee618d706aa 100644 --- a/ArchiSteamFarm/Steam/Interaction/Commands.cs +++ b/ArchiSteamFarm/Steam/Interaction/Commands.cs @@ -46,6 +46,7 @@ namespace ArchiSteamFarm.Steam.Interaction; public sealed class Commands { private const ushort SteamTypingStatusDelay = 10 * 1000; // Steam client broadcasts typing status each 10 seconds + private const byte EAssetRarities = 10; // Currently, we recognize 10 EAssetRarity values private readonly Bot Bot; private readonly Dictionary CachedGamesOwned = new(); @@ -236,6 +237,10 @@ public static EAccess GetProxyAccess(Bot bot, EAccess access, ulong steamID = 0) return await ResponseAdvancedLoot(access, args[1], args[2], Utilities.GetArgsAsText(message, 3), steamID).ConfigureAwait(false); case "LOOT^" when args.Length > 2: return await ResponseAdvancedLoot(access, args[1], args[2]).ConfigureAwait(false); + case "LOOT&" when args.Length > 4: + return await ResponseAdvancedLootByAssetRarity(access, args[1], args[2], args[3], Utilities.GetArgsAsText(args, 4, ",")).ConfigureAwait(false); + case "LOOT&" when args.Length > 3: + return await ResponseAdvancedLootByAssetRarity(access, args[1], args[2], args[3]).ConfigureAwait(false); case "LOOT@" when args.Length > 2: return await ResponseLootByRealAppIDs(access, args[1], Utilities.GetArgsAsText(args, 2, ","), false, steamID).ConfigureAwait(false); case "LOOT@": @@ -318,6 +323,10 @@ public static EAccess GetProxyAccess(Bot bot, EAccess access, ulong steamID = 0) return await ResponseAdvancedTransfer(access, args[1], args[2], args[3], Utilities.GetArgsAsText(message, 4), steamID).ConfigureAwait(false); case "TRANSFER^" when args.Length > 3: return await ResponseAdvancedTransfer(access, args[1], args[2], args[3]).ConfigureAwait(false); + case "TRANSFER&" when args.Length > 5: + return await ResponseAdvancedTransferByAssetRarity(access, args[1], args[2], args[3], args[4], Utilities.GetArgsAsText(args, 5, ","), steamID).ConfigureAwait(false); + case "TRANSFER&" when args.Length > 4: + return await ResponseAdvancedTransferByAssetRarity(access, args[1], args[2], args[3], args[4]).ConfigureAwait(false); case "TRANSFER@" when args.Length > 3: return await ResponseTransferByRealAppIDs(access, args[1], args[2], Utilities.GetArgsAsText(message, 3), false, steamID).ConfigureAwait(false); case "TRANSFER@" when args.Length > 2: @@ -745,6 +754,78 @@ internal void OnNewLicenseList() { return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; } + private async Task ResponseAdvancedLootByAssetRarity(EAccess access, string targetAppID, string targetContextID, string assetRaritiesText) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + ArgumentException.ThrowIfNullOrEmpty(targetAppID); + ArgumentException.ThrowIfNullOrEmpty(targetContextID); + ArgumentException.ThrowIfNullOrEmpty(assetRaritiesText); + + if (access < EAccess.Master) { + return null; + } + + if (!Bot.IsConnectedAndLoggedOn) { + return FormatBotResponse(Strings.BotNotConnected); + } + + if (!uint.TryParse(targetAppID, out uint appID) || (appID == 0)) { + return FormatBotResponse(Strings.FormatErrorIsInvalid(nameof(appID))); + } + + if (!ulong.TryParse(targetContextID, out ulong contextID) || (contextID == 0)) { + return FormatBotResponse(Strings.FormatErrorIsInvalid(nameof(contextID))); + } + + string[] assetRaritiesArgs = assetRaritiesText.Split(SharedInfo.ListElementSeparators, EAssetRarities + 1, StringSplitOptions.RemoveEmptyEntries); + + switch (assetRaritiesArgs.Length) { + case 0: + return FormatBotResponse(Strings.FormatErrorIsEmpty(nameof(assetRaritiesArgs))); + case > EAssetRarities: + return FormatBotResponse(Strings.FormatErrorIsInvalid(nameof(assetRaritiesArgs))); + } + + HashSet assetRarities = []; + + foreach (string assetRarityArg in assetRaritiesArgs) { + if (!Enum.TryParse(assetRarityArg, true, out EAssetRarity assetRarity) || !Enum.IsDefined(assetRarity)) { + return FormatStaticResponse(Strings.FormatErrorIsInvalid(nameof(assetRarity))); + } + + assetRarities.Add(assetRarity); + } + + (bool success, string message) = await Bot.Actions.SendInventory(appID, contextID, filterFunction: item => assetRarities.Contains(item.Rarity)).ConfigureAwait(false); + + return FormatBotResponse(success ? message : Strings.FormatWarningFailedWithError(message)); + } + + private static async Task ResponseAdvancedLootByAssetRarity(EAccess access, string botNames, string appID, string contextID, string assetRaritiesText, ulong steamID = 0) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + ArgumentException.ThrowIfNullOrEmpty(botNames); + ArgumentException.ThrowIfNullOrEmpty(appID); + ArgumentException.ThrowIfNullOrEmpty(contextID); + ArgumentException.ThrowIfNullOrEmpty(assetRaritiesText); + + HashSet? bots = Bot.GetBots(botNames); + + if ((bots == null) || (bots.Count == 0)) { + return access >= EAccess.Owner ? FormatStaticResponse(Strings.FormatBotNotFound(botNames)) : null; + } + + IList results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseAdvancedLootByAssetRarity(GetProxyAccess(bot, access, steamID), appID, contextID, assetRaritiesText))).ConfigureAwait(false); + + List responses = [..results.Where(static result => !string.IsNullOrEmpty(result))!]; + + return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; + } + private async Task ResponseAdvancedRedeem(EAccess access, string options, string keys, ulong steamID = 0) { if (!Enum.IsDefined(access)) { throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); @@ -926,6 +1007,135 @@ internal void OnNewLicenseList() { return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; } + private async Task ResponseAdvancedTransferByAssetRarity(EAccess access, uint appID, ulong contextID, Bot targetBot, HashSet assetRarities) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + ArgumentOutOfRangeException.ThrowIfZero(appID); + ArgumentOutOfRangeException.ThrowIfZero(contextID); + ArgumentNullException.ThrowIfNull(targetBot); + + if (access < EAccess.Master) { + return null; + } + + if (!Bot.IsConnectedAndLoggedOn) { + return FormatBotResponse(Strings.BotNotConnected); + } + + if (!targetBot.IsConnectedAndLoggedOn) { + return FormatBotResponse(Strings.TargetBotNotConnected); + } + + (bool success, string message) = await Bot.Actions.SendInventory(appID, contextID, targetBot.SteamID, filterFunction: item => assetRarities.Contains(item.Rarity)).ConfigureAwait(false); + + return FormatBotResponse(success ? message : Strings.FormatWarningFailedWithError(message)); + } + + private async Task ResponseAdvancedTransferByAssetRarity(EAccess access, string targetAppID, string targetContextID, string botNameTo, string assetRaritiesText) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + ArgumentException.ThrowIfNullOrEmpty(targetAppID); + ArgumentException.ThrowIfNullOrEmpty(targetContextID); + ArgumentException.ThrowIfNullOrEmpty(botNameTo); + ArgumentException.ThrowIfNullOrEmpty(assetRaritiesText); + + Bot? targetBot = Bot.GetBot(botNameTo); + + if (targetBot == null) { + return access >= EAccess.Owner ? FormatBotResponse(Strings.FormatBotNotFound(botNameTo)) : null; + } + + if (!uint.TryParse(targetAppID, out uint appID) || (appID == 0)) { + return FormatBotResponse(Strings.FormatErrorIsInvalid(nameof(appID))); + } + + if (!ulong.TryParse(targetContextID, out ulong contextID) || (contextID == 0)) { + return FormatBotResponse(Strings.FormatErrorIsInvalid(nameof(contextID))); + } + + string[] assetRaritiesArgs = assetRaritiesText.Split(SharedInfo.ListElementSeparators, EAssetRarities + 1, StringSplitOptions.RemoveEmptyEntries); + + switch (assetRaritiesArgs.Length) { + case 0: + return FormatBotResponse(Strings.FormatErrorIsEmpty(nameof(assetRaritiesArgs))); + case > EAssetRarities: + return FormatBotResponse(Strings.FormatErrorIsInvalid(nameof(assetRaritiesArgs))); + } + + HashSet assetRarities = []; + + foreach (string assetRarityArg in assetRaritiesArgs) { + if (!Enum.TryParse(assetRarityArg, true, out EAssetRarity assetRarity) || !Enum.IsDefined(assetRarity)) { + return FormatStaticResponse(Strings.FormatErrorIsInvalid(nameof(assetRarity))); + } + + assetRarities.Add(assetRarity); + } + + return await ResponseAdvancedTransferByAssetRarity(access, appID, contextID, targetBot, assetRarities).ConfigureAwait(false); + } + + private static async Task ResponseAdvancedTransferByAssetRarity(EAccess access, string botNames, string targetAppID, string targetContextID, string botNameTo, string assetRaritiesText, ulong steamID = 0) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + ArgumentException.ThrowIfNullOrEmpty(botNames); + ArgumentException.ThrowIfNullOrEmpty(targetAppID); + ArgumentException.ThrowIfNullOrEmpty(targetContextID); + ArgumentException.ThrowIfNullOrEmpty(botNameTo); + ArgumentException.ThrowIfNullOrEmpty(assetRaritiesText); + + HashSet? bots = Bot.GetBots(botNames); + + if ((bots == null) || (bots.Count == 0)) { + return access >= EAccess.Owner ? FormatStaticResponse(Strings.FormatBotNotFound(botNames)) : null; + } + + if (!uint.TryParse(targetAppID, out uint appID) || (appID == 0)) { + return FormatStaticResponse(Strings.FormatErrorIsInvalid(nameof(appID))); + } + + if (!ulong.TryParse(targetContextID, out ulong contextID) || (contextID == 0)) { + return FormatStaticResponse(Strings.FormatErrorIsInvalid(nameof(contextID))); + } + + Bot? targetBot = Bot.GetBot(botNameTo); + + if (targetBot == null) { + return access >= EAccess.Owner ? FormatStaticResponse(Strings.FormatBotNotFound(botNameTo)) : null; + } + + string[] assetRaritiesArgs = assetRaritiesText.Split(SharedInfo.ListElementSeparators, EAssetRarities + 1, StringSplitOptions.RemoveEmptyEntries); + + switch (assetRaritiesArgs.Length) { + case 0: + return FormatStaticResponse(Strings.FormatErrorIsEmpty(nameof(assetRaritiesArgs))); + case > EAssetRarities: + return FormatStaticResponse(Strings.FormatErrorIsInvalid(nameof(assetRaritiesArgs))); + } + + HashSet assetRarities = []; + + foreach (string assetRarityArg in assetRaritiesArgs) { + if (!Enum.TryParse(assetRarityArg, true, out EAssetRarity assetRarity) || !Enum.IsDefined(assetRarity)) { + return FormatStaticResponse(Strings.FormatErrorIsInvalid(nameof(assetRarity))); + } + + assetRarities.Add(assetRarity); + } + + IList results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseAdvancedTransferByAssetRarity(GetProxyAccess(bot, access, steamID), appID, contextID, targetBot, assetRarities))).ConfigureAwait(false); + + List responses = [..results.Where(static result => !string.IsNullOrEmpty(result))!]; + + return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; + } + private string? ResponseBackgroundGamesRedeemer(EAccess access) { if (!Enum.IsDefined(access)) { throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));