diff --git a/ArchiSteamFarm/ASF.cs b/ArchiSteamFarm/ASF.cs index d006791c8..6d16d13c3 100644 --- a/ArchiSteamFarm/ASF.cs +++ b/ArchiSteamFarm/ASF.cs @@ -55,11 +55,11 @@ namespace ArchiSteamFarm { [PublicAPI] public static WebBrowser WebBrowser { get; internal set; } - private static readonly ConcurrentDictionary LastWriteEvents = new ConcurrentDictionary(); private static readonly SemaphoreSlim UpdateSemaphore = new SemaphoreSlim(1, 1); private static Timer AutoUpdatesTimer; private static FileSystemWatcher FileSystemWatcher; + private static ConcurrentDictionary LastWriteEvents; [PublicAPI] public static bool IsOwner(ulong steamID) { @@ -317,7 +317,7 @@ namespace ArchiSteamFarm { } private static void InitEvents() { - if (FileSystemWatcher != null) { + if ((FileSystemWatcher != null) || (LastWriteEvents != null)) { return; } @@ -328,6 +328,8 @@ namespace ArchiSteamFarm { FileSystemWatcher.Deleted += OnDeleted; FileSystemWatcher.Renamed += OnRenamed; + LastWriteEvents = new ConcurrentDictionary(Bot.BotsComparer); + FileSystemWatcher.EnableRaisingEvents = true; } diff --git a/ArchiSteamFarm/ArchiWebHandler.cs b/ArchiSteamFarm/ArchiWebHandler.cs index b536b531e..d95aa78c0 100644 --- a/ArchiSteamFarm/ArchiWebHandler.cs +++ b/ArchiSteamFarm/ArchiWebHandler.cs @@ -60,12 +60,12 @@ namespace ArchiSteamFarm { private static readonly SemaphoreSlim InventorySemaphore = new SemaphoreSlim(1, 1); - private static readonly ImmutableDictionary WebLimitingSemaphores = new Dictionary(4) { + private static readonly ImmutableDictionary WebLimitingSemaphores = new Dictionary(4, StringComparer.Ordinal) { { nameof(ArchiWebHandler), (new SemaphoreSlim(1, 1), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, { SteamCommunityURL, (new SemaphoreSlim(1, 1), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, { SteamStoreURL, (new SemaphoreSlim(1, 1), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, { WebAPI.DefaultBaseAddress.Host, (new SemaphoreSlim(1, 1), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) } - }.ToImmutableDictionary(); + }.ToImmutableDictionary(StringComparer.Ordinal); [PublicAPI] public readonly ArchiCacheable CachedApiKey; @@ -615,7 +615,7 @@ namespace ArchiSteamFarm { if (data != null) { data[sessionName] = sessionID; } else { - data = new Dictionary(1) { { sessionName, sessionID } }; + data = new Dictionary(1, StringComparer.Ordinal) { { sessionName, sessionID } }; } } @@ -727,7 +727,7 @@ namespace ArchiSteamFarm { if (data != null) { data[sessionName] = sessionID; } else { - data = new Dictionary(1) { { sessionName, sessionID } }; + data = new Dictionary(1, StringComparer.Ordinal) { { sessionName, sessionID } }; } } @@ -954,7 +954,7 @@ namespace ArchiSteamFarm { if (data != null) { data[sessionName] = sessionID; } else { - data = new Dictionary(1) { { sessionName, sessionID } }; + data = new Dictionary(1, StringComparer.Ordinal) { { sessionName, sessionID } }; } } @@ -995,7 +995,7 @@ namespace ArchiSteamFarm { const string request = "/gifts/0/resolvegiftcard"; // Extra entry for sessionID - Dictionary data = new Dictionary(3) { + Dictionary data = new Dictionary(3, StringComparer.Ordinal) { { "accept", "1" }, { "giftcardid", giftCardID.ToString() } }; @@ -1016,7 +1016,7 @@ namespace ArchiSteamFarm { string referer = SteamCommunityURL + "/tradeoffer/" + tradeID; // Extra entry for sessionID - Dictionary data = new Dictionary(3) { + Dictionary data = new Dictionary(3, StringComparer.Ordinal) { { "serverid", "1" }, { "tradeofferid", tradeID.ToString() } }; @@ -1036,7 +1036,7 @@ namespace ArchiSteamFarm { const string request = "/checkout/addfreelicense"; // Extra entry for sessionID - Dictionary data = new Dictionary(3) { + Dictionary data = new Dictionary(3, StringComparer.Ordinal) { { "action", "add_to_cart" }, { "subid", subID.ToString() } }; @@ -1064,7 +1064,7 @@ namespace ArchiSteamFarm { string request = profileURL + "/ajaxsetprivacy"; // Extra entry for sessionID - Dictionary data = new Dictionary(3) { + Dictionary data = new Dictionary(3, StringComparer.Ordinal) { { "eCommentPermission", ((byte) userPrivacy.CommentPermission).ToString() }, { "Privacy", JsonConvert.SerializeObject(userPrivacy.Settings) } }; @@ -1094,7 +1094,7 @@ namespace ArchiSteamFarm { string request = "/app/" + appID; // Extra entry for sessionID - Dictionary data = new Dictionary(2) { { "appid_to_clear_from_queue", appID.ToString() } }; + Dictionary data = new Dictionary(2, StringComparer.Ordinal) { { "appid_to_clear_from_queue", appID.ToString() } }; return await UrlPostWithSession(SteamStoreURL, request, data).ConfigureAwait(false); } @@ -1154,7 +1154,7 @@ namespace ArchiSteamFarm { const string request = "/explore/generatenewdiscoveryqueue"; // Extra entry for sessionID - Dictionary data = new Dictionary(2) { { "queuetype", "0" } }; + Dictionary data = new Dictionary(2, StringComparer.Ordinal) { { "queuetype", "0" } }; Steam.NewDiscoveryQueueResponse output = await UrlPostToJsonObjectWithSession(SteamStoreURL, request, data).ConfigureAwait(false); @@ -1983,7 +1983,7 @@ namespace ArchiSteamFarm { string request = "/gid/" + groupID; // Extra entry for sessionID - Dictionary data = new Dictionary(2) { { "action", "join" } }; + Dictionary data = new Dictionary(2, StringComparer.Ordinal) { { "action", "join" } }; return await UrlPostWithSession(SteamCommunityURL, request, data, session: ESession.CamelCase).ConfigureAwait(false); } @@ -2053,7 +2053,7 @@ namespace ArchiSteamFarm { const string requestValidateCode = "/account/validatewalletcode"; // Extra entry for sessionID - Dictionary data = new Dictionary(2) { { "wallet_code", key } }; + Dictionary data = new Dictionary(2, StringComparer.Ordinal) { { "wallet_code", key } }; Steam.RedeemWalletResponse responseValidateCode = await UrlPostToJsonObjectWithSession(SteamStoreURL, requestValidateCode, data).ConfigureAwait(false); @@ -2140,7 +2140,7 @@ namespace ArchiSteamFarm { const string referer = SteamCommunityURL + "/tradeoffer/new"; // Extra entry for sessionID - Dictionary data = new Dictionary(6) { + Dictionary data = new Dictionary(6, StringComparer.Ordinal) { { "partner", partnerID.ToString() }, { "serverid", "1" }, { "trade_offer_create_params", string.IsNullOrEmpty(token) ? "" : new JObject { { "trade_offer_access_token", token } }.ToString(Formatting.None) }, @@ -2184,7 +2184,7 @@ namespace ArchiSteamFarm { string request = profileURL + "/ajaxunpackbooster"; // Extra entry for sessionID - Dictionary data = new Dictionary(3) { + Dictionary data = new Dictionary(3, StringComparer.Ordinal) { { "appid", appID.ToString() }, { "communityitemid", itemID.ToString() } }; @@ -2429,7 +2429,7 @@ namespace ArchiSteamFarm { const string request = "/dev/registerkey"; // Extra entry for sessionID - Dictionary data = new Dictionary(4) { + Dictionary data = new Dictionary(4, StringComparer.Ordinal) { { "agreeToTerms", "agreed" }, { "domain", "localhost" }, { "Submit", "Register" } @@ -2605,7 +2605,7 @@ namespace ArchiSteamFarm { return false; } - Dictionary data = new Dictionary(1) { { "pin", parentalCode } }; + Dictionary data = new Dictionary(1, StringComparer.Ordinal) { { "pin", parentalCode } }; // This request doesn't go through UrlPostRetryWithSession as we have no access to session refresh capability (this is in fact session initialization) WebBrowser.BasicResponse response = await WebLimitRequest(serviceURL, async () => await WebBrowser.UrlPost(serviceURL + request, data, serviceURL).ConfigureAwait(false)).ConfigureAwait(false); diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index 92ee60252..c34143101 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -1536,12 +1536,12 @@ namespace ArchiSteamFarm { } if (!File.Exists(filePath)) { - return new Dictionary(0); + return new Dictionary(0, StringComparer.Ordinal); } - try { - Dictionary keys = new Dictionary(); + Dictionary keys = new Dictionary(StringComparer.Ordinal); + try { using (StreamReader reader = new StreamReader(filePath)) { string line; @@ -1570,13 +1570,13 @@ namespace ArchiSteamFarm { keys[key] = name; } } - - return keys; } catch (Exception e) { ArchiLogger.LogGenericException(e); return null; } + + return keys; } private void HandleCallbacks() { diff --git a/ArchiSteamFarm/Collections/ConcurrentHashSet.cs b/ArchiSteamFarm/Collections/ConcurrentHashSet.cs index 8a94b6d6a..7b92e4a50 100644 --- a/ArchiSteamFarm/Collections/ConcurrentHashSet.cs +++ b/ArchiSteamFarm/Collections/ConcurrentHashSet.cs @@ -19,6 +19,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; @@ -31,7 +32,17 @@ namespace ArchiSteamFarm.Collections { public int Count => BackingCollection.Count; public bool IsReadOnly => false; - private readonly ConcurrentDictionary BackingCollection = new ConcurrentDictionary(); + private readonly ConcurrentDictionary BackingCollection; + + public ConcurrentHashSet() => BackingCollection = new ConcurrentDictionary(); + + public ConcurrentHashSet([NotNull] IEqualityComparer comparer) { + if (comparer == null) { + throw new ArgumentNullException(nameof(comparer)); + } + + BackingCollection = new ConcurrentDictionary(comparer); + } public bool Add(T item) => BackingCollection.TryAdd(item, true); public void Clear() => BackingCollection.Clear(); diff --git a/ArchiSteamFarm/Commands.cs b/ArchiSteamFarm/Commands.cs index cfb46f10d..33fd0c90d 100644 --- a/ArchiSteamFarm/Commands.cs +++ b/ArchiSteamFarm/Commands.cs @@ -2114,8 +2114,8 @@ namespace ArchiSteamFarm { bool distribute = !redeemFlags.HasFlag(ERedeemFlags.SkipDistributing) && (redeemFlags.HasFlag(ERedeemFlags.ForceDistributing) || Bot.BotConfig.RedeemingPreferences.HasFlag(BotConfig.ERedeemingPreferences.Distributing)); bool keepMissingGames = !redeemFlags.HasFlag(ERedeemFlags.SkipKeepMissingGames) && (redeemFlags.HasFlag(ERedeemFlags.ForceKeepMissingGames) || Bot.BotConfig.RedeemingPreferences.HasFlag(BotConfig.ERedeemingPreferences.KeepMissingGames)); - HashSet pendingKeys = keys.ToHashSet(); - HashSet unusedKeys = pendingKeys.ToHashSet(); + HashSet pendingKeys = keys.ToHashSet(StringComparer.Ordinal); + HashSet unusedKeys = pendingKeys.ToHashSet(StringComparer.Ordinal); HashSet rateLimitedBots = new HashSet(); diff --git a/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs b/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs index 76c12ecc5..661da77f1 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs @@ -73,7 +73,7 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { if ((ASF.GlobalConfig.AdditionalProperties != null) && (ASF.GlobalConfig.AdditionalProperties.Count > 0)) { if (request.GlobalConfig.AdditionalProperties == null) { - request.GlobalConfig.AdditionalProperties = new Dictionary(ASF.GlobalConfig.AdditionalProperties.Count); + request.GlobalConfig.AdditionalProperties = new Dictionary(ASF.GlobalConfig.AdditionalProperties.Count, StringComparer.Ordinal); } foreach ((string key, JToken value) in ASF.GlobalConfig.AdditionalProperties.Where(property => !request.GlobalConfig.AdditionalProperties.ContainsKey(property.Key))) { diff --git a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs index 9839f6792..8db91b6b8 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs @@ -74,7 +74,7 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { return BadRequest(new GenericResponse>(false, string.Format(Strings.ErrorIsInvalid, nameof(bots)))); } - return Ok(new GenericResponse>(bots.ToDictionary(bot => bot.BotName, bot => bot))); + return Ok(new GenericResponse>(bots.ToDictionary(bot => bot.BotName, bot => bot, Bot.BotsComparer))); } /// @@ -99,9 +99,9 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { request.BotConfig.ShouldSerializeEverything = false; request.BotConfig.ShouldSerializeHelperProperties = false; - HashSet bots = botNames.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Where(botName => botName != SharedInfo.ASF).ToHashSet(); + HashSet bots = botNames.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Where(botName => botName != SharedInfo.ASF).ToHashSet(Bot.BotsComparer); - Dictionary result = new Dictionary(bots.Count); + Dictionary result = new Dictionary(bots.Count, Bot.BotsComparer); foreach (string botName in bots) { if (Bot.Bots.TryGetValue(botName, out Bot bot)) { @@ -119,7 +119,7 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { if ((bot.BotConfig.AdditionalProperties != null) && (bot.BotConfig.AdditionalProperties.Count > 0)) { if (request.BotConfig.AdditionalProperties == null) { - request.BotConfig.AdditionalProperties = new Dictionary(bot.BotConfig.AdditionalProperties.Count); + request.BotConfig.AdditionalProperties = new Dictionary(bot.BotConfig.AdditionalProperties.Count, StringComparer.Ordinal); } foreach ((string key, JToken value) in bot.BotConfig.AdditionalProperties.Where(property => !request.BotConfig.AdditionalProperties.ContainsKey(property.Key))) { @@ -187,7 +187,7 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { IList<(Dictionary UnusedKeys, Dictionary UsedKeys)> results = await Utilities.InParallel(bots.Select(bot => bot.GetUsedAndUnusedKeys())).ConfigureAwait(false); - Dictionary result = new Dictionary(bots.Count); + Dictionary result = new Dictionary(bots.Count, Bot.BotsComparer); foreach (Bot bot in bots) { (Dictionary unusedKeys, Dictionary usedKeys) = results[result.Count]; @@ -228,7 +228,7 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { await Utilities.InParallel(bots.Select(bot => bot.AddGamesToRedeemInBackground(validGamesToRedeemInBackground))).ConfigureAwait(false); - Dictionary result = new Dictionary(bots.Count); + Dictionary result = new Dictionary(bots.Count, Bot.BotsComparer); foreach (Bot bot in bots) { result[bot.BotName] = validGamesToRedeemInBackground; @@ -290,12 +290,12 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { IList results = await Utilities.InParallel(bots.Select(bot => request.KeysToRedeem.Select(key => bot.Actions.RedeemKey(key))).SelectMany(task => task)).ConfigureAwait(false); - Dictionary> result = new Dictionary>(bots.Count); + Dictionary> result = new Dictionary>(bots.Count, Bot.BotsComparer); int count = 0; foreach (Bot bot in bots) { - Dictionary responses = new Dictionary(request.KeysToRedeem.Count); + Dictionary responses = new Dictionary(request.KeysToRedeem.Count, StringComparer.Ordinal); result[bot.BotName] = responses; foreach (string key in request.KeysToRedeem) { @@ -435,7 +435,7 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { IList<(bool Success, string Token, string Message)> results = await Utilities.InParallel(bots.Select(bot => bot.Actions.GenerateTwoFactorAuthenticationToken())).ConfigureAwait(false); - Dictionary> result = new Dictionary>(bots.Count); + Dictionary> result = new Dictionary>(bots.Count, Bot.BotsComparer); foreach (Bot bot in bots) { (bool success, string token, string message) = results[result.Count]; @@ -460,7 +460,7 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { IList<(bool Success, string Message)> results = await Utilities.InParallel(bots.Select(bot => bot.Actions.HandleTwoFactorAuthenticationConfirmations(accept))).ConfigureAwait(false); - Dictionary result = new Dictionary(bots.Count); + Dictionary result = new Dictionary(bots.Count, Bot.BotsComparer); foreach (Bot bot in bots) { (bool success, string message) = results[result.Count]; diff --git a/ArchiSteamFarm/IPC/Controllers/Api/TypeController.cs b/ArchiSteamFarm/IPC/Controllers/Api/TypeController.cs index b693f94bc..e47ec96a9 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/TypeController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/TypeController.cs @@ -53,10 +53,10 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { } string baseType = targetType.BaseType?.GetUnifiedName(); - HashSet customAttributes = targetType.CustomAttributes.Select(attribute => attribute.AttributeType.GetUnifiedName()).ToHashSet(); + HashSet customAttributes = targetType.CustomAttributes.Select(attribute => attribute.AttributeType.GetUnifiedName()).ToHashSet(StringComparer.Ordinal); string underlyingType = null; - Dictionary body = new Dictionary(); + Dictionary body = new Dictionary(StringComparer.Ordinal); if (targetType.IsClass) { foreach (FieldInfo field in targetType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Where(field => !field.IsPrivate)) { diff --git a/ArchiSteamFarm/Statistics.cs b/ArchiSteamFarm/Statistics.cs index 97217043f..f1b673a89 100644 --- a/ArchiSteamFarm/Statistics.cs +++ b/ArchiSteamFarm/Statistics.cs @@ -96,9 +96,9 @@ namespace ArchiSteamFarm { const string request = URL + "/Api/HeartBeat"; - Dictionary data = new Dictionary(2) { - { "SteamID", Bot.SteamID.ToString() }, - { "Guid", ASF.GlobalDatabase.Guid.ToString("N") } + Dictionary data = new Dictionary(2, StringComparer.Ordinal) { + { "Guid", ASF.GlobalDatabase.Guid.ToString("N") }, + { "SteamID", Bot.SteamID.ToString() } }; // Listing is free to deny our announce request, hence we don't retry @@ -164,15 +164,15 @@ namespace ArchiSteamFarm { const string request = URL + "/Api/Announce"; - Dictionary data = new Dictionary(9) { - { "SteamID", Bot.SteamID.ToString() }, - { "Guid", ASF.GlobalDatabase.Guid.ToString("N") }, - { "Nickname", nickname ?? "" }, + Dictionary data = new Dictionary(9, StringComparer.Ordinal) { { "AvatarHash", avatarHash ?? "" }, { "GamesCount", inventory.Select(item => item.RealAppID).Distinct().Count().ToString() }, + { "Guid", ASF.GlobalDatabase.Guid.ToString("N") }, { "ItemsCount", inventory.Count.ToString() }, { "MatchableTypes", JsonConvert.SerializeObject(acceptedMatchableTypes) }, { "MatchEverything", Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) ? "1" : "0" }, + { "Nickname", nickname ?? "" }, + { "SteamID", Bot.SteamID.ToString() }, { "TradeToken", tradeToken } };