diff --git a/ArchiSteamFarm.sln.DotSettings b/ArchiSteamFarm.sln.DotSettings index 659c79b11..62a290edb 100644 --- a/ArchiSteamFarm.sln.DotSettings +++ b/ArchiSteamFarm.sln.DotSettings @@ -107,6 +107,7 @@ SUGGESTION WARNING WARNING + <?xml version="1.0" encoding="utf-16"?><Profile name="Archi"><CSReorderTypeMembers>True</CSReorderTypeMembers><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><HtmlReformatCode>True</HtmlReformatCode><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><XMLReformatCode>True</XMLReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeArgumentsStyle="True" ArrangeCodeBodyStyle="True" ArrangeVarStyle="True" /><RemoveCodeRedundanciesVB>True</RemoveCodeRedundanciesVB><CssAlphabetizeProperties>True</CssAlphabetizeProperties><VBOptimizeImports>True</VBOptimizeImports><VBShortenReferences>True</VBShortenReferences><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><VBReformatCode>True</VBReformatCode><VBFormatDocComments>True</VBFormatDocComments><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments></Profile> USE_TABS_ONLY True Required @@ -119,6 +120,7 @@ Shift, Bitwise, Conditional END_OF_LINE END_OF_LINE + USE_TABS_ONLY False END_OF_LINE 1 @@ -153,8 +155,16 @@ WRAP_IF_LONG 65535 False + USE_TABS_ONLY + USE_TABS_ONLY Tab False + USE_TABS_ONLY + USE_TABS_ONLY + USE_TABS_ONLY + USE_TABS_ONLY + USE_TABS_ONLY + USE_TABS_ONLY 2 2 False @@ -367,6 +377,27 @@ UseExplicitType UseExplicitType UseExplicitType + _ _ _ ____ _ _____ + / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ + / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ + / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +/_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| + + Copyright 2015-2017 Ɓukasz "JustArchi" Domeradzki + Contact: JustArchi@JustArchi.net + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + AES API ASF @@ -419,7 +450,11 @@ True True True + True True True + True + True + True True 8 \ No newline at end of file diff --git a/ArchiSteamFarm/ArchiHandler.cs b/ArchiSteamFarm/ArchiHandler.cs index 4eec2302f..9d4074178 100644 --- a/ArchiSteamFarm/ArchiHandler.cs +++ b/ArchiSteamFarm/ArchiHandler.cs @@ -24,7 +24,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; @@ -124,9 +123,7 @@ namespace ArchiSteamFarm { } foreach (uint gameID in gameIDs.Where(gameID => gameID != 0)) { - request.Body.games_played.Add(new CMsgClientGamesPlayed.GamePlayed { - game_id = new GameID(gameID) - }); + request.Body.games_played.Add(new CMsgClientGamesPlayed.GamePlayed { game_id = new GameID(gameID) }); } Client.Send(request); @@ -182,6 +179,11 @@ namespace ArchiSteamFarm { } } + internal void RequestItemAnnouncements() { + ClientMsgProtobuf request = new ClientMsgProtobuf(EMsg.ClientRequestItemAnnouncements); + Client.Send(request); + } + private void HandleFSOfflineMessageNotification(IPacketMsg packetMsg) { if (packetMsg == null) { ArchiLogger.LogNullError(nameof(packetMsg)); @@ -253,7 +255,7 @@ namespace ArchiSteamFarm { } internal sealed class NotificationsCallback : CallbackMsg { - internal readonly HashSet Notifications; + internal readonly HashSet Notifications; internal NotificationsCallback(JobID jobID, CMsgClientUserNotifications msg) { if ((jobID == null) || (msg == null)) { @@ -266,7 +268,16 @@ namespace ArchiSteamFarm { return; } - Notifications = new HashSet(msg.notifications.Select(notification => (ENotification) notification.user_notification_type)); + Notifications = new HashSet(); + + foreach (CMsgClientUserNotifications.Notification notification in msg.notifications) { + if (notification.user_notification_type == 0) { + ASF.ArchiLogger.LogNullError(nameof(notification.user_notification_type)); + continue; + } + + Notifications.Add(new Notification((ENotification) notification.user_notification_type, notification.count)); + } } internal NotificationsCallback(JobID jobID, CMsgClientItemAnnouncements msg) { @@ -275,22 +286,29 @@ namespace ArchiSteamFarm { } JobID = jobID; + Notifications = new HashSet { new Notification(ENotification.Items, msg.count_new_items) }; + } - if (msg.count_new_items > 0) { - Notifications = new HashSet { - ENotification.Items - }; + internal sealed class Notification { + internal readonly uint Count; + internal readonly ENotification Type; + + internal Notification(ENotification type, uint count = 0) { + if (type == ENotification.Unknown) { + throw new ArgumentNullException(nameof(type)); + } + + Type = type; + Count = count; } } internal enum ENotification : byte { - [SuppressMessage("ReSharper", "UnusedMember.Global")] Unknown = 0, - Trading = 1, // Only custom below, different than ones available as user_notification_type - Items = 254 + Items = 255 } } diff --git a/ArchiSteamFarm/ArchiWebHandler.cs b/ArchiSteamFarm/ArchiWebHandler.cs index 689634f13..0bf346509 100644 --- a/ArchiSteamFarm/ArchiWebHandler.cs +++ b/ArchiSteamFarm/ArchiWebHandler.cs @@ -71,6 +71,7 @@ namespace ArchiSteamFarm { private bool? CachedPublicInventory; private string CachedTradeToken; private DateTime LastSessionRefreshCheck = DateTime.MinValue; + private bool MarkingInventoryScheduled; private ulong SteamID; private string VanityURL; @@ -945,17 +946,30 @@ namespace ArchiSteamFarm { return await WebBrowser.UrlPostRetry(request, data).ConfigureAwait(false); } - internal async Task MarkInventory() { - if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return false; - } + internal async Task MarkInventory() { + // We aim to have a maximum of 2 tasks, one already working, and one waiting in the queue + // This way we can call this function as many times as needed e.g. because of Steam events + lock (InventorySemaphore) { + if (MarkingInventoryScheduled) { + return; + } - const string request = SteamCommunityURL + "/my/inventory"; + MarkingInventoryScheduled = true; + } await InventorySemaphore.WaitAsync().ConfigureAwait(false); try { - return await WebBrowser.UrlHeadRetry(request).ConfigureAwait(false); + lock (InventorySemaphore) { + MarkingInventoryScheduled = false; + } + + if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { + return; + } + + const string request = SteamCommunityURL + "/my/inventory"; + await WebBrowser.UrlHeadRetry(request).ConfigureAwait(false); } finally { if (Program.GlobalConfig.InventoryLimiterDelay == 0) { InventorySemaphore.Release(); diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index f5313962e..9264dee31 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -119,6 +119,7 @@ namespace ArchiSteamFarm { private Timer FamilySharingInactivityTimer; private bool FirstTradeSent; private byte HeartBeatFailures; + private uint ItemsCount; private EResult LastLogOnResult; private ulong LibraryLockedBySteamID; private bool LootingAllowed = true; @@ -127,6 +128,7 @@ namespace ArchiSteamFarm { private Timer SendItemsTimer; private bool SkipFirstShutdown; private SteamSaleEvent SteamSaleEvent; + private uint TradesCount; private string TwoFactorCode; private byte TwoFactorCodeFailures; @@ -1470,20 +1472,27 @@ namespace ArchiSteamFarm { }).Forget(); } - private async Task MarkInventoryIfNeeded() { - if (!BotConfig.DismissInventoryNotifications) { - return; - } - - await ArchiWebHandler.MarkInventory().ConfigureAwait(false); - } - private async void OnAccountInfo(SteamUser.AccountInfoCallback callback) { if (callback == null) { ArchiLogger.LogNullError(nameof(callback)); return; } + ArchiHandler.RequestItemAnnouncements(); + + // Sometimes Steam won't send us our own PersonaStateCallback, so request it explicitly + RequestPersonaStateUpdate(); + + InitializeFamilySharing().Forget(); + Statistics?.OnAccountInfo().Forget(); + + if (BotConfig.SteamMasterClanID != 0) { + Task.Run(async () => { + await ArchiWebHandler.JoinGroup(BotConfig.SteamMasterClanID).ConfigureAwait(false); + JoinMasterChat(); + }).Forget(); + } + if (BotConfig.FarmOffline) { return; } @@ -1934,27 +1943,9 @@ namespace ArchiSteamFarm { } if (!await ArchiWebHandler.Init(callback.ClientSteamID, SteamClient.Universe, callback.WebAPIUserNonce, BotConfig.SteamParentalPIN, callback.VanityURL).ConfigureAwait(false)) { - if (!await RefreshSession().ConfigureAwait(false)) { - break; - } + await RefreshSession().ConfigureAwait(false); } - // Sometimes Steam won't send us our own PersonaStateCallback, so request it explicitly - RequestPersonaStateUpdate(); - - InitializeFamilySharing().Forget(); - MarkInventoryIfNeeded().Forget(); - - if (BotConfig.SteamMasterClanID != 0) { - Task.Run(async () => { - await ArchiWebHandler.JoinGroup(BotConfig.SteamMasterClanID).ConfigureAwait(false); - JoinMasterChat(); - }).Forget(); - } - - Statistics?.OnLoggedOn().Forget(); - Trading.OnNewTrade().Forget(); - break; case EResult.InvalidPassword: case EResult.NoConnection: @@ -2047,14 +2038,32 @@ namespace ArchiSteamFarm { return; } - foreach (ArchiHandler.NotificationsCallback.ENotification notification in callback.Notifications) { - switch (notification) { + foreach (ArchiHandler.NotificationsCallback.Notification notification in callback.Notifications) { + switch (notification.Type) { case ArchiHandler.NotificationsCallback.ENotification.Items: - CardsFarmer.OnNewItemsNotification().Forget(); - MarkInventoryIfNeeded().Forget(); + bool newItems = notification.Count > ItemsCount; + ItemsCount = notification.Count; + + if (newItems) { + CardsFarmer.OnNewItemsNotification().Forget(); + + if (BotConfig.DismissInventoryNotifications) { + ArchiWebHandler.MarkInventory().Forget(); + } + } + break; case ArchiHandler.NotificationsCallback.ENotification.Trading: - Trading.OnNewTrade().Forget(); + bool newTrades = notification.Count > TradesCount; + TradesCount = notification.Count; + + if (newTrades) { + Trading.OnNewTrade().Forget(); + } + + break; + default: + ASF.ArchiLogger.LogGenericWarning(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(notification.Type), notification.Type)); break; } } diff --git a/ArchiSteamFarm/Statistics.cs b/ArchiSteamFarm/Statistics.cs index fcae91b49..8220710a1 100644 --- a/ArchiSteamFarm/Statistics.cs +++ b/ArchiSteamFarm/Statistics.cs @@ -51,6 +51,8 @@ namespace ArchiSteamFarm { public void Dispose() => RequestsSemaphore.Dispose(); + internal async Task OnAccountInfo() => await Bot.ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID).ConfigureAwait(false); + internal async Task OnHeartBeat() { // Request persona update if needed if ((DateTime.UtcNow > LastPersonaStateRequest.AddHours(MinPersonaStateTTL)) && (DateTime.UtcNow > LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL))) { @@ -84,8 +86,6 @@ namespace ArchiSteamFarm { } } - internal async Task OnLoggedOn() => await Bot.ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID).ConfigureAwait(false); - internal async Task OnPersonaState(SteamFriends.PersonaStateCallback callback) { if (callback == null) { ASF.ArchiLogger.LogNullError(nameof(callback)); diff --git a/ArchiSteamFarm/Trading.cs b/ArchiSteamFarm/Trading.cs index cc4d6b914..2b0e1be13 100644 --- a/ArchiSteamFarm/Trading.cs +++ b/ArchiSteamFarm/Trading.cs @@ -48,7 +48,7 @@ namespace ArchiSteamFarm { internal void OnDisconnected() => IgnoredTrades.Clear(); internal async Task OnNewTrade() { - // We aim to have a maximum of 2 tasks, one already parsing, and one waiting in the queue + // We aim to have a maximum of 2 tasks, one already working, and one waiting in the queue // This way we can call this function as many times as needed e.g. because of Steam events lock (TradesSemaphore) { if (ParsingScheduled) {