Compare commits

..

42 Commits

Author SHA1 Message Date
JustArchi
f3da5d6afc Fix small regression caused by d0cc10f3c6 2016-04-23 16:21:12 +02:00
JustArchi
84f33fcef4 Bump 2016-04-23 16:12:57 +02:00
JustArchi
22f0d423a3 Closes #205 2016-04-23 14:47:39 +02:00
JustArchi
f1d7609796 Send trades also when we didn't farm anything
Previously I avoided that because of !loot looting entire steam EQ, but now when we loot only cards and boosters, that shouldn't be as annoying as before
2016-04-23 02:15:01 +02:00
JustArchi
77386ecae5 Misc 2016-04-22 17:51:13 +02:00
JustArchi
4e86d21ef8 Misc 2016-04-22 17:50:01 +02:00
JustArchi
044fc87691 Closes #201 2016-04-21 20:14:15 +02:00
JustArchi
79fad62a4d Bump 2016-04-21 19:26:47 +02:00
JustArchi
6622d3b147 Misc 2016-04-21 14:48:24 +02:00
JustArchi
6b6d5429ad Fix 1+1 -> 0+2, #84 2016-04-21 04:50:57 +02:00
JustArchi
d59d0a8415 Misc 2016-04-21 02:37:40 +02:00
JustArchi
e3100d3938 Implement super-smart STM calculations, #84
It's excellent now, also fixed inventory not returning all of my 6k items (sigh)
2016-04-21 02:32:36 +02:00
JustArchi
42c020e552 Add AppVeyor config 2016-04-21 01:09:52 +02:00
JustArchi
554273833b Make travis less annoying 2016-04-21 01:02:07 +02:00
JustArchi
093a29df62 Loot only steam cards and boosters, closes #111 2016-04-21 00:56:15 +02:00
JustArchi
31aa6b2e4a Remove my debug 2016-04-21 00:51:55 +02:00
JustArchi
9f7ecdf054 Add support for inventory descriptions, #111 2016-04-21 00:51:22 +02:00
JustArchi
7c4c74bf84 Bump 2016-04-20 23:57:06 +02:00
JustArchi
b8f03abd8b Fix fuckups 2016-04-20 23:48:33 +02:00
JustArchi
47f846540b Closes #200, thanks to @GUiHKX 2016-04-20 23:44:20 +02:00
JustArchi
bc14713079 Bump 2016-04-20 23:22:16 +02:00
JustArchi
770a8fee66 But keep trading only of cards + foils 2016-04-20 23:16:15 +02:00
JustArchi
1df9af08e6 Bugfixes + other types for STM 2016-04-20 23:02:02 +02:00
JustArchi
27464f6120 Add recent trades optimization 2016-04-20 22:19:31 +02:00
JustArchi
dfd45c6e25 Bump 2016-04-20 21:40:08 +02:00
JustArchi
adc1759cee Misc 2016-04-20 21:34:40 +02:00
JustArchi
88369ec71a Put massive amount of work into STM integration, #84 2016-04-20 21:27:57 +02:00
JustArchi
a5d8ae53dd Bump 2016-04-19 19:54:39 +02:00
JustArchi
cd7b65868a Misc 2016-04-19 12:24:07 +02:00
JustArchi
7575704a01 Misc 2016-04-19 12:23:24 +02:00
JustArchi
b6ce8f435c Use more optimized Slim manual reset events 2016-04-18 18:43:58 +02:00
JustArchi
565acca9fb Add AutoRestart property 2016-04-18 18:38:48 +02:00
JustArchi
610954ba73 Alter logic of key distribution, closes #199 2016-04-18 18:01:49 +02:00
JustArchi
74a748b03f Bump 2016-04-17 00:47:30 +02:00
JustArchi
585a075ec9 Closes #198
It was possible that we initiated a loop for bot that was connected, and it got disconnected shortly after, which could result in infinite loop if DistributeKeys was disabled (and nothing would change that bot to other one)
2016-04-17 00:36:38 +02:00
JustArchi
891d40afe1 Fix potential bug found by zinnerz
We might !stop account waiting in invalid password or game playing condition, which will then initiate connect without checking if it's still valid to do so
2016-04-16 19:24:04 +02:00
JustArchi
387f0dd1c7 Bump 2016-04-15 21:33:58 +02:00
JustArchi
8b4d3c219c Remove GUI app from final zip until I'm happy with the way how it works 2016-04-15 21:29:27 +02:00
JustArchi
365877ec89 Misc 2016-04-15 15:08:50 +02:00
JustArchi
f03a43d573 Misc 2016-04-15 00:32:55 +02:00
JustArchi
d15a9cbfca Misc 2016-04-15 00:18:24 +02:00
JustArchi
acfad624fb Bump 2016-04-14 22:44:35 +02:00
20 changed files with 784 additions and 231 deletions

View File

@@ -1,4 +1,3 @@
sudo: false
language: csharp language: csharp
solution: ArchiSteamFarm.sln solution: ArchiSteamFarm.sln
@@ -8,3 +7,6 @@ git:
mono: mono:
- weekly - weekly
- latest - latest
notifications:
email: false

View File

@@ -67,7 +67,7 @@ namespace ArchiSteamFarm {
} }
} }
internal readonly List<Notification> Notifications; internal readonly HashSet<Notification> Notifications;
internal NotificationsCallback(JobID jobID, CMsgClientUserNotifications msg) { internal NotificationsCallback(JobID jobID, CMsgClientUserNotifications msg) {
JobID = jobID; JobID = jobID;
@@ -76,7 +76,7 @@ namespace ArchiSteamFarm {
return; return;
} }
Notifications = new List<Notification>(msg.notifications.Count); Notifications = new HashSet<Notification>();
foreach (var notification in msg.notifications) { foreach (var notification in msg.notifications) {
Notifications.Add(new Notification((Notification.ENotificationType) notification.user_notification_type)); Notifications.Add(new Notification((Notification.ENotificationType) notification.user_notification_type));
} }
@@ -90,7 +90,7 @@ namespace ArchiSteamFarm {
} }
if (msg.count_new_items > 0) { if (msg.count_new_items > 0) {
Notifications = new List<Notification>(1) { Notifications = new HashSet<Notification>() {
new Notification(Notification.ENotificationType.Items) new Notification(Notification.ENotificationType.Items)
}; };
} }
@@ -99,7 +99,6 @@ namespace ArchiSteamFarm {
internal sealed class OfflineMessageCallback : CallbackMsg { internal sealed class OfflineMessageCallback : CallbackMsg {
internal readonly uint OfflineMessages; internal readonly uint OfflineMessages;
internal readonly List<uint> Users;
internal OfflineMessageCallback(JobID jobID, CMsgClientOfflineMessageNotification msg) { internal OfflineMessageCallback(JobID jobID, CMsgClientOfflineMessageNotification msg) {
JobID = jobID; JobID = jobID;
@@ -109,7 +108,6 @@ namespace ArchiSteamFarm {
} }
OfflineMessages = msg.offline_messages; OfflineMessages = msg.offline_messages;
Users = msg.friends_with_offline_messages;
} }
} }
@@ -155,8 +153,8 @@ namespace ArchiSteamFarm {
foreach (KeyValue lineItem in lineItems) { foreach (KeyValue lineItem in lineItems) {
uint appID = (uint) lineItem["PackageID"].AsUnsignedLong(); uint appID = (uint) lineItem["PackageID"].AsUnsignedLong();
string gameName = lineItem["ItemDescription"].AsString(); string gameName = lineItem["ItemDescription"].Value;
gameName = WebUtility.UrlDecode(gameName); // Apparently steam expects client to decode sent HTML gameName = WebUtility.HtmlDecode(gameName); // Apparently steam expects client to decode sent HTML
Items.Add(appID, gameName); Items.Add(appID, gameName);
} }
} }

View File

@@ -28,7 +28,6 @@ using SteamKit2;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using System.Net.Http;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
@@ -54,6 +53,53 @@ namespace ArchiSteamFarm {
SteamCommunityURL = (Program.GlobalConfig.ForceHttp ? "http://" : "https://") + SteamCommunity; SteamCommunityURL = (Program.GlobalConfig.ForceHttp ? "http://" : "https://") + SteamCommunity;
} }
private static uint GetAppIDFromMarketHashName(string hashName) {
if (string.IsNullOrEmpty(hashName)) {
return 0;
}
int index = hashName.IndexOf('-');
if (index < 1) {
return 0;
}
uint appID;
if (!uint.TryParse(hashName.Substring(0, index), out appID)) {
return 0;
}
return appID;
}
private static Steam.Item.EType GetItemType(string name) {
if (string.IsNullOrEmpty(name)) {
return Steam.Item.EType.Unknown;
}
switch (name) {
case "Booster Pack":
return Steam.Item.EType.BoosterPack;
case "Coupon":
return Steam.Item.EType.Coupon;
case "Gift":
return Steam.Item.EType.Gift;
case "Steam Gems":
return Steam.Item.EType.SteamGems;
default:
if (name.EndsWith("Emoticon", StringComparison.Ordinal)) {
return Steam.Item.EType.Emoticon;
} else if (name.EndsWith("Foil Trading Card", StringComparison.Ordinal)) {
return Steam.Item.EType.FoilTradingCard;
} else if (name.EndsWith("Profile Background", StringComparison.Ordinal)) {
return Steam.Item.EType.ProfileBackground;
} else if (name.EndsWith("Trading Card", StringComparison.Ordinal)) {
return Steam.Item.EType.TradingCard;
} else {
return Steam.Item.EType.Unknown;
}
}
}
internal ArchiWebHandler(Bot bot) { internal ArchiWebHandler(Bot bot) {
if (bot == null) { if (bot == null) {
throw new ArgumentNullException("bot"); throw new ArgumentNullException("bot");
@@ -133,9 +179,11 @@ namespace ArchiSteamFarm {
} }
internal async Task<bool?> IsLoggedIn() { internal async Task<bool?> IsLoggedIn() {
string request = SteamCommunityURL + "/my/profile";
HtmlDocument htmlDocument = null; HtmlDocument htmlDocument = null;
for (byte i = 0; i < WebBrowser.MaxRetries && htmlDocument == null; i++) { for (byte i = 0; i < WebBrowser.MaxRetries && htmlDocument == null; i++) {
htmlDocument = await WebBrowser.UrlGetToHtmlDocument(SteamCommunityURL + "/my/profile").ConfigureAwait(false); htmlDocument = await WebBrowser.UrlGetToHtmlDocument(request).ConfigureAwait(false);
} }
if (htmlDocument == null) { if (htmlDocument == null) {
@@ -148,15 +196,13 @@ namespace ArchiSteamFarm {
} }
internal async Task<bool> RefreshSessionIfNeeded() { internal async Task<bool> RefreshSessionIfNeeded() {
DateTime now = DateTime.Now; if (DateTime.Now.Subtract(LastSessionRefreshCheck).TotalSeconds < MinSessionTTL) {
if (now.Subtract(LastSessionRefreshCheck).TotalSeconds < MinSessionTTL) {
return true; return true;
} }
await SessionSemaphore.WaitAsync().ConfigureAwait(false); await SessionSemaphore.WaitAsync().ConfigureAwait(false);
now = DateTime.Now; if (DateTime.Now.Subtract(LastSessionRefreshCheck).TotalSeconds < MinSessionTTL) {
if (now.Subtract(LastSessionRefreshCheck).TotalSeconds < MinSessionTTL) {
SessionSemaphore.Release(); SessionSemaphore.Release();
return true; return true;
} }
@@ -166,8 +212,7 @@ namespace ArchiSteamFarm {
bool? isLoggedIn = await IsLoggedIn().ConfigureAwait(false); bool? isLoggedIn = await IsLoggedIn().ConfigureAwait(false);
if (isLoggedIn.GetValueOrDefault(true)) { if (isLoggedIn.GetValueOrDefault(true)) {
result = true; result = true;
now = DateTime.Now; LastSessionRefreshCheck = DateTime.Now;
LastSessionRefreshCheck = now;
} else { } else {
Logging.LogGenericInfo("Refreshing our session!", Bot.BotName); Logging.LogGenericInfo("Refreshing our session!", Bot.BotName);
result = await Bot.RefreshSession().ConfigureAwait(false); result = await Bot.RefreshSession().ConfigureAwait(false);
@@ -222,7 +267,7 @@ namespace ArchiSteamFarm {
return result; return result;
} }
internal List<Steam.TradeOffer> GetTradeOffers() { internal HashSet<Steam.TradeOffer> GetTradeOffers() {
if (string.IsNullOrEmpty(Bot.BotConfig.SteamApiKey)) { if (string.IsNullOrEmpty(Bot.BotConfig.SteamApiKey)) {
return null; return null;
} }
@@ -236,6 +281,7 @@ namespace ArchiSteamFarm {
response = iEconService.GetTradeOffers( response = iEconService.GetTradeOffers(
get_received_offers: 1, get_received_offers: 1,
active_only: 1, active_only: 1,
get_descriptions: 1,
secure: !Program.GlobalConfig.ForceHttp secure: !Program.GlobalConfig.ForceHttp
); );
} catch (Exception e) { } catch (Exception e) {
@@ -249,33 +295,87 @@ namespace ArchiSteamFarm {
return null; return null;
} }
List<Steam.TradeOffer> result = new List<Steam.TradeOffer>(); Dictionary<Tuple<ulong, ulong>, Tuple<uint, Steam.Item.EType>> descriptionMap = new Dictionary<Tuple<ulong, ulong>, Tuple<uint, Steam.Item.EType>>();
foreach (KeyValue description in response["descriptions"].Children) {
ulong classID = description["classid"].AsUnsignedLong();
if (classID == 0) {
continue;
}
ulong instanceID = description["instanceid"].AsUnsignedLong();
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(classID, instanceID);
if (descriptionMap.ContainsKey(key)) {
continue;
}
uint appID = 0;
Steam.Item.EType type = Steam.Item.EType.Unknown;
string hashName = description["market_hash_name"].Value;
if (!string.IsNullOrEmpty(hashName)) {
appID = GetAppIDFromMarketHashName(hashName);
}
string descriptionType = description["type"].Value;
if (!string.IsNullOrEmpty(descriptionType)) {
type = GetItemType(descriptionType);
}
descriptionMap[key] = new Tuple<uint, Steam.Item.EType>(appID, type);
}
HashSet<Steam.TradeOffer> result = new HashSet<Steam.TradeOffer>();
foreach (KeyValue trade in response["trade_offers_received"].Children) { foreach (KeyValue trade in response["trade_offers_received"].Children) {
// TODO: Correct some of these when SK2 with https://github.com/SteamRE/SteamKit/pull/255 gets released
Steam.TradeOffer tradeOffer = new Steam.TradeOffer { Steam.TradeOffer tradeOffer = new Steam.TradeOffer {
tradeofferid = trade["tradeofferid"].AsString(), TradeOfferID = trade["tradeofferid"].AsUnsignedLong(),
accountid_other = (uint) trade["accountid_other"].AsUnsignedLong(), // TODO: Correct this when SK2 with https://github.com/SteamRE/SteamKit/pull/255 gets released OtherSteamID3 = (uint) trade["accountid_other"].AsUnsignedLong(),
trade_offer_state = trade["trade_offer_state"].AsEnum<Steam.TradeOffer.ETradeOfferState>() State = trade["trade_offer_state"].AsEnum<Steam.TradeOffer.ETradeOfferState>()
}; };
foreach (KeyValue item in trade["items_to_give"].Children) { foreach (KeyValue item in trade["items_to_give"].Children) {
tradeOffer.items_to_give.Add(new Steam.Item { Steam.Item steamItem = new Steam.Item {
appid = item["appid"].AsString(), AppID = (uint) item["appid"].AsUnsignedLong(),
contextid = item["contextid"].AsString(), ContextID = item["contextid"].AsUnsignedLong(),
assetid = item["assetid"].AsString(), AssetID = item["assetid"].AsUnsignedLong(),
classid = item["classid"].AsString(), ClassID = item["classid"].AsUnsignedLong(),
instanceid = item["instanceid"].AsString(), InstanceID = item["instanceid"].AsUnsignedLong(),
amount = item["amount"].AsString(), Amount = (uint) item["amount"].AsUnsignedLong()
}); };
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(steamItem.ClassID, steamItem.InstanceID);
Tuple<uint, Steam.Item.EType> description;
if (descriptionMap.TryGetValue(key, out description)) {
steamItem.RealAppID = description.Item1;
steamItem.Type = description.Item2;
} }
tradeOffer.ItemsToGive.Add(steamItem);
}
foreach (KeyValue item in trade["items_to_receive"].Children) { foreach (KeyValue item in trade["items_to_receive"].Children) {
tradeOffer.items_to_receive.Add(new Steam.Item { Steam.Item steamItem = new Steam.Item {
appid = item["appid"].AsString(), AppID = (uint) item["appid"].AsUnsignedLong(),
contextid = item["contextid"].AsString(), ContextID = item["contextid"].AsUnsignedLong(),
assetid = item["assetid"].AsString(), AssetID = item["assetid"].AsUnsignedLong(),
classid = item["classid"].AsString(), ClassID = item["classid"].AsUnsignedLong(),
instanceid = item["instanceid"].AsString(), InstanceID = item["instanceid"].AsUnsignedLong(),
amount = item["amount"].AsString(), Amount = (uint) item["amount"].AsUnsignedLong()
}); };
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(steamItem.ClassID, steamItem.InstanceID);
Tuple<uint, Steam.Item.EType> description;
if (descriptionMap.TryGetValue(key, out description)) {
steamItem.RealAppID = description.Item1;
steamItem.Type = description.Item2;
} }
tradeOffer.ItemsToReceive.Add(steamItem);
}
result.Add(tradeOffer); result.Add(tradeOffer);
} }
@@ -300,8 +400,8 @@ namespace ArchiSteamFarm {
string request = SteamCommunityURL + "/gid/" + clanID; string request = SteamCommunityURL + "/gid/" + clanID;
Dictionary<string, string> data = new Dictionary<string, string>(2) { Dictionary<string, string> data = new Dictionary<string, string>(2) {
{"sessionID", sessionID}, { "sessionID", sessionID },
{"action", "join"} { "action", "join" }
}; };
bool result = false; bool result = false;
@@ -336,9 +436,9 @@ namespace ArchiSteamFarm {
string request = referer + "/accept"; string request = referer + "/accept";
Dictionary<string, string> data = new Dictionary<string, string>(3) { Dictionary<string, string> data = new Dictionary<string, string>(3) {
{"sessionid", sessionID}, { "sessionid", sessionID },
{"serverid", "1"}, { "serverid", "1" },
{"tradeofferid", tradeID.ToString()} { "tradeofferid", tradeID.ToString() }
}; };
bool result = false; bool result = false;
@@ -354,14 +454,20 @@ namespace ArchiSteamFarm {
return true; return true;
} }
internal async Task<List<Steam.Item>> GetMyTradableInventory() { internal async Task<HashSet<Steam.Item>> GetMyTradableInventory() {
if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) {
return null; return null;
} }
HashSet<Steam.Item> result = new HashSet<Steam.Item>();
ushort nextPage = 0;
while (true) {
string request = SteamCommunityURL + "/my/inventory/json/" + Steam.Item.SteamAppID + "/" + Steam.Item.SteamContextID + "?trading=1&start=" + nextPage;
JObject jObject = null; JObject jObject = null;
for (byte i = 0; i < WebBrowser.MaxRetries && jObject == null; i++) { for (byte i = 0; i < WebBrowser.MaxRetries && jObject == null; i++) {
jObject = await WebBrowser.UrlGetToJObject(SteamCommunityURL + "/my/inventory/json/753/6?trading=1").ConfigureAwait(false); jObject = await WebBrowser.UrlGetToJObject(request).ConfigureAwait(false);
} }
if (jObject == null) { if (jObject == null) {
@@ -369,25 +475,99 @@ namespace ArchiSteamFarm {
return null; return null;
} }
IEnumerable<JToken> jTokens = jObject.SelectTokens("$.rgInventory.*"); IEnumerable<JToken> descriptions = jObject.SelectTokens("$.rgDescriptions.*");
if (jTokens == null) { if (descriptions == null) {
Logging.LogNullError("jTokens", Bot.BotName);
return null; return null;
} }
List<Steam.Item> result = new List<Steam.Item>(); Dictionary<Tuple<ulong, ulong>, Tuple<uint, Steam.Item.EType>> descriptionMap = new Dictionary<Tuple<ulong, ulong>, Tuple<uint, Steam.Item.EType>>();
foreach (JToken jToken in jTokens) { foreach (JToken description in descriptions) {
string classIDString = description["classid"].ToString();
if (string.IsNullOrEmpty(classIDString)) {
continue;
}
ulong classID;
if (!ulong.TryParse(classIDString, out classID) || classID == 0) {
continue;
}
string instanceIDString = description["instanceid"].ToString();
if (string.IsNullOrEmpty(instanceIDString)) {
continue;
}
ulong instanceID;
if (!ulong.TryParse(instanceIDString, out instanceID)) {
continue;
}
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(classID, instanceID);
if (descriptionMap.ContainsKey(key)) {
continue;
}
uint appID = 0;
Steam.Item.EType type = Steam.Item.EType.Unknown;
string hashName = description["market_hash_name"].ToString();
if (!string.IsNullOrEmpty(hashName)) {
appID = GetAppIDFromMarketHashName(hashName);
}
string descriptionType = description["type"].ToString();
if (!string.IsNullOrEmpty(descriptionType)) {
type = GetItemType(descriptionType);
}
descriptionMap[key] = new Tuple<uint, Steam.Item.EType>(appID, type);
}
IEnumerable<JToken> items = jObject.SelectTokens("$.rgInventory.*");
if (descriptions == null) {
return null;
}
foreach (JToken item in items) {
Steam.Item steamItem;
try { try {
result.Add(JsonConvert.DeserializeObject<Steam.Item>(jToken.ToString())); steamItem = JsonConvert.DeserializeObject<Steam.Item>(item.ToString());
} catch (Exception e) { } catch (JsonException e) {
Logging.LogGenericException(e, Bot.BotName); Logging.LogGenericException(e, Bot.BotName);
continue;
}
if (steamItem == null) {
continue;
}
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(steamItem.ClassID, steamItem.InstanceID);
Tuple<uint, Steam.Item.EType> description;
if (descriptionMap.TryGetValue(key, out description)) {
steamItem.RealAppID = description.Item1;
steamItem.Type = description.Item2;
}
result.Add(steamItem);
}
bool more;
if (!bool.TryParse(jObject["more"].ToString(), out more) || !more) {
break;
}
if (!ushort.TryParse(jObject["more_start"].ToString(), out nextPage)) {
break;
} }
} }
return result; return result;
} }
internal async Task<bool> SendTradeOffer(List<Steam.Item> inventory, ulong partnerID, string token = null) { internal async Task<bool> SendTradeOffer(HashSet<Steam.Item> inventory, ulong partnerID, string token = null) {
if (inventory == null || inventory.Count == 0 || partnerID == 0) { if (inventory == null || inventory.Count == 0 || partnerID == 0) {
return false; return false;
} }
@@ -402,26 +582,30 @@ namespace ArchiSteamFarm {
return false; return false;
} }
List<Steam.TradeOfferRequest> trades = new List<Steam.TradeOfferRequest>(1 + inventory.Count / Trading.MaxItemsPerTrade); HashSet<Steam.TradeOfferRequest> trades = new HashSet<Steam.TradeOfferRequest>();
Steam.TradeOfferRequest singleTrade = null; Steam.TradeOfferRequest singleTrade = null;
for (ushort i = 0; i < inventory.Count; i++) {
if (i % Trading.MaxItemsPerTrade == 0) { byte itemID = 0;
foreach (Steam.Item item in inventory) {
if (itemID % Trading.MaxItemsPerTrade == 0) {
if (trades.Count >= Trading.MaxTradesPerAccount) { if (trades.Count >= Trading.MaxTradesPerAccount) {
break; break;
} }
singleTrade = new Steam.TradeOfferRequest(); singleTrade = new Steam.TradeOfferRequest();
trades.Add(singleTrade); trades.Add(singleTrade);
itemID = 0;
} }
Steam.Item item = inventory[i]; singleTrade.ItemsToGive.Assets.Add(new Steam.Item() {
singleTrade.me.assets.Add(new Steam.Item() { AppID = Steam.Item.SteamAppID,
appid = "753", ContextID = Steam.Item.SteamContextID,
contextid = "6", Amount = item.Amount,
amount = item.amount, AssetID = item.AssetID
assetid = item.id
}); });
itemID++;
} }
string referer = SteamCommunityURL + "/tradeoffer/new"; string referer = SteamCommunityURL + "/tradeoffer/new";
@@ -429,12 +613,12 @@ namespace ArchiSteamFarm {
foreach (Steam.TradeOfferRequest trade in trades) { foreach (Steam.TradeOfferRequest trade in trades) {
Dictionary<string, string> data = new Dictionary<string, string>(6) { Dictionary<string, string> data = new Dictionary<string, string>(6) {
{"sessionid", sessionID}, { "sessionid", sessionID },
{"serverid", "1"}, { "serverid", "1" },
{"partner", partnerID.ToString()}, { "partner", partnerID.ToString() },
{"tradeoffermessage", "Sent by ASF"}, { "tradeoffermessage", "Sent by ASF" },
{"json_tradeoffer", JsonConvert.SerializeObject(trade)}, { "json_tradeoffer", JsonConvert.SerializeObject(trade) },
{"trade_offer_create_params", string.IsNullOrEmpty(token) ? "" : $"{{\"trade_offer_access_token\":\"{token}\"}}"} { "trade_offer_create_params", string.IsNullOrEmpty(token) ? "" : $"{{\"trade_offer_access_token\":\"{token}\"}}" }
}; };
bool result = false; bool result = false;
@@ -460,9 +644,11 @@ namespace ArchiSteamFarm {
return null; return null;
} }
string request = SteamCommunityURL + "/my/badges?p=" + page;
HtmlDocument htmlDocument = null; HtmlDocument htmlDocument = null;
for (byte i = 0; i < WebBrowser.MaxRetries && htmlDocument == null; i++) { for (byte i = 0; i < WebBrowser.MaxRetries && htmlDocument == null; i++) {
htmlDocument = await WebBrowser.UrlGetToHtmlDocument(SteamCommunityURL + "/my/badges?p=" + page).ConfigureAwait(false); htmlDocument = await WebBrowser.UrlGetToHtmlDocument(request).ConfigureAwait(false);
} }
if (htmlDocument == null) { if (htmlDocument == null) {
@@ -482,9 +668,11 @@ namespace ArchiSteamFarm {
return null; return null;
} }
string request = SteamCommunityURL + "/my/gamecards/" + appID;
HtmlDocument htmlDocument = null; HtmlDocument htmlDocument = null;
for (byte i = 0; i < WebBrowser.MaxRetries && htmlDocument == null; i++) { for (byte i = 0; i < WebBrowser.MaxRetries && htmlDocument == null; i++) {
htmlDocument = await WebBrowser.UrlGetToHtmlDocument(SteamCommunityURL + "/my/gamecards/" + appID).ConfigureAwait(false); htmlDocument = await WebBrowser.UrlGetToHtmlDocument(request).ConfigureAwait(false);
} }
if (htmlDocument == null) { if (htmlDocument == null) {
@@ -500,9 +688,11 @@ namespace ArchiSteamFarm {
return false; return false;
} }
string request = SteamCommunityURL + "/my/inventory";
bool result = false; bool result = false;
for (byte i = 0; i < WebBrowser.MaxRetries && !result; i++) { for (byte i = 0; i < WebBrowser.MaxRetries && !result; i++) {
result = await WebBrowser.UrlGet(SteamCommunityURL + "/my/inventory").ConfigureAwait(false); result = await WebBrowser.UrlGet(request).ConfigureAwait(false);
} }
if (!result) { if (!result) {

View File

@@ -33,6 +33,7 @@ using System.Security.Cryptography;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
namespace ArchiSteamFarm { namespace ArchiSteamFarm {
internal sealed class Bot { internal sealed class Bot {
@@ -100,9 +101,8 @@ namespace ArchiSteamFarm {
} }
// Steam keys are offered in many formats: https://support.steampowered.com/kb_article.php?ref=7480-WUSF-3601 // Steam keys are offered in many formats: https://support.steampowered.com/kb_article.php?ref=7480-WUSF-3601
// It's pointless to implement them all, so we'll just do a simple check if key is supposed to be valid // This regex should catch all of them, we can always further extend it in future
// Every valid key, apart from Prey one has at least two dashes return Regex.IsMatch(key, @"[0-9A-Z]{4,5}-[0-9A-Z]{4,5}-[0-9A-Z]{4,5}-?(?:(?:[0-9A-Z]{4,5}-?)?(?:[0-9A-Z]{4,5}))?");
return Utilities.GetCharCountInString(key, '-') >= 2;
} }
internal Bot(string botName) { internal Bot(string botName) {
@@ -295,8 +295,8 @@ namespace ArchiSteamFarm {
return true; return true;
} }
internal async Task OnFarmingFinished(bool farmedSomething) { internal async Task OnFarmingFinished() {
if (farmedSomething && BotConfig.SendOnFarmingFinished) { if (BotConfig.SendOnFarmingFinished) {
await ResponseSendTrade(BotConfig.SteamMasterID).ConfigureAwait(false); await ResponseSendTrade(BotConfig.SteamMasterID).ConfigureAwait(false);
} }
@@ -310,11 +310,11 @@ namespace ArchiSteamFarm {
return null; return null;
} }
if (!message.StartsWith("!")) { if (message[0] != '!') {
return await ResponseRedeem(steamID, message.Replace(",", Environment.NewLine), true).ConfigureAwait(false); return await ResponseRedeem(steamID, message.Replace(",", Environment.NewLine), true).ConfigureAwait(false);
} }
if (!message.Contains(" ")) { if (message.IndexOf(' ') < 0) {
switch (message) { switch (message) {
case "!2fa": case "!2fa":
return Response2FA(steamID); return Response2FA(steamID);
@@ -350,7 +350,7 @@ namespace ArchiSteamFarm {
return ResponseUnknown(steamID); return ResponseUnknown(steamID);
} }
} else { } else {
string[] args = message.Split(' '); string[] args = message.Split((char[]) null, StringSplitOptions.RemoveEmptyEntries);
switch (args[0]) { switch (args[0]) {
case "!2fa": case "!2fa":
return Response2FA(steamID, args[1]); return Response2FA(steamID, args[1]);
@@ -410,8 +410,8 @@ namespace ArchiSteamFarm {
Task.Run(() => HandleCallbacks()).Forget(); Task.Run(() => HandleCallbacks()).Forget();
} }
// 2FA tokens are expiring soon, use limiter only when we don't have any pending // 2FA tokens are expiring soon, use limiter only when user is providing one
if (TwoFactorCode == null) { if (TwoFactorCode == null || BotDatabase.SteamGuardAccount != null) {
await Program.LimitSteamRequestsAsync().ConfigureAwait(false); await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
} }
@@ -463,6 +463,7 @@ namespace ArchiSteamFarm {
Logging.LogGenericInfo("ASF requires a few more steps to complete authenticator import...", BotName); Logging.LogGenericInfo("ASF requires a few more steps to complete authenticator import...", BotName);
if (!InitializeLoginAndPassword()) { if (!InitializeLoginAndPassword()) {
BotDatabase.SteamGuardAccount = null;
return; return;
} }
@@ -473,10 +474,12 @@ namespace ArchiSteamFarm {
case LoginResult.Need2FA: case LoginResult.Need2FA:
userLogin.TwoFactorCode = Program.GetUserInput(Program.EUserInputType.TwoFactorAuthentication, BotName); userLogin.TwoFactorCode = Program.GetUserInput(Program.EUserInputType.TwoFactorAuthentication, BotName);
if (string.IsNullOrEmpty(userLogin.TwoFactorCode)) { if (string.IsNullOrEmpty(userLogin.TwoFactorCode)) {
BotDatabase.SteamGuardAccount = null;
return; return;
} }
break; break;
default: default:
BotDatabase.SteamGuardAccount = null;
Logging.LogGenericError("Unhandled situation: " + loginResult, BotName); Logging.LogGenericError("Unhandled situation: " + loginResult, BotName);
return; return;
} }
@@ -588,12 +591,18 @@ namespace ArchiSteamFarm {
} }
await Trading.LimitInventoryRequestsAsync().ConfigureAwait(false); await Trading.LimitInventoryRequestsAsync().ConfigureAwait(false);
List<Steam.Item> inventory = await ArchiWebHandler.GetMyTradableInventory().ConfigureAwait(false); HashSet<Steam.Item> inventory = await ArchiWebHandler.GetMyTradableInventory().ConfigureAwait(false);
if (inventory == null || inventory.Count == 0) { if (inventory == null || inventory.Count == 0) {
return "Nothing to send, inventory seems empty!"; return "Nothing to send, inventory seems empty!";
} }
// Remove from our pending inventory all items that are not steam cards and boosters
inventory.RemoveWhere(item => item.Type != Steam.Item.EType.TradingCard && item.Type != Steam.Item.EType.FoilTradingCard && item.Type != Steam.Item.EType.BoosterPack);
if (inventory.Count == 0) {
return "Nothing to send, inventory seems empty!";
}
if (await ArchiWebHandler.SendTradeOffer(inventory, BotConfig.SteamMasterID, BotConfig.SteamTradeToken).ConfigureAwait(false)) { if (await ArchiWebHandler.SendTradeOffer(inventory, BotConfig.SteamMasterID, BotConfig.SteamTradeToken).ConfigureAwait(false)) {
await AcceptConfirmations(true, Confirmation.ConfirmationType.Trade).ConfigureAwait(false); await AcceptConfirmations(true, Confirmation.ConfirmationType.Trade).ConfigureAwait(false);
return "Trade offer sent successfully!"; return "Trade offer sent successfully!";
@@ -774,12 +783,16 @@ namespace ArchiSteamFarm {
while (!string.IsNullOrEmpty(key) && currentBot != null) { while (!string.IsNullOrEmpty(key) && currentBot != null) {
if (validate && !IsValidCdKey(key)) { if (validate && !IsValidCdKey(key)) {
key = reader.ReadLine(); // Next key key = reader.ReadLine(); // Next key
continue; // Without changing the bot continue; // Keep current bot
} }
if (currentBot.SteamClient.IsConnected) { if (!currentBot.SteamClient.IsConnected) {
currentBot = null; // Either bot will be changed, or loop aborted
} else {
ArchiHandler.PurchaseResponseCallback result = await currentBot.ArchiHandler.RedeemKey(key).ConfigureAwait(false); ArchiHandler.PurchaseResponseCallback result = await currentBot.ArchiHandler.RedeemKey(key).ConfigureAwait(false);
if (result != null) { if (result == null) {
currentBot = null; // Either bot will be changed, or loop aborted
} else {
switch (result.PurchaseResult) { switch (result.PurchaseResult) {
case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.DuplicatedKey: case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.DuplicatedKey:
case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.InvalidKey: case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.InvalidKey:
@@ -787,7 +800,12 @@ namespace ArchiSteamFarm {
response.Append(Environment.NewLine + "<" + currentBot.BotName + "> Key: " + key + " | Status: " + result.PurchaseResult + " | Items: " + string.Join("", result.Items)); response.Append(Environment.NewLine + "<" + currentBot.BotName + "> Key: " + key + " | Status: " + result.PurchaseResult + " | Items: " + string.Join("", result.Items));
key = reader.ReadLine(); // Next key key = reader.ReadLine(); // Next key
if (result.PurchaseResult == ArchiHandler.PurchaseResponseCallback.EPurchaseResult.OK) {
break; // Next bot (if needed) break; // Next bot (if needed)
} else {
continue; // Keep current bot
}
case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.AlreadyOwned: case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.AlreadyOwned:
case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.BaseGameRequired: case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.BaseGameRequired:
case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.OnCooldown: case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.OnCooldown:
@@ -1226,15 +1244,28 @@ namespace ArchiSteamFarm {
string sms = Program.GetUserInput(Program.EUserInputType.SMS, BotName); string sms = Program.GetUserInput(Program.EUserInputType.SMS, BotName);
if (string.IsNullOrEmpty(sms)) { if (string.IsNullOrEmpty(sms)) {
Logging.LogGenericWarning("Aborted!", BotName);
DelinkMobileAuthenticator();
return; return;
} }
AuthenticatorLinker.FinalizeResult finalizeResult = authenticatorLinker.FinalizeAddAuthenticator(sms); AuthenticatorLinker.FinalizeResult finalizeResult;
if (finalizeResult != AuthenticatorLinker.FinalizeResult.Success) { while ((finalizeResult = authenticatorLinker.FinalizeAddAuthenticator(sms)) != AuthenticatorLinker.FinalizeResult.Success) {
switch (finalizeResult) {
case AuthenticatorLinker.FinalizeResult.BadSMSCode:
sms = Program.GetUserInput(Program.EUserInputType.SMS, BotName);
if (string.IsNullOrEmpty(sms)) {
Logging.LogGenericWarning("Aborted!", BotName);
DelinkMobileAuthenticator();
return;
}
break;
default:
Logging.LogGenericError("Unhandled situation: " + finalizeResult, BotName); Logging.LogGenericError("Unhandled situation: " + finalizeResult, BotName);
DelinkMobileAuthenticator(); DelinkMobileAuthenticator();
return; return;
} }
}
// Ensure that we also save changes made by finalization step (if any) // Ensure that we also save changes made by finalization step (if any)
BotDatabase.Save(); BotDatabase.Save();
@@ -1248,13 +1279,13 @@ namespace ArchiSteamFarm {
return false; return false;
} }
bool result = BotDatabase.SteamGuardAccount.DeactivateAuthenticator(); // Try to deactivate authenticator, and assume we're safe to remove if it wasn't fully enrolled yet (even if request fails)
if (BotDatabase.SteamGuardAccount.DeactivateAuthenticator() || !BotDatabase.SteamGuardAccount.FullyEnrolled) {
if (result) {
BotDatabase.SteamGuardAccount = null; BotDatabase.SteamGuardAccount = null;
return true;
} }
return result; return false;
} }
private void JoinMasterChat() { private void JoinMasterChat() {
@@ -1364,10 +1395,6 @@ namespace ArchiSteamFarm {
return; return;
} }
if (!KeepRunning) {
return;
}
if (InvalidPassword) { if (InvalidPassword) {
InvalidPassword = false; InvalidPassword = false;
if (!string.IsNullOrEmpty(BotDatabase.LoginKey)) { // InvalidPassword means usually that login key has expired, if we used it if (!string.IsNullOrEmpty(BotDatabase.LoginKey)) { // InvalidPassword means usually that login key has expired, if we used it
@@ -1389,10 +1416,14 @@ namespace ArchiSteamFarm {
await Utilities.SleepAsync(Program.GlobalConfig.AccountPlayingDelay * 60 * 1000).ConfigureAwait(false); await Utilities.SleepAsync(Program.GlobalConfig.AccountPlayingDelay * 60 * 1000).ConfigureAwait(false);
} }
if (!KeepRunning || SteamClient.IsConnected) {
return;
}
Logging.LogGenericInfo("Reconnecting...", BotName); Logging.LogGenericInfo("Reconnecting...", BotName);
// 2FA tokens are expiring soon, use limiter only when we don't have any pending // 2FA tokens are expiring soon, use limiter only when user is providing one
if (TwoFactorCode == null) { if (TwoFactorCode == null || BotDatabase.SteamGuardAccount != null) {
await Program.LimitSteamRequestsAsync().ConfigureAwait(false); await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
} }
@@ -1714,7 +1745,7 @@ namespace ArchiSteamFarm {
} }
private void OnOfflineMessage(ArchiHandler.OfflineMessageCallback callback) { private void OnOfflineMessage(ArchiHandler.OfflineMessageCallback callback) {
if (callback == null) { if (callback == null || callback.OfflineMessages == 0) {
return; return;
} }

View File

@@ -68,6 +68,9 @@ namespace ArchiSteamFarm {
[JsonProperty(Required = Required.DisallowNull)] [JsonProperty(Required = Required.DisallowNull)]
internal bool AcceptGifts { get; private set; } = false; internal bool AcceptGifts { get; private set; } = false;
[JsonProperty(Required = Required.DisallowNull)]
internal bool SteamTradeMatcher { get; private set; } = false;
[JsonProperty(Required = Required.DisallowNull)] [JsonProperty(Required = Required.DisallowNull)]
internal bool ForwardKeysToOtherBots { get; private set; } = false; internal bool ForwardKeysToOtherBots { get; private set; } = false;

View File

@@ -37,7 +37,7 @@ namespace ArchiSteamFarm {
internal readonly ConcurrentDictionary<uint, float> GamesToFarm = new ConcurrentDictionary<uint, float>(); internal readonly ConcurrentDictionary<uint, float> GamesToFarm = new ConcurrentDictionary<uint, float>();
internal readonly HashSet<uint> CurrentGamesFarming = new HashSet<uint>(); internal readonly HashSet<uint> CurrentGamesFarming = new HashSet<uint>();
private readonly ManualResetEvent FarmResetEvent = new ManualResetEvent(false); private readonly ManualResetEventSlim FarmResetEvent = new ManualResetEventSlim(false);
private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1); private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1);
private readonly Bot Bot; private readonly Bot Bot;
private readonly Timer Timer; private readonly Timer Timer;
@@ -94,7 +94,7 @@ namespace ArchiSteamFarm {
if (!await IsAnythingToFarm().ConfigureAwait(false)) { if (!await IsAnythingToFarm().ConfigureAwait(false)) {
Semaphore.Release(); // We have nothing to do, don't forget to release semaphore Semaphore.Release(); // We have nothing to do, don't forget to release semaphore
Logging.LogGenericInfo("We don't have anything to farm on this account!", Bot.BotName); Logging.LogGenericInfo("We don't have anything to farm on this account!", Bot.BotName);
await Bot.OnFarmingFinished(false).ConfigureAwait(false); await Bot.OnFarmingFinished().ConfigureAwait(false);
return; return;
} }
@@ -102,8 +102,6 @@ namespace ArchiSteamFarm {
NowFarming = true; NowFarming = true;
Semaphore.Release(); // From this point we allow other calls to shut us down Semaphore.Release(); // From this point we allow other calls to shut us down
bool farmedSomething = false;
do { do {
// Now the algorithm used for farming depends on whether account is restricted or not // Now the algorithm used for farming depends on whether account is restricted or not
if (Bot.BotConfig.CardDropsRestricted) { // If we have restricted card drops, we use complex algorithm if (Bot.BotConfig.CardDropsRestricted) { // If we have restricted card drops, we use complex algorithm
@@ -114,7 +112,6 @@ namespace ArchiSteamFarm {
while (gamesToFarmSolo.Count > 0) { while (gamesToFarmSolo.Count > 0) {
uint appID = gamesToFarmSolo.First(); uint appID = gamesToFarmSolo.First();
if (await FarmSolo(appID).ConfigureAwait(false)) { if (await FarmSolo(appID).ConfigureAwait(false)) {
farmedSomething = true;
gamesToFarmSolo.Remove(appID); gamesToFarmSolo.Remove(appID);
gamesToFarmSolo.TrimExcess(); gamesToFarmSolo.TrimExcess();
} else { } else {
@@ -135,9 +132,7 @@ namespace ArchiSteamFarm {
Logging.LogGenericInfo("Chosen farming algorithm: Simple", Bot.BotName); Logging.LogGenericInfo("Chosen farming algorithm: Simple", Bot.BotName);
while (GamesToFarm.Count > 0) { while (GamesToFarm.Count > 0) {
uint appID = GamesToFarm.Keys.FirstOrDefault(); uint appID = GamesToFarm.Keys.FirstOrDefault();
if (await FarmSolo(appID).ConfigureAwait(false)) { if (!await FarmSolo(appID).ConfigureAwait(false)) {
farmedSomething = true;
} else {
NowFarming = false; NowFarming = false;
return; return;
} }
@@ -150,7 +145,7 @@ namespace ArchiSteamFarm {
NowFarming = false; NowFarming = false;
Logging.LogGenericInfo("Farming finished!", Bot.BotName); Logging.LogGenericInfo("Farming finished!", Bot.BotName);
await Bot.OnFarmingFinished(farmedSomething).ConfigureAwait(false); await Bot.OnFarmingFinished().ConfigureAwait(false);
} }
internal async Task StopFarming() { internal async Task StopFarming() {
@@ -421,7 +416,7 @@ namespace ArchiSteamFarm {
bool? keepFarming = await ShouldFarm(appID).ConfigureAwait(false); bool? keepFarming = await ShouldFarm(appID).ConfigureAwait(false);
for (ushort farmingTime = 0; farmingTime <= 60 * Program.GlobalConfig.MaxFarmingTime && keepFarming.GetValueOrDefault(true); farmingTime += Program.GlobalConfig.FarmingDelay) { for (ushort farmingTime = 0; farmingTime <= 60 * Program.GlobalConfig.MaxFarmingTime && keepFarming.GetValueOrDefault(true); farmingTime += Program.GlobalConfig.FarmingDelay) {
if (FarmResetEvent.WaitOne(60 * 1000 * Program.GlobalConfig.FarmingDelay)) { if (FarmResetEvent.Wait(60 * 1000 * Program.GlobalConfig.FarmingDelay)) {
success = false; success = false;
break; break;
} }
@@ -452,7 +447,7 @@ namespace ArchiSteamFarm {
bool success = true; bool success = true;
while (maxHour < 2) { while (maxHour < 2) {
if (FarmResetEvent.WaitOne(60 * 1000 * Program.GlobalConfig.FarmingDelay)) { if (FarmResetEvent.Wait(60 * 1000 * Program.GlobalConfig.FarmingDelay)) {
success = false; success = false;
break; break;
} }

View File

@@ -55,6 +55,9 @@ namespace ArchiSteamFarm {
[JsonProperty(Required = Required.DisallowNull)] [JsonProperty(Required = Required.DisallowNull)]
internal bool AutoUpdates { get; private set; } = true; internal bool AutoUpdates { get; private set; } = true;
[JsonProperty(Required = Required.DisallowNull)]
internal bool AutoRestart { get; private set; } = true;
[JsonProperty(Required = Required.DisallowNull)] [JsonProperty(Required = Required.DisallowNull)]
internal EUpdateChannel UpdateChannel { get; private set; } = EUpdateChannel.Stable; internal EUpdateChannel UpdateChannel { get; private set; } = EUpdateChannel.Stable;

View File

@@ -28,40 +28,161 @@ using System.Collections.Generic;
namespace ArchiSteamFarm { namespace ArchiSteamFarm {
internal static class Steam { internal static class Steam {
internal sealed class Item { internal sealed class Item { // REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_Asset
// REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_Asset internal const ushort SteamAppID = 753;
[JsonProperty(Required = Required.DisallowNull)] internal const byte SteamContextID = 6;
internal string appid { get; set; }
[JsonProperty(Required = Required.DisallowNull)] internal enum EType : byte {
internal string contextid { get; set; } Unknown,
[JsonProperty(Required = Required.DisallowNull)] BoosterPack,
internal string assetid { get; set; } Coupon,
Gift,
SteamGems,
[JsonProperty(Required = Required.DisallowNull)] Emoticon,
internal string id { FoilTradingCard,
get { return assetid; } ProfileBackground,
set { assetid = value; } TradingCard
} }
[JsonProperty(Required = Required.AllowNull)] internal uint AppID;
internal string classid { get; set; }
[JsonProperty(Required = Required.AllowNull)] [JsonProperty(PropertyName = "appid", Required = Required.DisallowNull)]
internal string instanceid { get; set; } internal string AppIDString {
get {
[JsonProperty(Required = Required.Always)] return AppID.ToString();
internal string amount { get; set; } }
set {
if (string.IsNullOrEmpty(value)) {
return;
} }
internal sealed class ItemList { uint result;
[JsonProperty(Required = Required.Always)] if (!uint.TryParse(value, out result)) {
internal List<Steam.Item> assets { get; } = new List<Steam.Item>(); return;
} }
internal sealed class TradeOffer { AppID = result;
// REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_TradeOffer }
}
internal ulong ContextID;
[JsonProperty(PropertyName = "contextid", Required = Required.DisallowNull)]
internal string ContextIDString {
get {
return ContextID.ToString();
}
set {
if (string.IsNullOrEmpty(value)) {
return;
}
ulong result;
if (!ulong.TryParse(value, out result)) {
return;
}
ContextID = result;
}
}
internal ulong AssetID;
[JsonProperty(PropertyName = "assetid", Required = Required.DisallowNull)]
internal string AssetIDString {
get {
return AssetID.ToString();
}
set {
if (string.IsNullOrEmpty(value)) {
return;
}
ulong result;
if (!ulong.TryParse(value, out result)) {
return;
}
AssetID = result;
}
}
[JsonProperty(PropertyName = "id", Required = Required.DisallowNull)]
internal string ID {
get { return AssetIDString; }
set { AssetIDString = value; }
}
internal ulong ClassID;
[JsonProperty(PropertyName = "classid", Required = Required.DisallowNull)]
internal string ClassIDString {
get {
return ClassID.ToString();
}
set {
if (string.IsNullOrEmpty(value)) {
return;
}
ulong result;
if (!ulong.TryParse(value, out result)) {
return;
}
ClassID = result;
}
}
internal ulong InstanceID;
[JsonProperty(PropertyName = "instanceid", Required = Required.DisallowNull)]
internal string InstanceIDString {
get {
return InstanceID.ToString();
}
set {
if (string.IsNullOrEmpty(value)) {
return;
}
ulong result;
if (!ulong.TryParse(value, out result)) {
return;
}
InstanceID = result;
}
}
internal uint Amount;
[JsonProperty(PropertyName = "amount", Required = Required.Always)]
internal string AmountString {
get {
return Amount.ToString();
}
set {
if (string.IsNullOrEmpty(value)) {
return;
}
uint result;
if (!uint.TryParse(value, out result)) {
return;
}
Amount = result;
}
}
internal uint RealAppID { get; set; }
internal EType Type { get; set; }
}
internal sealed class TradeOffer { // REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_TradeOffer
internal enum ETradeOfferState : byte { internal enum ETradeOfferState : byte {
Unknown, Unknown,
Invalid, Invalid,
@@ -77,46 +198,148 @@ namespace ArchiSteamFarm {
OnHold OnHold
} }
[JsonProperty(Required = Required.Always)] internal ulong TradeOfferID;
internal string tradeofferid { get; set; }
[JsonProperty(Required = Required.Always)] [JsonProperty(PropertyName = "tradeofferid", Required = Required.Always)]
internal uint accountid_other { get; set; } internal string TradeOfferIDString {
get {
return TradeOfferID.ToString();
}
set {
if (string.IsNullOrEmpty(value)) {
return;
}
[JsonProperty(Required = Required.Always)] ulong result;
internal ETradeOfferState trade_offer_state { get; set; } if (!ulong.TryParse(value, out result)) {
return;
}
[JsonProperty(Required = Required.Always)] TradeOfferID = result;
internal List<Steam.Item> items_to_give { get; } = new List<Steam.Item>(); }
}
[JsonProperty(Required = Required.Always)] [JsonProperty(PropertyName = "accountid_other", Required = Required.Always)]
internal List<Steam.Item> items_to_receive { get; } = new List<Steam.Item>(); internal uint OtherSteamID3 { get; set; }
[JsonProperty(PropertyName = "trade_offer_state", Required = Required.Always)]
internal ETradeOfferState State { get; set; }
[JsonProperty(PropertyName = "items_to_give", Required = Required.Always)]
internal HashSet<Item> ItemsToGive { get; } = new HashSet<Item>();
[JsonProperty(PropertyName = "items_to_receive", Required = Required.Always)]
internal HashSet<Item> ItemsToReceive { get; } = new HashSet<Item>();
// Extra // Extra
private ulong _OtherSteamID64 = 0;
internal ulong OtherSteamID64 { internal ulong OtherSteamID64 {
get { get {
if (_OtherSteamID64 == 0 && accountid_other != 0) { if (OtherSteamID3 == 0) {
_OtherSteamID64 = new SteamID(accountid_other, EUniverse.Public, EAccountType.Individual).ConvertToUInt64(); return 0;
} }
return _OtherSteamID64; return new SteamID(OtherSteamID3, EUniverse.Public, EAccountType.Individual);
} }
set {
if (value == 0) {
return;
}
OtherSteamID3 = new SteamID(value).AccountID;
}
}
internal bool IsSteamCardsOnlyTrade() {
foreach (Item item in ItemsToGive) {
if (item.AppID != Item.SteamAppID || item.ContextID != Item.SteamContextID || (item.Type != Item.EType.FoilTradingCard && item.Type != Item.EType.TradingCard)) {
return false;
}
}
foreach (Item item in ItemsToReceive) {
if (item.AppID != Item.SteamAppID || item.ContextID != Item.SteamContextID || (item.Type != Item.EType.FoilTradingCard && item.Type != Item.EType.TradingCard)) {
return false;
}
}
return true;
}
internal bool IsPotentiallyDupesTrade() {
Dictionary<uint, Dictionary<Item.EType, uint>> ItemsToGivePerGame = new Dictionary<uint, Dictionary<Item.EType, uint>>();
foreach (Item item in ItemsToGive) {
Dictionary<Item.EType, uint> ItemsPerType;
if (!ItemsToGivePerGame.TryGetValue(item.RealAppID, out ItemsPerType)) {
ItemsPerType = new Dictionary<Item.EType, uint>();
ItemsPerType[item.Type] = item.Amount;
ItemsToGivePerGame[item.RealAppID] = ItemsPerType;
} else {
uint amount;
if (ItemsPerType.TryGetValue(item.Type, out amount)) {
ItemsPerType[item.Type] = amount + item.Amount;
} else {
ItemsPerType[item.Type] = item.Amount;
}
}
}
Dictionary<uint, Dictionary<Item.EType, uint>> ItemsToReceivePerGame = new Dictionary<uint, Dictionary<Item.EType, uint>>();
foreach (Item item in ItemsToReceive) {
Dictionary<Item.EType, uint> ItemsPerType;
if (!ItemsToReceivePerGame.TryGetValue(item.RealAppID, out ItemsPerType)) {
ItemsPerType = new Dictionary<Item.EType, uint>();
ItemsPerType[item.Type] = item.Amount;
ItemsToReceivePerGame[item.RealAppID] = ItemsPerType;
} else {
uint amount;
if (ItemsPerType.TryGetValue(item.Type, out amount)) {
ItemsPerType[item.Type] = amount + item.Amount;
} else {
ItemsPerType[item.Type] = item.Amount;
}
}
}
// Ensure that amount per type and per game matches
foreach (KeyValuePair<uint, Dictionary<Item.EType, uint>> ItemsPerGame in ItemsToGivePerGame) {
Dictionary<Item.EType, uint> otherItemsPerType;
if (!ItemsToReceivePerGame.TryGetValue(ItemsPerGame.Key, out otherItemsPerType)) {
return false;
}
foreach (KeyValuePair<Item.EType, uint> ItemsPerType in ItemsPerGame.Value) {
uint otherAmount;
if (!otherItemsPerType.TryGetValue(ItemsPerType.Key, out otherAmount)) {
return false;
}
if (ItemsPerType.Value != otherAmount) {
return false;
}
}
}
return true;
} }
} }
internal sealed class TradeOfferRequest { internal sealed class TradeOfferRequest {
[JsonProperty(Required = Required.Always)] internal sealed class ItemList {
internal bool newversion { get; } = true; [JsonProperty(PropertyName = "assets", Required = Required.Always)]
internal HashSet<Item> Assets { get; } = new HashSet<Item>();
}
[JsonProperty(Required = Required.Always)] [JsonProperty(PropertyName = "newversion", Required = Required.Always)]
internal int version { get; } = 2; internal bool NewVersion { get; } = true;
[JsonProperty(Required = Required.Always)] [JsonProperty(PropertyName = "version", Required = Required.Always)]
internal Steam.ItemList me { get; } = new Steam.ItemList(); internal byte Version { get; } = 2;
[JsonProperty(Required = Required.Always)] [JsonProperty(PropertyName = "me", Required = Required.Always)]
internal Steam.ItemList them { get; } = new Steam.ItemList(); internal ItemList ItemsToGive { get; } = new ItemList();
[JsonProperty(PropertyName = "them", Required = Required.Always)]
internal ItemList ItemsToReceive { get; } = new ItemList();
} }
} }
} }

View File

@@ -65,13 +65,12 @@ namespace ArchiSteamFarm {
private const string GithubReleaseURL = "https://api.github.com/repos/" + GithubRepo + "/releases"; // GitHub API is HTTPS only private const string GithubReleaseURL = "https://api.github.com/repos/" + GithubRepo + "/releases"; // GitHub API is HTTPS only
private static readonly Assembly Assembly = Assembly.GetExecutingAssembly(); internal static readonly Version Version = Assembly.GetEntryAssembly().GetName().Version;
internal static readonly Version Version = Assembly.GetName().Version;
private static readonly object ConsoleLock = new object(); private static readonly object ConsoleLock = new object();
private static readonly SemaphoreSlim SteamSemaphore = new SemaphoreSlim(1); private static readonly SemaphoreSlim SteamSemaphore = new SemaphoreSlim(1);
private static readonly ManualResetEvent ShutdownResetEvent = new ManualResetEvent(false); private static readonly ManualResetEventSlim ShutdownResetEvent = new ManualResetEventSlim(false);
private static readonly string ExecutableFile = Assembly.Location; private static readonly string ExecutableFile = Assembly.GetEntryAssembly().Location;
private static readonly string ExecutableName = Path.GetFileName(ExecutableFile); private static readonly string ExecutableName = Path.GetFileName(ExecutableFile);
private static readonly string ExecutableDirectory = Path.GetDirectoryName(ExecutableFile); private static readonly string ExecutableDirectory = Path.GetDirectoryName(ExecutableFile);
private static readonly WCF WCF = new WCF(); private static readonly WCF WCF = new WCF();
@@ -187,7 +186,7 @@ namespace ArchiSteamFarm {
GitHub.Asset binaryAsset = null; GitHub.Asset binaryAsset = null;
foreach (var asset in releaseResponse.Assets) { foreach (var asset in releaseResponse.Assets) {
if (string.IsNullOrEmpty(asset.Name) || !asset.Name.Equals(ExecutableName)) { if (string.IsNullOrEmpty(asset.Name) || !asset.Name.Equals(ExecutableName, StringComparison.OrdinalIgnoreCase)) {
continue; continue;
} }
@@ -251,10 +250,17 @@ namespace ArchiSteamFarm {
return; return;
} }
Logging.LogGenericInfo("Update process is finished! ASF will now restart itself..."); Logging.LogGenericInfo("Update process finished!");
await Utilities.SleepAsync(5000);
if (GlobalConfig.AutoRestart) {
Logging.LogGenericInfo("Restarting...");
await Utilities.SleepAsync(5000).ConfigureAwait(false);
Restart(); Restart();
} else {
Logging.LogGenericInfo("Exiting...");
await Utilities.SleepAsync(5000).ConfigureAwait(false);
Exit();
}
} }
internal static void Exit(int exitCode = 0) { internal static void Exit(int exitCode = 0) {
@@ -516,7 +522,7 @@ namespace ArchiSteamFarm {
Init(args); Init(args);
// Wait for signal to shutdown // Wait for signal to shutdown
ShutdownResetEvent.WaitOne(); ShutdownResetEvent.Wait();
// We got a signal to shutdown // We got a signal to shutdown
Exit(); Exit();

View File

@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("2.0.3.1")] [assembly: AssemblyVersion("2.0.4.0")]
[assembly: AssemblyFileVersion("2.0.3.1")] [assembly: AssemblyFileVersion("2.0.4.0")]

View File

@@ -37,6 +37,7 @@ namespace ArchiSteamFarm {
private readonly Bot Bot; private readonly Bot Bot;
private readonly SemaphoreSlim TradesSemaphore = new SemaphoreSlim(1); private readonly SemaphoreSlim TradesSemaphore = new SemaphoreSlim(1);
private readonly HashSet<ulong> RecentlyParsedTrades = new HashSet<ulong>();
private byte ParsingTasks; private byte ParsingTasks;
@@ -79,43 +80,62 @@ namespace ArchiSteamFarm {
TradesSemaphore.Release(); TradesSemaphore.Release();
} }
private async Task ForgetRecentTrade(ulong tradeID) {
await Utilities.SleepAsync(24 * 60 * 60 * 1000).ConfigureAwait(false);
lock (RecentlyParsedTrades) {
RecentlyParsedTrades.Remove(tradeID);
RecentlyParsedTrades.TrimExcess();
}
}
private async Task ParseActiveTrades() { private async Task ParseActiveTrades() {
List<Steam.TradeOffer> tradeOffers = Bot.ArchiWebHandler.GetTradeOffers(); HashSet<Steam.TradeOffer> tradeOffers = Bot.ArchiWebHandler.GetTradeOffers();
if (tradeOffers == null) { if (tradeOffers == null || tradeOffers.Count == 0) {
return; return;
} }
lock (RecentlyParsedTrades) {
tradeOffers.RemoveWhere(trade => RecentlyParsedTrades.Contains(trade.TradeOfferID));
}
if (tradeOffers.Count == 0) {
return;
}
foreach (Steam.TradeOffer tradeOffer in tradeOffers) {
lock (RecentlyParsedTrades) {
RecentlyParsedTrades.Add(tradeOffer.TradeOfferID);
}
ForgetRecentTrade(tradeOffer.TradeOfferID).Forget();
}
await tradeOffers.ForEachAsync(ParseTrade).ConfigureAwait(false); await tradeOffers.ForEachAsync(ParseTrade).ConfigureAwait(false);
await Bot.AcceptConfirmations(true, Confirmation.ConfirmationType.Trade).ConfigureAwait(false); await Bot.AcceptConfirmations(true, Confirmation.ConfirmationType.Trade).ConfigureAwait(false);
} }
private async Task ParseTrade(Steam.TradeOffer tradeOffer) { private async Task ParseTrade(Steam.TradeOffer tradeOffer) {
if (tradeOffer == null || tradeOffer.trade_offer_state != Steam.TradeOffer.ETradeOfferState.Active) { if (tradeOffer == null || tradeOffer.State != Steam.TradeOffer.ETradeOfferState.Active) {
return; return;
} }
ulong tradeID; if (await ShouldAcceptTrade(tradeOffer).ConfigureAwait(false)) {
if (!ulong.TryParse(tradeOffer.tradeofferid, out tradeID)) { Logging.LogGenericInfo("Accepting trade: " + tradeOffer.TradeOfferID, Bot.BotName);
return; await Bot.ArchiWebHandler.AcceptTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false);
}
if (ShouldAcceptTrade(tradeOffer)) {
Logging.LogGenericInfo("Accepting trade: " + tradeID, Bot.BotName);
await Bot.ArchiWebHandler.AcceptTradeOffer(tradeID).ConfigureAwait(false);
} else { } else {
Logging.LogGenericInfo("Ignoring trade: " + tradeID, Bot.BotName); Logging.LogGenericInfo("Ignoring trade: " + tradeOffer.TradeOfferID, Bot.BotName);
} }
} }
private bool ShouldAcceptTrade(Steam.TradeOffer tradeOffer) { private async Task<bool> ShouldAcceptTrade(Steam.TradeOffer tradeOffer) {
if (tradeOffer == null) { if (tradeOffer == null) {
return false; return false;
} }
// Always accept trades when we're not losing anything // Always accept trades when we're not losing anything
if (tradeOffer.items_to_give.Count == 0) { if (tradeOffer.ItemsToGive.Count == 0) {
// Unless it's steam fuckup and we're dealing with broken trade // Unless it's steam fuckup and we're dealing with broken trade
return tradeOffer.items_to_receive.Count > 0; return tradeOffer.ItemsToReceive.Count > 0;
} }
// Always accept trades from SteamMasterID // Always accept trades from SteamMasterID
@@ -123,10 +143,84 @@ namespace ArchiSteamFarm {
return true; return true;
} }
// TODO: Add optional SteamTradeMatcher integration here // If we don't have SteamTradeMatcher enabled, this is the end for us
if (!Bot.BotConfig.SteamTradeMatcher) {
// If no rule above matched this trade, reject it
return false; return false;
} }
// Rule 1 - We always trade the same amount of items
if (tradeOffer.ItemsToGive.Count != tradeOffer.ItemsToReceive.Count) {
return false;
}
// Rule 2 - We always trade steam cards and only for the same set
if (!tradeOffer.IsSteamCardsOnlyTrade() || !tradeOffer.IsPotentiallyDupesTrade()) {
return false;
}
// At this point we're sure that STM trade is valid
// Now check if it's worth for us to do the trade
HashSet<Steam.Item> inventory = await Bot.ArchiWebHandler.GetMyTradableInventory().ConfigureAwait(false);
if (inventory == null || inventory.Count == 0) {
return true; // OK, assume that this trade is valid, we can't check our EQ
}
// Get appIDs we're interested in
HashSet<uint> appIDs = new HashSet<uint>();
foreach (Steam.Item item in tradeOffer.ItemsToGive) {
appIDs.Add(item.RealAppID);
}
// Now remove from our inventory all items we're NOT interested in
inventory.RemoveWhere(item => !appIDs.Contains(item.RealAppID));
// If for some reason Valve is talking crap and we can't find mentioned items, assume OK
if (inventory.Count == 0) {
return true;
}
// Now let's create a map which maps items to their amount in our EQ
Dictionary<Tuple<ulong, ulong>, uint> amountMap = new Dictionary<Tuple<ulong, ulong>, uint>();
foreach (Steam.Item item in inventory) {
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(item.ClassID, item.InstanceID);
uint amount;
if (amountMap.TryGetValue(key, out amount)) {
amountMap[key] = amount + item.Amount;
} else {
amountMap[key] = item.Amount;
}
}
// Calculate our value of items to give
uint itemsToGiveDupesValue = 0;
foreach (Steam.Item item in tradeOffer.ItemsToGive) {
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(item.ClassID, item.InstanceID);
uint amount;
if (!amountMap.TryGetValue(key, out amount)) {
continue;
}
itemsToGiveDupesValue += amount;
}
// Calculate our value of items to receive
uint itemsToReceiveDupesValue = 0;
foreach (Steam.Item item in tradeOffer.ItemsToReceive) {
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(item.ClassID, item.InstanceID);
uint amount;
if (!amountMap.TryGetValue(key, out amount)) {
continue;
}
itemsToReceiveDupesValue += amount;
}
// Trade is worth for us if we're in total trading more of our dupes for less of our dupes (or at least same amount)
// Which means that itemsToGiveDupesValue should be greater than itemsToReceiveDupesValue
return itemsToGiveDupesValue > itemsToReceiveDupesValue;
}
} }
} }

View File

@@ -33,6 +33,10 @@ namespace ArchiSteamFarm {
internal static void Forget(this Task task) { } internal static void Forget(this Task task) { }
internal static Task ForEachAsync<T>(this IEnumerable<T> sequence, Func<T, Task> action) { internal static Task ForEachAsync<T>(this IEnumerable<T> sequence, Func<T, Task> action) {
if (action == null) {
return Task.FromResult(true);
}
return Task.WhenAll(sequence.Select(action)); return Task.WhenAll(sequence.Select(action));
} }
@@ -64,20 +68,5 @@ namespace ArchiSteamFarm {
return Task.Delay(miliseconds); return Task.Delay(miliseconds);
} }
internal static uint GetCharCountInString(string s, char c) {
if (string.IsNullOrEmpty(s)) {
return 0;
}
uint count = 0;
foreach (char singleChar in s) {
if (singleChar == c) {
count++;
}
}
return count;
}
} }
} }

View File

@@ -141,10 +141,8 @@ namespace ArchiSteamFarm {
return null; return null;
} }
content = WebUtility.HtmlDecode(content);
HtmlDocument htmlDocument = new HtmlDocument(); HtmlDocument htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(content); htmlDocument.LoadHtml(WebUtility.HtmlDecode(content));
return htmlDocument; return htmlDocument;
} }
@@ -213,7 +211,7 @@ namespace ArchiSteamFarm {
return null; return null;
} }
if (request.StartsWith("https://") && Program.GlobalConfig.ForceHttp) { if (request.StartsWith("https://", StringComparison.Ordinal) && Program.GlobalConfig.ForceHttp) {
return null; return null;
} }
@@ -239,15 +237,17 @@ namespace ArchiSteamFarm {
} }
} }
if (responseMessage == null || !responseMessage.IsSuccessStatusCode) { if (responseMessage == null) {
return null;
}
if (!responseMessage.IsSuccessStatusCode) {
if (Debugging.IsDebugBuild || Program.GlobalConfig.Debug) { if (Debugging.IsDebugBuild || Program.GlobalConfig.Debug) {
Logging.LogGenericError("Request: " + request + " failed!", Identifier); Logging.LogGenericError("Request: " + request + " failed!", Identifier);
if (responseMessage != null) {
Logging.LogGenericError("Status code: " + responseMessage.StatusCode, Identifier); Logging.LogGenericError("Status code: " + responseMessage.StatusCode, Identifier);
Logging.LogGenericError("Content: " + Environment.NewLine + await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false), Identifier); Logging.LogGenericError("Content: " + Environment.NewLine + await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false), Identifier);
}
responseMessage.Dispose(); responseMessage.Dispose();
}
}
return null; return null;
} }

View File

@@ -2,6 +2,7 @@
"Debug": false, "Debug": false,
"Headless": false, "Headless": false,
"AutoUpdates": true, "AutoUpdates": true,
"AutoRestart": true,
"UpdateChannel": 1, "UpdateChannel": 1,
"SteamProtocol": 6, "SteamProtocol": 6,
"SteamOwnerID": 0, "SteamOwnerID": 0,

View File

@@ -12,6 +12,7 @@
"FarmOffline": false, "FarmOffline": false,
"HandleOfflineMessages": false, "HandleOfflineMessages": false,
"AcceptGifts": false, "AcceptGifts": false,
"SteamTradeMatcher": false,
"ForwardKeysToOtherBots": false, "ForwardKeysToOtherBots": false,
"DistributeKeys": false, "DistributeKeys": false,
"UseAsfAsMobileAuthenticator": false, "UseAsfAsMobileAuthenticator": false,

View File

@@ -68,6 +68,9 @@ namespace ConfigGenerator {
[JsonProperty(Required = Required.DisallowNull)] [JsonProperty(Required = Required.DisallowNull)]
public bool AcceptGifts { get; set; } = false; public bool AcceptGifts { get; set; } = false;
[JsonProperty(Required = Required.DisallowNull)]
public bool SteamTradeMatcher { get; set; } = false;
[JsonProperty(Required = Required.DisallowNull)] [JsonProperty(Required = Required.DisallowNull)]
public bool ForwardKeysToOtherBots { get; set; } = false; public bool ForwardKeysToOtherBots { get; set; } = false;

View File

@@ -54,6 +54,9 @@ namespace ConfigGenerator {
[JsonProperty(Required = Required.DisallowNull)] [JsonProperty(Required = Required.DisallowNull)]
public bool AutoUpdates { get; set; } = true; public bool AutoUpdates { get; set; } = true;
[JsonProperty(Required = Required.DisallowNull)]
public bool AutoRestart { get; set; } = true;
[JsonProperty(Required = Required.DisallowNull)] [JsonProperty(Required = Required.DisallowNull)]
public EUpdateChannel UpdateChannel { get; set; } = EUpdateChannel.Stable; public EUpdateChannel UpdateChannel { get; set; } = EUpdateChannel.Stable;

View File

@@ -36,7 +36,7 @@ namespace ConfigGenerator {
private const string ASFDirectory = "ArchiSteamFarm"; private const string ASFDirectory = "ArchiSteamFarm";
private static readonly string ExecutableDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); private static readonly string ExecutableDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
/// <summary> /// <summary>
/// The main entry point for the application. /// The main entry point for the application.

View File

@@ -135,6 +135,7 @@
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup> <PropertyGroup>
<!--
<PostBuildEvent Condition=" '$(OS)' != 'Unix' AND '$(ConfigurationName)' == 'Release' "> <PostBuildEvent Condition=" '$(OS)' != 'Unix' AND '$(ConfigurationName)' == 'Release' ">
"$(SolutionDir)tools\ILRepack\ILRepack.exe" /ndebug /internalize /parallel /targetplatform:v4 /wildcards /out:"$(SolutionDir)out\ASF-GUI.exe" "$(TargetDir)$(TargetName).exe" "$(TargetDir)*.dll" "$(SolutionDir)tools\ILRepack\ILRepack.exe" /ndebug /internalize /parallel /targetplatform:v4 /wildcards /out:"$(SolutionDir)out\ASF-GUI.exe" "$(TargetDir)$(TargetName).exe" "$(TargetDir)*.dll"
del "$(SolutionDir)out\ASF-GUI.exe.config" del "$(SolutionDir)out\ASF-GUI.exe.config"
@@ -143,6 +144,7 @@
mono -O=all "$(SolutionDir)tools/ILRepack/ILRepack.exe" /ndebug /internalize /parallel /targetplatform:v4 /wildcards /out:"$(SolutionDir)out/ASF-GUI.exe" "$(TargetDir)$(TargetName).exe" "$(TargetDir)*.dll" mono -O=all "$(SolutionDir)tools/ILRepack/ILRepack.exe" /ndebug /internalize /parallel /targetplatform:v4 /wildcards /out:"$(SolutionDir)out/ASF-GUI.exe" "$(TargetDir)$(TargetName).exe" "$(TargetDir)*.dll"
rm "$(SolutionDir)out/ASF-GUI.exe.config" rm "$(SolutionDir)out/ASF-GUI.exe.config"
</PostBuildEvent> </PostBuildEvent>
-->
</PropertyGroup> </PropertyGroup>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- 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. Other similar extension points exist, see Microsoft.Common.targets.

9
appveyor.yml Normal file
View File

@@ -0,0 +1,9 @@
version: 1.0.{build}-{branch}
image: Visual Studio 2015
configuration: Release
platform: Any CPU
clone_depth: 10
build:
project: ArchiSteamFarm.sln
parallel: true
verbosity: minimal