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) {