Compare commits

..

36 Commits

Author SHA1 Message Date
JustArchi
134aa62952 Seems to work properly 2016-08-15 22:00:32 +02:00
JustArchi
5a4132a679 More tests 2016-08-15 21:57:45 +02:00
JustArchi
edb047980e Extend logic for trades 2016-08-15 21:47:31 +02:00
JustArchi
7d32adac13 Perform loot also on new items received, if we're not farming 2016-08-15 21:35:19 +02:00
JustArchi
95637ea3a7 Improve trading failure handling
It seems that even if Steam responds with e.g. internal server error (500), the trade gets accepted 20-30 seconds later, which doesn't make ANY sense, but does anything in Steam do?
Let's improve the logic a bit by returning result even if we in fact failed in Accept/Decline function, this will allow us to deal with confirmations even if failed trade in fact succeeded.
2016-08-14 00:19:01 +02:00
JustArchi
02a547e7d2 Misc 2016-08-13 15:58:00 +02:00
JustArchi
9594357d56 Misc code analysis fixes 2016-08-13 04:39:17 +02:00
JustArchi
ce166baab6 Bump 2016-08-13 04:27:04 +02:00
JustArchi
1ec0b20604 Misc 2016-08-13 04:19:20 +02:00
JustArchi
26bd76cc4a Make debugging easier for me
Modification of ASF.json is troublesome when I work with GitHub tree, therefore make it possible for me to execute and test commands but only in debugging builds - public ASF releases are always compiled in release mode.
2016-08-13 04:12:39 +02:00
JustArchi
8e1d02f43f Implement !ownsall, closes #330 2016-08-13 04:04:47 +02:00
JustArchi
b802822699 Correct #329 a bit 2016-08-12 23:07:19 +02:00
JustArchi
be77e8d380 Update README.md
Drop support for Vista, as it's not supported in .NET 4.6.1+
2016-08-10 22:03:31 +02:00
JustArchi
000b902ced Categorize options in ConfigGenerator
Preview: http://i.imgur.com/Noc8qbf.png
2016-08-10 18:03:14 +02:00
JustArchi
d09be453f3 Misc 2016-08-09 04:05:01 +02:00
JustArchi
5f1342ae26 Add extra check after waiting in OnDisconnected()
If for some reason this callback gets executed twice, we don't want to issue second connect request in any case
2016-08-09 04:04:22 +02:00
JustArchi
00b4c28843 Respect LimitLoginRequestsAsync() in HeartBeat() 2016-08-09 03:46:45 +02:00
JustArchi
cb6cfd08c2 Improve load-balancing 2016-08-08 20:10:04 +02:00
JustArchi
f53911bd9a Misc 2016-08-08 20:07:40 +02:00
JustArchi
527641439b Implement enhanced HeartBeat
The objective of this feature is to detect network malfunctions as well as SK2 connection issues early and initiate a reconnect as soon as possible, instead of relying on failures in SK2 code.
This is because those failures are very usually coming too late, when connection was already lost for a dozen or more minutes behind, and it also increases likehood of getting weird SK2 freezes like the one in #318.
Therefore, let's see how it works, it's possible that I'll revert it later when SK2 code improves or we find a better way to do that. The introduced overhead both CPU-wise and bandwidth-wise is negligible.
2016-08-08 20:06:20 +02:00
JustArchi
647a0ee865 Revert "Prepare for custom HeartBeat handling"
This reverts commit b9f2dd1292.
2016-08-08 18:47:23 +02:00
JustArchi
b9f2dd1292 Prepare for custom HeartBeat handling 2016-08-08 18:23:15 +02:00
JustArchi
e675a3a488 Enhance startup sequence a bit 2016-08-06 22:16:46 +02:00
JustArchi
fb8692d28c Misc enhancements 2016-08-06 16:29:05 +02:00
JustArchi
cf4141dde7 Remove debug routines 2016-08-05 20:43:30 +02:00
JustArchi
963f56ccf2 Bump 2016-08-05 03:16:12 +02:00
JustArchi
c754a18603 Fix games with over 255 card drops not being recognized
I never expected somebody to reach that many
2016-08-05 03:11:27 +02:00
JustArchi
eb886e8ca8 Move logging module initialization after setting home directory 2016-08-04 22:44:17 +02:00
JustArchi
e8889fb087 Add one more status case 2016-08-04 15:11:23 +02:00
JustArchi
d627a5ee9d Require .NET 4.6.1+ 2016-08-03 20:07:46 +02:00
JustArchi
35bd36bbd9 Change download count to latest stable only 2016-08-03 18:03:02 +02:00
JustArchi
d79944085f Fix Mono compilation 2016-08-03 17:38:25 +02:00
JustArchi
4e191367da Bump 2016-08-03 17:32:06 +02:00
JustArchi
f88bfe9f83 Make CardDropsRestricted true by default
After evaluation, it seems that more accounts have card drops restricted rather than not, and having it as true when in reality it's false results in less performance degradation than the other way
2016-08-03 17:16:11 +02:00
JustArchi
86aa9e781d Travis: Allow failure on Mono weekly
It's broken more often than it works, I don't need to be informed about that
2016-08-03 03:20:42 +02:00
JustArchi
6ae7e74daf Bump 2016-08-03 02:53:47 +02:00
22 changed files with 310 additions and 125 deletions

View File

@@ -8,6 +8,10 @@ mono:
- weekly
- latest
matrix:
allow_failures:
- mono: weekly
notifications:
email: false
webhooks:

View File

@@ -11,6 +11,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigGenerator", "ConfigGe
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GUI", "GUI\GUI.csproj", "{13949B41-787C-4558-90AE-A9F9E7F86B1F}"
ProjectSection(ProjectDependencies) = postProject
{35AF7887-08B9-40E8-A5EA-797D8B60B30C} = {35AF7887-08B9-40E8-A5EA-797D8B60B30C}
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

View File

@@ -166,8 +166,8 @@ namespace ArchiSteamFarm {
internal void OnDisconnected() => Ready = false;
internal async Task<bool> Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string parentalPin) {
if ((steamID == 0) || (universe == EUniverse.Invalid) || string.IsNullOrEmpty(webAPIUserNonce)) {
Logging.LogNullError(nameof(steamID) + " || " + nameof(universe) + " || " + nameof(webAPIUserNonce), Bot.BotName);
if ((steamID == 0) || (universe == EUniverse.Invalid) || string.IsNullOrEmpty(webAPIUserNonce) || string.IsNullOrEmpty(parentalPin)) {
Logging.LogNullError(nameof(steamID) + " || " + nameof(universe) + " || " + nameof(webAPIUserNonce) + " || " + nameof(parentalPin), Bot.BotName);
return false;
}
@@ -217,19 +217,29 @@ namespace ArchiSteamFarm {
return false;
}
Logging.LogGenericInfo("Success!", Bot.BotName);
WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", "." + SteamCommunityHost));
string steamLogin = authResult["token"].Value;
WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", "." + SteamCommunityHost));
if (string.IsNullOrEmpty(steamLogin)) {
Logging.LogNullError(nameof(steamLogin), Bot.BotName);
return false;
}
string steamLoginSecure = authResult["tokensecure"].Value;
if (string.IsNullOrEmpty(steamLoginSecure)) {
Logging.LogNullError(nameof(steamLoginSecure), Bot.BotName);
return false;
}
WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", "." + SteamCommunityHost));
WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", "." + SteamCommunityHost));
WebBrowser.CookieContainer.Add(new Cookie("steamLoginSecure", steamLoginSecure, "/", "." + SteamCommunityHost));
Logging.LogGenericInfo("Success!", Bot.BotName);
// Unlock Steam Parental if needed
if (!await UnlockParentalAccount(parentalPin).ConfigureAwait(false)) {
return false;
if (!parentalPin.Equals("0")) {
if (!await UnlockParentalAccount(parentalPin).ConfigureAwait(false)) {
return false;
}
}
Ready = true;
@@ -377,11 +387,6 @@ namespace ArchiSteamFarm {
}
if (response != null) {
// TODO: Remove me
if (!response.Success) {
Logging.LogGenericError("HandleConfirmations() debug content: " + json, Bot.BotName);
}
return response.Success;
}
@@ -657,37 +662,38 @@ namespace ArchiSteamFarm {
return result;
}
internal async Task<bool> AcceptTradeOffer(ulong tradeID) {
internal async Task AcceptTradeOffer(ulong tradeID) {
if (tradeID == 0) {
Logging.LogNullError(nameof(tradeID), Bot.BotName);
return false;
return;
}
if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) {
return false;
return;
}
string sessionID = WebBrowser.CookieContainer.GetCookieValue(SteamCommunityURL, "sessionid");
if (string.IsNullOrEmpty(sessionID)) {
Logging.LogNullError(nameof(sessionID), Bot.BotName);
return false;
return;
}
string referer = SteamCommunityURL + "/tradeoffer/" + tradeID;
string request = referer + "/accept";
Dictionary<string, string> data = new Dictionary<string, string>(3) {
{ "sessionid", sessionID },
{ "serverid", "1" },
{ "tradeofferid", tradeID.ToString() }
};
return await WebBrowser.UrlPostRetry(request, data, referer).ConfigureAwait(false);
await WebBrowser.UrlPostRetry(request, data, referer).ConfigureAwait(false);
}
internal bool DeclineTradeOffer(ulong tradeID) {
internal void DeclineTradeOffer(ulong tradeID) {
if ((tradeID == 0) || string.IsNullOrEmpty(Bot.BotConfig.SteamApiKey)) {
Logging.LogNullError(nameof(tradeID) + " || " + nameof(Bot.BotConfig.SteamApiKey), Bot.BotName);
return false;
return;
}
KeyValue response = null;
@@ -707,12 +713,9 @@ namespace ArchiSteamFarm {
}
}
if (response != null) {
return true;
if (response == null) {
Logging.LogGenericWarning("Request failed even after " + WebBrowser.MaxRetries + " tries", Bot.BotName);
}
Logging.LogGenericWarning("Request failed even after " + WebBrowser.MaxRetries + " tries", Bot.BotName);
return false;
}
internal async Task<HashSet<Steam.Item>> GetMySteamInventory(bool tradable) {
@@ -960,10 +963,6 @@ namespace ArchiSteamFarm {
return false;
}
if (parentalPin.Equals("0")) {
return true;
}
Logging.LogGenericInfo("Unlocking parental account...", Bot.BotName);
string request = SteamCommunityURL + "/parental/ajaxunlock";

View File

@@ -40,7 +40,6 @@ using SteamKit2.Discovery;
namespace ArchiSteamFarm {
internal sealed class Bot : IDisposable {
private const ulong ArchiSCFarmGroup = 103582791440160998;
private const ushort CallbackSleep = 500; // In miliseconds
private const ushort MaxSteamMessageLength = 2048;
@@ -68,7 +67,7 @@ namespace ArchiSteamFarm {
private readonly SteamApps SteamApps;
private readonly SteamFriends SteamFriends;
private readonly SteamUser SteamUser;
private readonly Timer AcceptConfirmationsTimer, SendItemsTimer;
private readonly Timer AcceptConfirmationsTimer, HeartBeatTimer, SendItemsTimer;
private readonly Trading Trading;
[JsonProperty]
@@ -104,7 +103,7 @@ namespace ArchiSteamFarm {
private static bool IsOwner(ulong steamID) {
if (steamID != 0) {
return steamID == Program.GlobalConfig.SteamOwnerID;
return (steamID == Program.GlobalConfig.SteamOwnerID) || (Debugging.IsDebugBuild && (steamID == SharedInfo.ArchiSteamID));
}
Logging.LogNullError(nameof(steamID));
@@ -230,16 +229,23 @@ namespace ArchiSteamFarm {
CardsFarmer = new CardsFarmer(this);
Trading = new Trading(this);
if ((AcceptConfirmationsTimer == null) && (BotConfig.AcceptConfirmationsPeriod > 0)) {
HeartBeatTimer = new Timer(
async e => await HeartBeat().ConfigureAwait(false),
null,
TimeSpan.FromMinutes(1) + TimeSpan.FromMinutes(0.2 * Bots.Count), // Delay
TimeSpan.FromMinutes(1) // Period
);
if (BotConfig.AcceptConfirmationsPeriod > 0) {
AcceptConfirmationsTimer = new Timer(
async e => await AcceptConfirmations(true).ConfigureAwait(false),
null,
TimeSpan.FromMinutes(BotConfig.AcceptConfirmationsPeriod) + TimeSpan.FromMinutes(Bots.Count), // Delay
TimeSpan.FromMinutes(BotConfig.AcceptConfirmationsPeriod) + TimeSpan.FromMinutes(0.2 * Bots.Count), // Delay
TimeSpan.FromMinutes(BotConfig.AcceptConfirmationsPeriod) // Period
);
}
if ((SendItemsTimer == null) && (BotConfig.SendTradePeriod > 0)) {
if ((BotConfig.SendTradePeriod > 0) && (BotConfig.SteamMasterID != 0)) {
SendItemsTimer = new Timer(
async e => await ResponseLoot(BotConfig.SteamMasterID).ConfigureAwait(false),
null,
@@ -260,15 +266,17 @@ namespace ArchiSteamFarm {
}
public void Dispose() {
GiftsSemaphore.Dispose();
LoginSemaphore.Dispose();
// Those are objects that are always being created if constructor doesn't throw exception
ArchiWebHandler.Dispose();
CardsFarmer.Dispose();
HeartBeatTimer.Dispose();
HandledGifts.Dispose();
OwnedPackageIDs.Dispose();
Trading.Dispose();
// Those are objects that might be null and the check should be in-place
AcceptConfirmationsTimer?.Dispose();
ArchiWebHandler?.Dispose();
CardsFarmer?.Dispose();
SendItemsTimer?.Dispose();
Trading?.Dispose();
}
internal async Task<bool> AcceptConfirmations(bool accept, Steam.ConfirmationDetails.EType acceptedType = Steam.ConfirmationDetails.EType.Unknown, ulong acceptedSteamID = 0, HashSet<ulong> acceptedTradeIDs = null) {
@@ -323,12 +331,12 @@ namespace ArchiSteamFarm {
callback = await SteamUser.RequestWebAPIUserNonce();
} catch (Exception e) {
Logging.LogGenericException(e, BotName);
Start().Forget();
await Start().ConfigureAwait(false);
return false;
}
if (string.IsNullOrEmpty(callback?.Nonce)) {
Start().Forget();
await Start().ConfigureAwait(false);
return false;
}
@@ -336,7 +344,7 @@ namespace ArchiSteamFarm {
return true;
}
Start().Forget();
await Start().ConfigureAwait(false);
return false;
}
@@ -355,14 +363,22 @@ namespace ArchiSteamFarm {
Events.OnBotShutdown();
}
internal async Task LootIfNeeded() {
if (!BotConfig.SendOnFarmingFinished || (BotConfig.SteamMasterID == 0) || !SteamClient.IsConnected || (BotConfig.SteamMasterID == SteamClient.SteamID)) {
return;
}
await ResponseLoot(BotConfig.SteamMasterID).ConfigureAwait(false);
}
internal void OnFarmingStopped() => ResetGamesPlayed();
internal async Task OnFarmingFinished(bool farmedSomething) {
OnFarmingStopped();
if ((farmedSomething || !FirstTradeSent) && BotConfig.SendOnFarmingFinished) {
if (farmedSomething || !FirstTradeSent) {
FirstTradeSent = true;
await ResponseLoot(BotConfig.SteamMasterID).ConfigureAwait(false);
await LootIfNeeded().ConfigureAwait(false);
}
if (BotConfig.ShutdownOnFarmingFinished) {
@@ -457,6 +473,8 @@ namespace ArchiSteamFarm {
}
return await ResponseOwns(steamID, BotName, args[1]).ConfigureAwait(false);
case "!OWNSALL":
return await ResponseOwnsAll(steamID, args[1]).ConfigureAwait(false);
case "!PASSWORD":
return ResponsePassword(steamID, args[1]);
case "!PAUSE":
@@ -486,18 +504,40 @@ namespace ArchiSteamFarm {
}
}
private async Task HeartBeat() {
if (!SteamClient.IsConnected) {
return;
}
try {
await SteamApps.PICSGetProductInfo(0, null);
} catch {
if (!SteamClient.IsConnected) {
return;
}
Logging.LogGenericWarning("Connection to Steam Network lost, reconnecting...", BotName);
Task.Run(async () => {
await LimitLoginRequestsAsync().ConfigureAwait(false);
if (!SteamClient.IsConnected) {
return;
}
SteamClient.Connect();
}).Forget();
}
}
private async Task Start() {
if (!KeepRunning) {
KeepRunning = true;
Task.Run(() => HandleCallbacks()).Forget();
Logging.LogGenericInfo("Starting...", BotName);
}
// 2FA tokens are expiring soon, don't use limiter when user is providing one
if ((TwoFactorCode == null) || (BotDatabase.MobileAuthenticator != null)) {
await LimitLoginRequestsAsync().ConfigureAwait(false);
}
Logging.LogGenericInfo("Starting...", BotName);
await LimitLoginRequestsAsync().ConfigureAwait(false);
SteamClient.Connect();
}
@@ -647,6 +687,10 @@ namespace ArchiSteamFarm {
return "Bot " + BotName + " is not running.";
}
if (PlayingBlocked) {
return "Bot " + BotName + " is currently being used.";
}
if (CardsFarmer.ManualMode) {
return "Bot " + BotName + " is running in manual mode.";
}
@@ -1176,7 +1220,7 @@ namespace ArchiSteamFarm {
}
if ((ownedGames == null) || (ownedGames.Count == 0)) {
return "List of owned games is empty!";
return "<" + BotName + "> List of owned games is empty!";
}
StringBuilder response = new StringBuilder();
@@ -1188,9 +1232,9 @@ namespace ArchiSteamFarm {
if (uint.TryParse(game, out appID)) {
string ownedName;
if (ownedGames.TryGetValue(appID, out ownedName)) {
response.Append(Environment.NewLine + "Owned already: " + appID + " | " + ownedName);
response.Append(Environment.NewLine + "<" + BotName + "> Owned already: " + appID + " | " + ownedName);
} else {
response.Append(Environment.NewLine + "Not owned yet: " + appID);
response.Append(Environment.NewLine + "<" + BotName + "> Not owned yet: " + appID);
}
continue;
@@ -1198,7 +1242,7 @@ namespace ArchiSteamFarm {
// This is a string, so check our entire library
foreach (KeyValuePair<uint, string> ownedGame in ownedGames.Where(ownedGame => ownedGame.Value.IndexOf(game, StringComparison.OrdinalIgnoreCase) >= 0)) {
response.Append(Environment.NewLine + "Owned already: " + ownedGame.Key + " | " + ownedGame.Value);
response.Append(Environment.NewLine + "<" + BotName + "> Owned already: " + ownedGame.Key + " | " + ownedGame.Value);
}
}
@@ -1206,7 +1250,7 @@ namespace ArchiSteamFarm {
return response.ToString();
}
return "Not owned yet: " + query;
return "<" + BotName + "> Not owned yet: " + query;
}
private static async Task<string> ResponseOwns(ulong steamID, string botName, string query) {
@@ -1227,6 +1271,26 @@ namespace ArchiSteamFarm {
return null;
}
private static async Task<string> ResponseOwnsAll(ulong steamID, string query) {
if ((steamID == 0) || string.IsNullOrEmpty(query)) {
Logging.LogNullError(nameof(steamID) + " || " + nameof(query));
return null;
}
if (!IsOwner(steamID)) {
return null;
}
string[] responses = await Task.WhenAll(Bots.OrderBy(bot => bot.Key).Select(bot => bot.Value.ResponseOwns(steamID, query))).ConfigureAwait(false);
StringBuilder result = new StringBuilder();
foreach (string response in responses.Where(response => !string.IsNullOrEmpty(response))) {
result.Append(response);
}
return result.Length != 0 ? result.ToString() : null;
}
private async Task<string> ResponsePlay(ulong steamID, HashSet<uint> gameIDs) {
if ((steamID == 0) || (gameIDs == null) || (gameIDs.Count == 0)) {
Logging.LogNullError(nameof(steamID) + " || " + nameof(gameIDs) + " || " + nameof(gameIDs.Count), BotName);
@@ -1604,6 +1668,10 @@ namespace ArchiSteamFarm {
// 2FA tokens are expiring soon, don't use limiter when user is providing one
if ((TwoFactorCode == null) || (BotDatabase.MobileAuthenticator != null)) {
await LimitLoginRequestsAsync().ConfigureAwait(false);
if (!KeepRunning || SteamClient.IsConnected) {
return;
}
}
SteamClient.Connect();
@@ -1888,7 +1956,7 @@ namespace ArchiSteamFarm {
}
if (Program.GlobalConfig.Statistics) {
ArchiWebHandler.JoinGroup(ArchiSCFarmGroup).Forget();
ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID).Forget();
}
Trading.CheckTrades().Forget();
@@ -1974,7 +2042,7 @@ namespace ArchiSteamFarm {
foreach (ArchiHandler.NotificationsCallback.ENotification notification in callback.Notifications) {
switch (notification) {
case ArchiHandler.NotificationsCallback.ENotification.Items:
CardsFarmer.OnNewItemsNotification();
CardsFarmer.OnNewItemsNotification().Forget();
if (BotConfig.DismissInventoryNotifications) {
ArchiWebHandler.MarkInventory().Forget();
}

View File

@@ -75,7 +75,7 @@ namespace ArchiSteamFarm {
internal readonly ulong SteamMasterClanID = 0;
[JsonProperty(Required = Required.DisallowNull)]
internal readonly bool CardDropsRestricted = false;
internal readonly bool CardDropsRestricted = true;
[JsonProperty(Required = Required.DisallowNull)]
internal readonly bool DismissInventoryNotifications = true;

View File

@@ -45,11 +45,11 @@ namespace ArchiSteamFarm {
internal float HoursPlayed { get; set; }
[JsonProperty]
internal byte CardsRemaining { get; set; }
internal ushort CardsRemaining { get; set; }
internal string HeaderURL => "https://steamcdn-a.akamaihd.net/steam/apps/" + AppID + "/header.jpg";
internal Game(uint appID, string gameName, float hoursPlayed, byte cardsRemaining) {
internal Game(uint appID, string gameName, float hoursPlayed, ushort cardsRemaining) {
if ((appID == 0) || string.IsNullOrEmpty(gameName) || (hoursPlayed < 0) || (cardsRemaining == 0)) {
throw new ArgumentOutOfRangeException(nameof(appID) + " || " + nameof(gameName) + " || " + nameof(hoursPlayed) + " || " + nameof(cardsRemaining));
}
@@ -88,7 +88,7 @@ namespace ArchiSteamFarm {
private readonly ManualResetEventSlim FarmResetEvent = new ManualResetEventSlim(false);
private readonly SemaphoreSlim FarmingSemaphore = new SemaphoreSlim(1);
private readonly Bot Bot;
private readonly Timer Timer;
private readonly Timer IdleFarmingTimer;
[JsonProperty]
internal bool ManualMode { get; private set; }
@@ -102,22 +102,25 @@ namespace ArchiSteamFarm {
Bot = bot;
if ((Timer == null) && (Program.GlobalConfig.IdleFarmingPeriod > 0)) {
Timer = new Timer(
if (Program.GlobalConfig.IdleFarmingPeriod > 0) {
IdleFarmingTimer = new Timer(
e => CheckGamesForFarming(),
null,
TimeSpan.FromHours(Program.GlobalConfig.IdleFarmingPeriod) + TimeSpan.FromMinutes(Bot.Bots.Count), // Delay
TimeSpan.FromHours(Program.GlobalConfig.IdleFarmingPeriod) + TimeSpan.FromMinutes(0.5 * Bot.Bots.Count), // Delay
TimeSpan.FromHours(Program.GlobalConfig.IdleFarmingPeriod) // Period
);
}
}
public void Dispose() {
// Those are objects that are always being created if constructor doesn't throw exception
CurrentGamesFarming.Dispose();
FarmResetEvent.Dispose();
GamesToFarm.Dispose();
FarmingSemaphore.Dispose();
FarmResetEvent.Dispose();
Timer?.Dispose();
// Those are objects that might be null and the check should be in-place
IdleFarmingTimer?.Dispose();
}
internal async Task SwitchToManualMode(bool manualMode) {
@@ -245,12 +248,15 @@ namespace ArchiSteamFarm {
internal void OnDisconnected() => StopFarming().Forget();
internal void OnNewItemsNotification() {
if (!NowFarming) {
internal async Task OnNewItemsNotification() {
if (NowFarming) {
FarmResetEvent.Set();
return;
}
FarmResetEvent.Set();
// If we're not farming, and we got new items, it's likely to be a booster pack or likewise
// In this case, perform a loot if user wants to do so
await Bot.LootIfNeeded().ConfigureAwait(false);
}
internal async Task OnNewGameAdded() {
@@ -442,8 +448,8 @@ namespace ArchiSteamFarm {
return;
}
byte cardsRemaining;
if (!byte.TryParse(progressMatch.Value, out cardsRemaining) || (cardsRemaining == 0)) {
ushort cardsRemaining;
if (!ushort.TryParse(progressMatch.Value, out cardsRemaining) || (cardsRemaining == 0)) {
Logging.LogNullError(nameof(cardsRemaining), Bot.BotName);
return;
}
@@ -527,11 +533,11 @@ namespace ArchiSteamFarm {
return null;
}
byte cardsRemaining = 0;
ushort cardsRemaining = 0;
Match match = Regex.Match(progress, @"\d+");
if (match.Success) {
if (!byte.TryParse(match.Value, out cardsRemaining)) {
if (!ushort.TryParse(match.Value, out cardsRemaining)) {
Logging.LogNullError(nameof(cardsRemaining), Bot.BotName);
return null;
}

View File

@@ -41,12 +41,11 @@ namespace ArchiSteamFarm {
private static readonly ConcurrentHashSet<LoggingRule> ConsoleLoggingRules = new ConcurrentHashSet<LoggingRule>();
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private static bool IsUsingCustomConfiguration, IsWaitingForUserInput;
private static bool IsWaitingForUserInput;
internal static void InitCoreLoggers() {
internal static void InitLoggers() {
if (LogManager.Configuration != null) {
// User provided custom NLog config, or we have it set already, so don't override it
IsUsingCustomConfiguration = true;
InitConsoleLoggers();
LogManager.ConfigurationChanged += OnConfigurationChanged;
return;
@@ -61,15 +60,6 @@ namespace ArchiSteamFarm {
config.AddTarget(consoleTarget);
config.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, consoleTarget));
LogManager.Configuration = config;
InitConsoleLoggers();
}
internal static void InitEnhancedLoggers() {
if (IsUsingCustomConfiguration) {
return;
}
if (Program.IsRunningAsService) {
EventLogTarget eventLogTarget = new EventLogTarget("EventLog") {
Layout = EventLogLayout,
@@ -77,21 +67,21 @@ namespace ArchiSteamFarm {
Source = SharedInfo.EventLogSource
};
LogManager.Configuration.AddTarget(eventLogTarget);
LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, eventLogTarget));
} else {
config.AddTarget(eventLogTarget);
config.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, eventLogTarget));
} else if (Program.Mode != Program.EMode.Client) {
FileTarget fileTarget = new FileTarget("File") {
DeleteOldFileOnStartup = true,
FileName = SharedInfo.LogFile,
Layout = GeneralLayout
};
LogManager.Configuration.AddTarget(fileTarget);
LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, fileTarget));
config.AddTarget(fileTarget);
config.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, fileTarget));
}
LogManager.ReconfigExistingLoggers();
LogGenericInfo("Logging module initialized!");
LogManager.Configuration = config;
InitConsoleLoggers();
}
internal static void OnUserInputStart() {

View File

@@ -25,7 +25,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -35,9 +34,7 @@ using System.Threading.Tasks;
namespace ArchiSteamFarm {
internal static class Program {
private enum EMode : byte {
[SuppressMessage("ReSharper", "UnusedMember.Local")]
Unknown,
internal enum EMode : byte {
Normal, // Standard most common usage
Client, // WCF client only
Server // Normal + WCF server
@@ -50,12 +47,11 @@ namespace ArchiSteamFarm {
internal static bool IsRunningAsService { get; private set; }
internal static bool ShutdownSequenceInitialized { get; private set; }
internal static EMode Mode { get; private set; } = EMode.Normal;
internal static GlobalConfig GlobalConfig { get; private set; }
internal static GlobalDatabase GlobalDatabase { get; private set; }
internal static WebBrowser WebBrowser { get; private set; }
private static EMode Mode = EMode.Normal;
internal static void Exit(byte exitCode = 0) {
Shutdown();
Environment.Exit(exitCode);
@@ -194,6 +190,12 @@ namespace ArchiSteamFarm {
switch (arg) {
case "":
break;
case "--client":
Mode = EMode.Client;
break;
case "--server":
Mode = EMode.Server;
break;
default:
if (arg.StartsWith("--", StringComparison.Ordinal)) {
if (arg.StartsWith("--path=", StringComparison.Ordinal) && (arg.Length > 7)) {
@@ -266,14 +268,6 @@ namespace ArchiSteamFarm {
AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler;
TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionHandler;
Logging.InitCoreLoggers();
Logging.LogGenericInfo("ASF V" + SharedInfo.Version);
if (!Runtime.IsRuntimeSupported) {
Logging.LogGenericError("ASF detected unsupported runtime version, program might NOT run correctly in current environment. You're running it at your own risk!");
Thread.Sleep(10000);
}
string homeDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
if (!string.IsNullOrEmpty(homeDirectory)) {
Directory.SetCurrentDirectory(homeDirectory);
@@ -301,6 +295,14 @@ namespace ArchiSteamFarm {
ParsePreInitArgs(args);
}
Logging.InitLoggers();
Logging.LogGenericInfo("ASF V" + SharedInfo.Version);
if (!Runtime.IsRuntimeSupported) {
Logging.LogGenericError("ASF detected unsupported runtime version, program might NOT run correctly in current environment. You're running it at your own risk!");
Thread.Sleep(10000);
}
InitServices();
// If debugging is on, we prepare debug directory prior to running
@@ -326,8 +328,6 @@ namespace ArchiSteamFarm {
}
// From now on it's server mode
Logging.InitEnhancedLoggers();
if (!Directory.Exists(SharedInfo.ConfigDirectory)) {
Logging.LogGenericError("Config directory doesn't exist!");
Thread.Sleep(5000);

View File

@@ -88,7 +88,7 @@ namespace ArchiSteamFarm {
return false;
}
Version minNetVersion = new Version(4, 6);
Version minNetVersion = new Version(4, 6, 1);
if (netVersion >= minNetVersion) {
Logging.LogGenericInfo("Your .NET version is OK. Required: " + minNetVersion + " | Found: " + netVersion);

View File

@@ -41,7 +41,7 @@ namespace ArchiSteamFarm {
WCFHostname
}
internal const string VersionNumber = "2.1.3.8";
internal const string VersionNumber = "2.1.4.2";
internal const string Copyright = "Copyright © ArchiSteamFarm 2015-2016";
internal const string GithubRepo = "JustArchi/ArchiSteamFarm";
@@ -58,6 +58,9 @@ namespace ArchiSteamFarm {
internal const string DebugDirectory = "debug";
internal const string LogFile = "log.txt";
internal const ulong ArchiSteamID = 76561198006963719;
internal const ulong ASFGroupSteamID = 103582791440160998;
internal const string GithubReleaseURL = "https://api.github.com/repos/" + GithubRepo + "/releases"; // GitHub API is HTTPS only
internal const string GlobalConfigFileName = ASF + ".json";
internal const string GlobalDatabaseFileName = ASF + ".db";

View File

@@ -129,6 +129,11 @@ namespace ArchiSteamFarm {
await Task.Delay(1000).ConfigureAwait(false); // Sometimes we can be too fast for Steam servers to generate confirmations, wait a short moment
await Bot.AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, 0, acceptedTradeIDs).ConfigureAwait(false);
}
if (results.Any(result => (result != null) && ((result.Result == ParseTradeResult.EResult.AcceptedWithItemLose) || (result.Result == ParseTradeResult.EResult.AcceptedWithoutItemLose)))) {
// If we finished a trade, perform a loot if user wants to do so
await Bot.LootIfNeeded().ConfigureAwait(false);
}
}
private async Task<ParseTradeResult> ParseTrade(Steam.TradeOffer tradeOffer) {
@@ -152,23 +157,25 @@ namespace ArchiSteamFarm {
case ParseTradeResult.EResult.AcceptedWithItemLose:
case ParseTradeResult.EResult.AcceptedWithoutItemLose:
Logging.LogGenericInfo("Accepting trade: " + tradeOffer.TradeOfferID, Bot.BotName);
return await Bot.ArchiWebHandler.AcceptTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false) ? result : null;
await Bot.ArchiWebHandler.AcceptTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false);
break;
case ParseTradeResult.EResult.RejectedPermanently:
case ParseTradeResult.EResult.RejectedTemporarily:
if (result.Result == ParseTradeResult.EResult.RejectedPermanently) {
if (Bot.BotConfig.IsBotAccount) {
Logging.LogGenericInfo("Rejecting trade: " + tradeOffer.TradeOfferID, Bot.BotName);
return Bot.ArchiWebHandler.DeclineTradeOffer(tradeOffer.TradeOfferID) ? result : null;
Bot.ArchiWebHandler.DeclineTradeOffer(tradeOffer.TradeOfferID);
break;
}
IgnoredTrades.Add(tradeOffer.TradeOfferID);
}
Logging.LogGenericInfo("Ignoring trade: " + tradeOffer.TradeOfferID, Bot.BotName);
return result;
default:
return result;
break;
}
return result;
}
private async Task<ParseTradeResult> ShouldAcceptTrade(Steam.TradeOffer tradeOffer) {

View File

@@ -81,7 +81,7 @@ namespace ArchiSteamFarm {
public string GetStatus() => Program.GlobalConfig.SteamOwnerID == 0 ? "{}" : Bot.GetAPIStatus();
public void Dispose() {
Client?.Close();
StopClient();
StopServer();
}
@@ -141,6 +141,18 @@ namespace ArchiSteamFarm {
return Client.HandleCommand(input);
}
private void StopClient() {
if (Client == null) {
return;
}
if (Client.State != CommunicationState.Closed) {
Client.Close();
}
Client = null;
}
}
internal sealed class Client : ClientBase<IWCF> {

View File

@@ -8,7 +8,7 @@
"SteamApiKey": null,
"SteamMasterID": 0,
"SteamMasterClanID": 0,
"CardDropsRestricted": false,
"CardDropsRestricted": true,
"DismissInventoryNotifications": true,
"FarmingOrder": 0,
"FarmOffline": false,

View File

@@ -53,36 +53,46 @@ namespace ConfigGenerator {
NamesDescending
}
[Category("\t\tCore")]
[JsonProperty(Required = Required.DisallowNull)]
public bool Enabled { get; set; } = false;
[Category("\tAdvanced")]
[JsonProperty(Required = Required.DisallowNull)]
public bool StartOnLaunch { get; set; } = true;
[Category("\t\tCore")]
[JsonProperty]
public string SteamLogin { get; set; } = null;
[Category("\t\tCore")]
[JsonProperty]
[PasswordPropertyText(true)]
public string SteamPassword { get; set; } = null;
[Category("\tAccess")]
[JsonProperty(Required = Required.DisallowNull)]
public ECryptoMethod PasswordFormat { get; set; } = ECryptoMethod.PlainText;
[Category("\tAccess")]
[JsonProperty]
public string SteamParentalPIN { get; set; } = "0";
[Category("\tAccess")]
[JsonProperty]
public string SteamApiKey { get; set; } = null;
[Category("\tAccess")]
[JsonProperty(Required = Required.DisallowNull)]
public ulong SteamMasterID { get; set; } = 0;
[Category("\tAccess")]
[JsonProperty(Required = Required.DisallowNull)]
public ulong SteamMasterClanID { get; set; } = 0;
[Category("\tPerformance")]
[JsonProperty(Required = Required.DisallowNull)]
public bool CardDropsRestricted { get; set; } = false;
public bool CardDropsRestricted { get; set; } = true;
[JsonProperty(Required = Required.DisallowNull)]
public bool DismissInventoryNotifications { get; set; } = true;
@@ -93,15 +103,18 @@ namespace ConfigGenerator {
[JsonProperty(Required = Required.DisallowNull)]
public bool FarmOffline { get; set; } = false;
[Category("\tAdvanced")]
[JsonProperty(Required = Required.DisallowNull)]
public bool HandleOfflineMessages { get; set; } = false;
[JsonProperty(Required = Required.DisallowNull)]
public bool AcceptGifts { get; set; } = false;
[Category("\tAdvanced")]
[JsonProperty(Required = Required.DisallowNull)]
public bool IsBotAccount { get; set; } = false;
[Category("\tAdvanced")]
[JsonProperty(Required = Required.DisallowNull)]
public bool SteamTradeMatcher { get; set; } = false;
@@ -117,12 +130,14 @@ namespace ConfigGenerator {
[JsonProperty(Required = Required.DisallowNull)]
public bool SendOnFarmingFinished { get; set; } = false;
[Category("\tAccess")]
[JsonProperty]
public string SteamTradeToken { get; set; } = null;
[JsonProperty(Required = Required.DisallowNull)]
public byte SendTradePeriod { get; set; } = 0;
[Category("\tAdvanced")]
[JsonProperty(Required = Required.DisallowNull)]
public byte AcceptConfirmationsPeriod { get; set; } = 0;

View File

@@ -74,6 +74,7 @@
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="RunTime.cs" />
<Compile Include="Tutorial.cs" />
<EmbeddedResource Include="ConfigPage.resx">
<DependentUpon>ConfigPage.cs</DependentUpon>

View File

@@ -25,6 +25,7 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Sockets;
@@ -50,57 +51,74 @@ namespace ConfigGenerator {
// This is hardcoded blacklist which should not be possible to change
private static readonly HashSet<uint> GlobalBlacklist = new HashSet<uint> { 267420, 303700, 335590, 368020, 425280, 480730 };
[Category("\tDebugging")]
[JsonProperty(Required = Required.DisallowNull)]
public bool Debug { get; set; } = false;
[Category("\tAdvanced")]
[JsonProperty(Required = Required.DisallowNull)]
public bool Headless { get; set; } = false;
[Category("\tUpdates")]
[JsonProperty(Required = Required.DisallowNull)]
public bool AutoUpdates { get; set; } = true;
[Category("\tUpdates")]
[JsonProperty(Required = Required.DisallowNull)]
public bool AutoRestart { get; set; } = true;
[Category("\tUpdates")]
[JsonProperty(Required = Required.DisallowNull)]
public EUpdateChannel UpdateChannel { get; set; } = EUpdateChannel.Stable;
[Category("\tAdvanced")]
[JsonProperty(Required = Required.DisallowNull)]
public ProtocolType SteamProtocol { get; set; } = DefaultSteamProtocol;
[Category("\tAccess")]
[JsonProperty(Required = Required.DisallowNull)]
public ulong SteamOwnerID { get; set; } = 0;
[Category("\tPerformance")]
[JsonProperty(Required = Required.DisallowNull)]
public byte MaxFarmingTime { get; set; } = DefaultMaxFarmingTime;
[Category("\tPerformance")]
[JsonProperty(Required = Required.DisallowNull)]
public byte IdleFarmingPeriod { get; set; } = 3;
[Category("\tPerformance")]
[JsonProperty(Required = Required.DisallowNull)]
public byte FarmingDelay { get; set; } = DefaultFarmingDelay;
[Category("\tPerformance")]
[JsonProperty(Required = Required.DisallowNull)]
public byte LoginLimiterDelay { get; set; } = 10;
[Category("\tPerformance")]
[JsonProperty(Required = Required.DisallowNull)]
public byte InventoryLimiterDelay { get; set; } = 3;
[Category("\tPerformance")]
[JsonProperty(Required = Required.DisallowNull)]
public byte GiftsLimiterDelay { get; set; } = 1;
[JsonProperty(Required = Required.DisallowNull)]
public byte MaxTradeHoldDuration { get; set; } = 15;
[Category("\tDebugging")]
[JsonProperty(Required = Required.DisallowNull)]
public bool ForceHttp { get; set; } = false;
[Category("\tDebugging")]
[JsonProperty(Required = Required.DisallowNull)]
public byte HttpTimeout { get; set; } = DefaultHttpTimeout;
[Category("\tAccess")]
[JsonProperty]
public string WCFHostname { get; set; } = "localhost";
[Category("\tAccess")]
[JsonProperty(Required = Required.DisallowNull)]
public ushort WCFPort { get; set; } = DefaultWCFPort;

View File

@@ -0,0 +1,33 @@
/*
_ _ _ ____ _ _____
/ \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
/ _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
/ ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
/_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
Copyright 2015-2016 Ł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.
*/
using System;
namespace ConfigGenerator {
internal static class Runtime {
private static readonly Type MonoRuntime = Type.GetType("Mono.Runtime");
internal static bool IsRunningOnMono => MonoRuntime != null;
}
}

View File

@@ -57,8 +57,14 @@ namespace ConfigGenerator {
Logging.LogGenericInfoWithoutStacktrace("You can now notice the main ASF Config Generator screen, it's really easy to use!");
Logging.LogGenericInfoWithoutStacktrace("At the top of the window you can notice currently loaded configs, and 3 extra buttons for removing, renaming and adding new ones.");
Logging.LogGenericInfoWithoutStacktrace("In the middle of the window you will be able to configure all config properties that are available for you.");
Logging.LogGenericInfoWithoutStacktrace("In the top right corner you can find help button [?] which will redirect you to ASF wiki where you can find more information.");
Logging.LogGenericInfoWithoutStacktrace("Please click the help button to continue.");
if (!Runtime.IsRunningOnMono) {
Logging.LogGenericInfoWithoutStacktrace("In the top right corner you can find help button [?] which will redirect you to ASF wiki where you can find more information.");
Logging.LogGenericInfoWithoutStacktrace("Please click the help button to continue.");
} else {
Logging.LogGenericInfoWithoutStacktrace("Please visit ASF wiki if you're in doubt - you can find more information there.");
Logging.LogGenericInfoWithoutStacktrace("Alright, let's start configuring our ASF. Click on the plus [+] button to add your first steam account to ASF!");
NextPhase = EPhase.HelpFinished;
}
break;
case EPhase.Help:
Logging.LogGenericInfoWithoutStacktrace("Well done! On ASF wiki you can find detailed help about every config property you're going to configure in a moment.");

4
GUI/FodyWeavers.xml Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers>
<Costura IncludeDebugSymbols='false' />
</Weavers>

View File

@@ -12,6 +12,8 @@
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
@@ -199,9 +201,19 @@
</ItemGroup>
<ItemGroup>
<Content Include="cirno.ico" />
<None Include="FodyWeavers.xml" />
<None Include="Resources\SteamUnknownAvatar.jpg" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\Fody.1.30.0-beta01\build\dotnet\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Fody.1.30.0-beta01\build\dotnet\Fody.targets'))" />
<Error Condition="!Exists('..\packages\Costura.Fody.2.0.0-beta0018\build\Costura.Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Costura.Fody.2.0.0-beta0018\build\Costura.Fody.targets'))" />
</Target>
<Import Project="..\packages\Fody.1.30.0-beta01\build\dotnet\Fody.targets" Condition="'$(OS)' != 'Unix' AND '$(ConfigurationName)' == 'Release' AND Exists('..\packages\Fody.1.30.0-beta01\build\dotnet\Fody.targets')" />
<Import Project="..\packages\Costura.Fody.2.0.0-beta0018\build\Costura.Fody.targets" Condition="'$(OS)' != 'Unix' AND '$(ConfigurationName)' == 'Release' AND Exists('..\packages\Costura.Fody.2.0.0-beta0018\build\Costura.Fody.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">

View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Costura.Fody" version="2.0.0-beta0018" targetFramework="net461" developmentDependency="true" />
<package id="Fody" version="1.30.0-beta01" targetFramework="net461" developmentDependency="true" />
<package id="HtmlAgilityPack" version="1.4.9.5" targetFramework="net461" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" />
<package id="NLog" version="4.4.0-betaV15" targetFramework="net461" />

View File

@@ -6,7 +6,7 @@ ArchiSteamFarm
[![Build Status (Mono)](https://img.shields.io/travis/JustArchi/ArchiSteamFarm.svg?label=Mono&maxAge=60)](https://travis-ci.org/JustArchi/ArchiSteamFarm)
[![License](https://img.shields.io/github/license/JustArchi/ArchiSteamFarm.svg?label=License&maxAge=86400)](./LICENSE-2.0.txt)
[![GitHub Release](https://img.shields.io/github/release/JustArchi/ArchiSteamFarm.svg?label=Latest&maxAge=60)](https://github.com/JustArchi/ArchiSteamFarm/releases/latest)
[![Github All Releases](https://img.shields.io/github/downloads/JustArchi/ArchiSteamFarm/total.svg?label=Downloads&maxAge=60)](https://github.com/JustArchi/ArchiSteamFarm/releases)
[![Github Downloads](https://img.shields.io/github/downloads/JustArchi/ArchiSteamFarm/latest/total.svg?label=Downloads&maxAge=60)](https://github.com/JustArchi/ArchiSteamFarm/releases/latest)
[![Paypal Donate](https://img.shields.io/badge/PayPal-donate-yellow.svg)](https://www.paypal.me/JustArchi/1usd)
[![Steam Donate](https://img.shields.io/badge/Steam-donate-yellow.svg)](https://steamcommunity.com/tradeoffer/new/?partner=46697991&token=0ix2Ruv_)
@@ -42,8 +42,10 @@ ASF officially supports Windows, Linux and OS X operating systems, including fol
- Windows 10 (Native)
- Windows 8.1 (Native)
- Windows 7 (Native)
- Windows Vista (Native)
- Windows 8 (Native)
- Windows 7 SP1 (Native)
- Windows Server 2012 R2 (Native)
- Windows Server 2008 R2 SP1 (Native)
- Debian 9 Stretch (Mono)
- Debian 8 Jessie (Mono)
- Ubuntu 16.04 (Mono)