Compare commits

...

108 Commits

Author SHA1 Message Date
JustArchi
9f4a6e6e67 Translations update 2017-08-28 12:16:31 +02:00
JustArchi
9718245793 Packages update + websocket tests 2017-08-28 12:13:59 +02:00
JustArchi
9af542597c Fix initial GamesPlayedWhileIdle in paused mode 2017-08-28 00:00:57 +02:00
JustArchi
6391a5f79c Misc optimization
We don't need to call StartFarming() twice on first login if there is nothing to farm.
2017-08-27 09:54:08 +02:00
JustArchi
20c0ba28ef Translations update 2017-08-26 02:52:20 +02:00
JustArchi
f01740a3db CI: Misc 2017-08-26 02:51:06 +02:00
JustArchi
a89c6e630c Always reject trades from blacklisted users 2017-08-26 02:48:28 +02:00
JustArchi
0b37323a5f CI: Misc 2017-08-24 20:13:03 +02:00
JustArchi
14799c2733 Bump 2017-08-24 18:14:25 +02:00
JustArchi
031386ffb9 Closes #623 2017-08-24 17:55:25 +02:00
JustArchi
b561c3fc17 Misc 2017-08-23 17:32:58 +02:00
JustArchi
644f95e96c Bump 2017-08-23 17:11:41 +02:00
JustArchi
864c909760 Translations update 2017-08-23 17:11:15 +02:00
JustArchi
aacf491a0c Improve relability of CustomGamePlayed
It's freaking unbelievable that I found a root cause of that.
2017-08-23 17:04:12 +02:00
JustArchi
5ce219eb6e Misc 2017-08-23 16:09:03 +02:00
JustArchi
f1578a5174 Improve IdleRefundableGames
We didn't take into account 2 hours passed scenario.
2017-08-23 15:59:56 +02:00
JustArchi
1d7dbe3791 Misc, #615 2017-08-23 01:17:43 +02:00
JustArchi
5c121bee75 Misc complex algorithm optimization 2017-08-22 20:54:27 +02:00
JustArchi
6be0cff505 Bump 2017-08-22 16:13:15 +02:00
JustArchi
6281e258da Translations update 2017-08-22 15:49:25 +02:00
JustArchi
68110f2038 Misc 2017-08-22 15:48:00 +02:00
JustArchi
f423f35c62 Further fix 2017-08-21 15:16:55 +02:00
JustArchi
56246d3853 Fix GamesPlayedWhileIdle 2017-08-20 23:35:03 +02:00
JustArchi
f6cbd67206 CodeStyle update 2017-08-19 00:12:51 +02:00
JustArchi
720214016b Bump 2017-08-18 22:59:11 +02:00
JustArchi
55d7ccd28b Translations update 2017-08-18 22:56:01 +02:00
Łukasz Domeradzki
6d1ea0b20c AppVeyor/Travis: 2.0.0 runtime updates (#619) 2017-08-18 19:41:17 +02:00
JustArchi
40e8d85359 Bump 2017-08-16 01:14:45 +02:00
JustArchi
e397b7f7cc Bump IdleFarmingPeriod 2017-08-16 01:08:30 +02:00
JustArchi
4090b40ac7 Misc 2017-08-16 00:44:09 +02:00
JustArchi
05df0ee725 Add extra logic for disconnects 2017-08-16 00:42:20 +02:00
JustArchi
eaf56d0221 Optimize badges parsing
Currently we parse badge pages on account becoming free (both game lock and family lock), as well as on new game redeemed on account.

This can be further optimized by avoiding checking badge pages when we can be sure that it didn't change since our last run, so when we already confirmed that we have nothing to farm AND we didn't get disconnect in between. This would make it possible to avoid stupid checks e.g. when user is exiting game being played if we confirmed that there is nothing to farm and we didn't get new game being redeemed event.

If by any chance user will redeem new game while playing something, this will still work correctly and trigger badge check after he's done playing.
2017-08-16 00:19:23 +02:00
JustArchi
1fc9b96f83 Slightly help with !owns, #586 2017-08-15 16:08:18 +02:00
JustArchi
462020f842 Misc 2017-08-15 15:10:27 +02:00
Łukasz Domeradzki
b4e8e24921 Merge pull request #615 from KlappPc/master
Allow cross site access, (for example via javascript) for IPC
2017-08-15 15:07:35 +02:00
Florian Lang
84129da691 moved cross origin to writeAsync 2017-08-15 14:52:00 +02:00
JustArchi
141673409f Closes #613 2017-08-15 14:30:59 +02:00
Florian Lang
2ad10a5946 Merge remote-tracking branch 'upstream/master' 2017-08-15 11:24:59 +02:00
Florian Lang
495b7594f9 allow cross site acces from file:/// 2017-08-15 11:24:32 +02:00
JustArchi
1727492e07 Misc 2017-08-14 19:02:06 +02:00
JustArchi
82e38d78eb CI: Slightly improve speed 2017-08-14 18:26:00 +02:00
JustArchi
55af650da5 Bump 2017-08-14 18:10:06 +02:00
JustArchi
a062e98f2d crowdin-cli 2.0.18 2017-08-14 18:00:35 +02:00
JustArchi
8b8671c679 Use TCP by default
We'll play it safe, websocket/udp is not fully tested yet.
2017-08-14 17:58:30 +02:00
JustArchi
5e4f6a7926 Revert "Test"
This reverts commit 57b2c8f1cf.
2017-08-14 06:24:27 +02:00
JustArchi
57b2c8f1cf Test 2017-08-14 06:17:57 +02:00
JustArchi
02e7f2144f Misc 2017-08-14 05:32:22 +02:00
JustArchi
c13fd10bd8 Implement BackgroundGCPeriod
Tough decision to make, but I promised going for full performance, so there is that.
2017-08-14 05:31:01 +02:00
JustArchi
7fe0b4499b Misc 2017-08-12 20:17:00 +02:00
JustArchi
94160bdc42 Packages update 2017-08-12 01:22:52 +02:00
JustArchi
c01c0f7cd1 General cleanup of #609 2017-08-12 01:16:44 +02:00
Łukasz Domeradzki
65b06ae3b9 Merge pull request #609 from KlappPc/master
Transfer command (easy version, flexible loot)
2017-08-12 01:02:41 +02:00
Florian Lang
5d443f3ed2 simplified 2017-08-12 00:45:05 +02:00
Florian Lang
6a6c903d7d tradetoken 2017-08-12 00:31:51 +02:00
Florian Lang
52f5ef2a39 misc 2017-08-12 00:19:08 +02:00
Florian Lang
b92a4ea505 switched to tab as intend 2017-08-11 22:51:27 +02:00
Florian Lang
8ae9db3a34 transfer command easy version 2017-08-11 22:36:03 +02:00
JustArchi
388c72052c Misc optimization
I had to miss this one.
2017-08-10 00:05:00 +02:00
JustArchi
e965899395 Misc 2017-08-09 23:08:46 +02:00
JustArchi
5605e04f0a Properly deal with #607, #586 2017-08-09 18:58:51 +02:00
JustArchi
953103719c Misc 2017-08-09 00:18:38 +02:00
JustArchi
ea53fd0a87 Packages update 2017-08-09 00:12:50 +02:00
JustArchi
f6770ea1c9 Misc 2017-08-09 00:12:24 +02:00
JustArchi
561b2a3566 Bump 2017-08-07 17:42:39 +02:00
JustArchi
d24e6d0302 Packages update 2017-08-07 16:42:03 +02:00
JustArchi
98491a4562 Misc 2017-08-07 16:41:24 +02:00
JustArchi
08d7b9deb0 Misc 2017-08-06 22:09:19 +02:00
JustArchi
b904b1062f Misc 2017-08-05 23:08:40 +02:00
JustArchi
ad187c0d88 Closes #607 2017-08-05 22:35:03 +02:00
JustArchi
6a035f4832 Standarize IPC response, #586 2017-08-05 17:14:31 +02:00
JustArchi
021e8d2ad9 Don't move NLog.config during update, #586 2017-08-05 15:13:06 +02:00
JustArchi
61609574d0 Bump 2017-08-05 12:55:08 +02:00
JustArchi
7b7f1518a6 Translations update 2017-08-04 19:41:51 +02:00
JustArchi
6aa5a633e4 async/dispose code review 2017-08-04 19:26:37 +02:00
JustArchi
6f80ee9faa Misc 2017-08-04 18:27:30 +02:00
JustArchi
99e8df318c Free the main thread
Now possible with C# 7.1
2017-08-04 18:22:49 +02:00
JustArchi
2ea334c62e Improve farming reset logic #586 2017-08-03 12:14:47 +02:00
JustArchi
27d0b7427a Packages update 2017-08-02 23:09:13 +02:00
JustArchi
62c20c331e Bump 2017-08-02 20:53:44 +02:00
JustArchi
51d59f0f66 Fix Windows builds 2017-08-02 20:47:53 +02:00
JustArchi
ab90c9dc68 Bump 2017-08-02 19:59:49 +02:00
JustArchi
f7c8b871b3 Translations update 2017-08-02 19:47:23 +02:00
JustArchi
678b32f318 Remove debug, issue fixed 2017-08-02 19:44:55 +02:00
JustArchi
62221fd6b8 Test fix 2017-08-02 19:36:43 +02:00
JustArchi
f932be1395 Keep original dotnet permissions
Dotnet uses rwx-rw-rw
2017-08-02 19:00:27 +02:00
JustArchi
d8fd1035c3 Handle chmod +x for updates #586 2017-08-02 15:03:22 +02:00
JustArchi
09e8a52811 Packages update + config defaults 2017-08-02 14:32:01 +02:00
JustArchi
0b1032199b Misc 2017-08-01 20:50:41 +02:00
JustArchi
3e5dfb3174 Bump 2017-08-01 19:14:51 +02:00
JustArchi
4ada8595bd Translations update 2017-08-01 19:14:20 +02:00
JustArchi
bc64c43748 Misc 2017-08-01 19:04:55 +02:00
JustArchi
f4f7935d4c Add a workaround for invalid scheme redirections
https://github.com/JustArchi/ArchiSteamFarm/issues/586#issuecomment-319422864
2017-08-01 18:59:10 +02:00
JustArchi
aacc0a1720 Bump 2017-08-01 13:23:57 +02:00
JustArchi
e5ee909b96 Remove debug leftovers 2017-08-01 13:23:19 +02:00
JustArchi
f3f444d0bd Test fix #605 2017-08-01 13:00:54 +02:00
JustArchi
43d18b6d49 Move debug to appropriate place 2017-08-01 12:44:22 +02:00
JustArchi
f24be67c8c Bunch of stuff for #605 2017-08-01 12:41:57 +02:00
JustArchi
6ce4f2941b Bump 2017-07-31 17:51:04 +02:00
JustArchi
ef5398fae8 Translations update 2017-07-31 17:39:40 +02:00
JustArchi
ce600fdf42 Packages update 2017-07-31 17:38:38 +02:00
JustArchi
7a6485775d Misc 2017-07-31 17:35:17 +02:00
JustArchi
d39e7a32d6 Add debug for #586 2017-07-31 17:22:18 +02:00
JustArchi
b4cbe85b36 Closes #605 2017-07-31 16:57:10 +02:00
JustArchi
4d83417c9d Misc 2017-07-24 12:18:40 +02:00
JustArchi
25bfbf4862 Misc optimization
I like how R# spots multiple enumerations.
2017-07-24 11:13:55 +02:00
JustArchi
e539e1bdc7 Fixes #537
Apparently Steam web is too stupid to handle redirections properly.
2017-07-24 10:39:51 +02:00
JustArchi
b0f83fcd6d AppVeyor: Misc 2017-07-24 10:08:51 +02:00
JustArchi
4880a4e60b Bump 2017-07-23 13:42:03 +02:00
53 changed files with 1156 additions and 535 deletions

View File

@@ -16,13 +16,14 @@ branches:
mono: none
# ASF requires .NET Core 2.0+
# TODO: We should target stable 2.0.0 once it's released
dotnet: 2.0.0-preview2-006497
dotnet: 2.0.0
env:
global:
- CONFIGURATION: Release
- DOTNET_CLI_TELEMETRY_OPTOUT: 1
- DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
- RUNTIMES="generic win-x64 linux-x64 linux-arm osx-x64" # https://github.com/travis-ci/travis-ci/issues/1444
before_script: dotnet restore
@@ -30,31 +31,33 @@ script:
- |
set -e
RUNTIMES="generic win-x64 linux-x64 linux-arm osx-x64"
dotnet build -c "$CONFIGURATION" -o 'out/source' --no-restore /nologo
dotnet test ArchiSteamFarm.Tests -c "$CONFIGURATION" -o 'out/source' --no-build --no-restore
dotnet build -c Release
dotnet test -c Release --no-build --no-restore ArchiSteamFarm.Tests
for RUNTIME in $RUNTIMES; do
if [ "$RUNTIME" = "generic" ]; then
dotnet publish -c Release -o "out/${RUNTIME}"
publish() {
if [ "$1" = 'generic' ]; then
dotnet publish ArchiSteamFarm -c "$CONFIGURATION" -o "out/${1}" --no-restore /nologo
else
dotnet publish -c Release -r "$RUNTIME" -o "out/${RUNTIME}"
dotnet publish ArchiSteamFarm -c "$CONFIGURATION" -o "out/${1}" -r "$1" --no-restore /nologo
fi
echo "$RUNTIME" > "ArchiSteamFarm/out/${RUNTIME}/ArchiSteamFarm.version"
echo "$1" > "ArchiSteamFarm/out/${1}/ArchiSteamFarm.version"
}
for RUNTIME in $RUNTIMES; do
publish "$RUNTIME" &
done
wait
matrix:
# We can use fast finish, as we don't need to wait for allow_failures builds to mark build as success
fast_finish: true
include:
# We're building ASF with dotnet on latest versions of Linux and OS X
# Ref: https://docs.travis-ci.com/user/ci-environment/#Virtualization-environments
- os: linux
# Ref: https://docs.travis-ci.com/user/trusty-ci-environment/
# Ref: https://docs.travis-ci.com/user/reference/trusty/
dist: trusty
sudo: false
- os: osx
# Ref: https://docs.travis-ci.com/user/osx-ci-environment/
# Ref: https://docs.travis-ci.com/user/reference/osx/
osx_image: xcode9

View File

@@ -24,9 +24,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170628-02" />
<PackageReference Include="MSTest.TestAdapter" Version="1.2.0-beta" />
<PackageReference Include="MSTest.TestFramework" Version="1.2.0-beta" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0-preview-20170810-02" />
<PackageReference Include="MSTest.TestAdapter" Version="1.2.0-beta3" />
<PackageReference Include="MSTest.TestFramework" Version="1.2.0-beta3" />
</ItemGroup>
<ItemGroup>

View File

@@ -210,6 +210,13 @@ namespace ArchiSteamFarm {
return;
}
if (IsUnixVersion(version)) {
string executable = Path.Combine(targetDirectory, SharedInfo.AssemblyName);
if (File.Exists(executable)) {
OS.UnixSetFileAccessExecutable(executable);
}
}
ArchiLogger.LogGenericInfo(Strings.UpdateFinished);
await RestartOrExit().ConfigureAwait(false);
}
@@ -268,6 +275,22 @@ namespace ArchiSteamFarm {
Bot.RegisterBot(botName);
}
private static bool IsUnixVersion(string version) {
if (string.IsNullOrEmpty(version)) {
ArchiLogger.LogNullError(nameof(version));
return false;
}
switch (version) {
case "linux-arm":
case "linux-x64":
case "osx-x64":
return true;
default:
return false;
}
}
private static bool IsValidBotName(string botName) {
if (string.IsNullOrEmpty(botName)) {
ArchiLogger.LogNullError(nameof(botName));
@@ -459,7 +482,14 @@ namespace ArchiSteamFarm {
// Move top-level runtime in-use files to other directory
// We must do it in order to not crash at later stage - all libraries/executables must keep original names
foreach (string file in Directory.EnumerateFiles(targetDirectory)) {
string target = Path.Combine(backupDirectory, Path.GetFileName(file));
string fileName = Path.GetFileName(file);
switch (fileName) {
// Files that we want to keep in original directory
case "NLog.config":
continue;
}
string target = Path.Combine(backupDirectory, fileName);
File.Move(file, target);
}

View File

@@ -96,7 +96,7 @@ namespace ArchiSteamFarm {
Client.Send(request);
}
internal void PlayGames(IEnumerable<uint> gameIDs, string gameName = null) {
internal async Task PlayGames(IEnumerable<uint> gameIDs, string gameName = null) {
if (gameIDs == null) {
ArchiLogger.LogNullError(nameof(gameIDs));
return;
@@ -109,6 +109,11 @@ namespace ArchiSteamFarm {
ClientMsgProtobuf<CMsgClientGamesPlayed> request = new ClientMsgProtobuf<CMsgClientGamesPlayed>(EMsg.ClientGamesPlayed);
if (!string.IsNullOrEmpty(gameName)) {
// If we have custom name to display, we must workaround the Steam network fuckup and send request on clean non-playing session
// This ensures that custom name will in fact display properly
Client.Send(request);
await Task.Delay(Bot.CallbackSleep).ConfigureAwait(false);
request.Body.games_played.Add(new CMsgClientGamesPlayed.GamePlayed {
game_extra_info = gameName,
game_id = new GameID {

View File

@@ -3,8 +3,8 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<AssemblyVersion>3.0.0.6</AssemblyVersion>
<FileVersion>3.0.0.6</FileVersion>
<AssemblyVersion>3.0.2.0</AssemblyVersion>
<FileVersion>3.0.2.0</FileVersion>
<LangVersion>latest</LangVersion>
<ErrorReport>none</ErrorReport>
<ApplicationIcon>ASF.ico</ApplicationIcon>
@@ -18,6 +18,11 @@
<RepositoryUrl>https://github.com/JustArchi/ArchiSteamFarm.git</RepositoryUrl>
<PackageIconUrl>https://github.com/JustArchi/ArchiSteamFarm/raw/master/resources/ASF.ico</PackageIconUrl>
<RepositoryType>Git</RepositoryType>
<NoWarn />
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
@@ -26,12 +31,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.5.2-beta3" />
<PackageReference Include="HtmlAgilityPack" Version="1.5.2-beta6" />
<PackageReference Include="Humanizer" Version="2.2.0" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0-beta1" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="NLog" Version="5.0.0-beta09" />
<PackageReference Include="SteamKit2" Version="2.0.0-Alpha5" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.4.0-preview2-25405-01" />
<PackageReference Include="SteamKit2" Version="2.0.0-Alpha7" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.4.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -58,15 +58,15 @@ namespace ArchiSteamFarm {
private const string SteamStoreURL = "http://" + SteamStoreHost;
private static readonly SemaphoreSlim InventorySemaphore = new SemaphoreSlim(1);
private static readonly SemaphoreSlim InventorySemaphore = new SemaphoreSlim(1, 1);
private static int Timeout = GlobalConfig.DefaultConnectionTimeout * 1000; // This must be int type
private readonly SemaphoreSlim ApiKeySemaphore = new SemaphoreSlim(1);
private readonly SemaphoreSlim ApiKeySemaphore = new SemaphoreSlim(1, 1);
private readonly Bot Bot;
private readonly SemaphoreSlim PublicInventorySemaphore = new SemaphoreSlim(1);
private readonly SemaphoreSlim SessionSemaphore = new SemaphoreSlim(1);
private readonly SemaphoreSlim TradeTokenSemaphore = new SemaphoreSlim(1);
private readonly SemaphoreSlim PublicInventorySemaphore = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim SessionSemaphore = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim TradeTokenSemaphore = new SemaphoreSlim(1, 1);
private readonly WebBrowser WebBrowser;
private string CachedApiKey;
@@ -74,6 +74,7 @@ namespace ArchiSteamFarm {
private string CachedTradeToken;
private DateTime LastSessionRefreshCheck = DateTime.MinValue;
private ulong SteamID;
private string VanityURL;
internal ArchiWebHandler(Bot bot) {
Bot = bot ?? throw new ArgumentNullException(nameof(bot));
@@ -85,6 +86,7 @@ namespace ArchiSteamFarm {
PublicInventorySemaphore.Dispose();
SessionSemaphore.Dispose();
TradeTokenSemaphore.Dispose();
WebBrowser.Dispose();
}
internal async Task<bool> AcceptTradeOffer(ulong tradeID) {
@@ -481,7 +483,7 @@ namespace ArchiSteamFarm {
}
[SuppressMessage("ReSharper", "FunctionComplexityOverflow")]
internal async Task<HashSet<Steam.Item>> GetMySteamInventory(bool tradable, HashSet<Steam.Item.EType> wantedTypes, HashSet<uint> wantedRealAppIDs = null) {
internal async Task<HashSet<Steam.Item>> GetMySteamInventory(bool trading, HashSet<Steam.Item.EType> wantedTypes, HashSet<uint> wantedRealAppIDs = null) {
if ((wantedTypes == null) || (wantedTypes.Count == 0)) {
Bot.ArchiLogger.LogNullError(nameof(wantedTypes));
return null;
@@ -493,7 +495,7 @@ namespace ArchiSteamFarm {
HashSet<Steam.Item> result = new HashSet<Steam.Item>();
string request = SteamCommunityURL + "/my/inventory/json/" + Steam.Item.SteamAppID + "/" + Steam.Item.SteamCommunityContextID + "?l=english&trading=" + (tradable ? "1" : "0") + "&start=";
string request = SteamCommunityURL + "/my/inventory/json/" + Steam.Item.SteamAppID + "/" + Steam.Item.SteamCommunityContextID + "?l=english" + (trading ? "&trading=1" : "") + "&start=";
uint currentPage = 0;
await InventorySemaphore.WaitAsync().ConfigureAwait(false);
@@ -867,12 +869,16 @@ namespace ArchiSteamFarm {
internal static void Init() => Timeout = Program.GlobalConfig.ConnectionTimeout * 1000;
internal async Task<bool> Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string parentalPin) {
internal async Task<bool> Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string parentalPin, string vanityURL = null) {
if ((steamID == 0) || (universe == EUniverse.Invalid) || string.IsNullOrEmpty(webAPIUserNonce) || string.IsNullOrEmpty(parentalPin)) {
Bot.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(universe) + " || " + nameof(webAPIUserNonce) + " || " + nameof(parentalPin));
return false;
}
if (!string.IsNullOrEmpty(vanityURL)) {
VanityURL = vanityURL;
}
string sessionID = Convert.ToBase64String(Encoding.UTF8.GetBytes(steamID.ToString()));
// Generate an AES session key
@@ -1104,7 +1110,7 @@ namespace ArchiSteamFarm {
return false;
}
string request = SteamCommunityURL + "/profiles/" + SteamID + "/ajaxunpackbooster";
string request = GetAbsoluteProfileURL() + "/ajaxunpackbooster";
Dictionary<string, string> data = new Dictionary<string, string>(3) {
{ "sessionid", sessionID },
{ "appid", appID.ToString() },
@@ -1115,6 +1121,14 @@ namespace ArchiSteamFarm {
return response?.Result == EResult.OK;
}
private string GetAbsoluteProfileURL() {
if (!string.IsNullOrEmpty(VanityURL)) {
return SteamCommunityURL + "/id/" + VanityURL;
}
return SteamCommunityURL + "/profiles/" + SteamID;
}
private async Task<string> GetApiKey() {
if (CachedApiKey != null) {
// We fetched API key already, and either got valid one, or permanent AccessDenied
@@ -1165,7 +1179,7 @@ namespace ArchiSteamFarm {
break;
default:
// We got an unhandled error, this should never happen
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(result.Value.State), result.Value.State));
Bot.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(result.Value.State), result.Value.State));
break;
}

View File

@@ -41,9 +41,9 @@ using SteamKit2.Discovery;
namespace ArchiSteamFarm {
internal sealed class Bot : IDisposable {
internal const ushort CallbackSleep = 500; // In miliseconds
internal const byte MinPlayingBlockedTTL = 60; // Delay in seconds added when account was occupied during our disconnect, to not disconnect other Steam client session too soon
private const ushort CallbackSleep = 500; // In miliseconds
private const byte FamilySharingInactivityMinutes = 5;
private const byte LoginCooldownInMinutes = 25; // Captcha disappears after around 20 minutes, so we make it 25
private const uint LoginID = GlobalConfig.DefaultIPCPort; // This must be the same for all ASF bots and all ASF processes
@@ -53,8 +53,8 @@ namespace ArchiSteamFarm {
internal static readonly ConcurrentDictionary<string, Bot> Bots = new ConcurrentDictionary<string, Bot>();
private static readonly SemaphoreSlim GiftsSemaphore = new SemaphoreSlim(1);
private static readonly SemaphoreSlim LoginSemaphore = new SemaphoreSlim(1);
private static readonly SemaphoreSlim GiftsSemaphore = new SemaphoreSlim(1, 1);
private static readonly SemaphoreSlim LoginSemaphore = new SemaphoreSlim(1, 1);
private static readonly SteamConfiguration SteamConfiguration = new SteamConfiguration();
internal readonly ArchiLogger ArchiLogger;
@@ -73,15 +73,16 @@ namespace ArchiSteamFarm {
private readonly BotDatabase BotDatabase;
private readonly string BotName;
private readonly CallbackManager CallbackManager;
private readonly SemaphoreSlim CallbackSemaphore = new SemaphoreSlim(1);
private readonly SemaphoreSlim CallbackSemaphore = new SemaphoreSlim(1, 1);
[JsonProperty]
private readonly CardsFarmer CardsFarmer;
private readonly ConcurrentHashSet<ulong> HandledGifts = new ConcurrentHashSet<ulong>();
private readonly Timer HeartBeatTimer;
private readonly SemaphoreSlim InitializationSemaphore = new SemaphoreSlim(1);
private readonly SemaphoreSlim LootingSemaphore = new SemaphoreSlim(1);
private readonly SemaphoreSlim InitializationSemaphore = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim LootingSemaphore = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim PICSSemaphore = new SemaphoreSlim(1, 1);
private readonly Statistics Statistics;
private readonly SteamApps SteamApps;
private readonly SteamClient SteamClient;
@@ -230,20 +231,24 @@ namespace ArchiSteamFarm {
public void Dispose() {
// Those are objects that are always being created if constructor doesn't throw exception
ArchiWebHandler.Dispose();
CallbackSemaphore.Dispose();
CardsFarmer.Dispose();
HeartBeatTimer.Dispose();
InitializationSemaphore.Dispose();
Trading.Dispose();
LootingSemaphore.Dispose();
PICSSemaphore.Dispose();
// Those are objects that might be null and the check should be in-place
ArchiWebHandler?.Dispose();
BotDatabase?.Dispose();
CardsFarmer?.Dispose();
CardsFarmerResumeTimer?.Dispose();
ConnectionFailureTimer?.Dispose();
FamilySharingInactivityTimer?.Dispose();
HeartBeatTimer?.Dispose();
PlayingWasBlockedTimer?.Dispose();
SendItemsTimer?.Dispose();
Statistics?.Dispose();
SteamSaleEvent?.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) {
@@ -313,15 +318,15 @@ namespace ArchiSteamFarm {
return null;
}
internal async Task<(uint PlayableAppID, DateTime IgnoredUntil)> GetAppDataForIdling(uint appID, bool allowRecursiveDiscovery = true) {
if (appID == 0) {
ArchiLogger.LogNullError(nameof(appID));
return (0, DateTime.MinValue);
internal async Task<(uint PlayableAppID, DateTime IgnoredUntil)> GetAppDataForIdling(uint appID, float hoursPlayed, bool allowRecursiveDiscovery = true) {
if ((appID == 0) || (hoursPlayed < 0)) {
ArchiLogger.LogNullError(nameof(appID) + " || " + nameof(hoursPlayed));
return (0, DateTime.MaxValue);
}
if (!BotConfig.IdleRefundableGames) {
if ((hoursPlayed < CardsFarmer.HoursToBump) && !BotConfig.IdleRefundableGames) {
if (!Program.GlobalDatabase.AppIDsToPackageIDs.TryGetValue(appID, out ConcurrentHashSet<uint> packageIDs)) {
return (0, DateTime.MinValue);
return (0, DateTime.MaxValue);
}
if (packageIDs.Count > 0) {
@@ -339,7 +344,7 @@ namespace ArchiSteamFarm {
}
if (mostRecent != DateTime.MinValue) {
DateTime playableIn = mostRecent.AddDays(14);
DateTime playableIn = mostRecent.AddDays(CardsFarmer.DaysForRefund);
if (playableIn > DateTime.UtcNow) {
return (0, playableIn);
}
@@ -349,11 +354,15 @@ namespace ArchiSteamFarm {
AsyncJobMultiple<SteamApps.PICSProductInfoCallback>.ResultSet productInfoResultSet;
await PICSSemaphore.WaitAsync().ConfigureAwait(false);
try {
productInfoResultSet = await SteamApps.PICSGetProductInfo(appID, null, false);
} catch (Exception e) {
ArchiLogger.LogGenericException(e);
return (0, DateTime.MinValue);
ArchiLogger.LogGenericWarningException(e);
return (0, DateTime.MaxValue);
} finally {
PICSSemaphore.Release();
}
// ReSharper disable once LoopCanBePartlyConvertedToQuery - C# 7.0 out can't be used within LINQ query yet | https://github.com/dotnet/roslyn/issues/15619
@@ -433,7 +442,7 @@ namespace ArchiSteamFarm {
break;
}
(uint PlayableAppID, DateTime IgnoredUntil) dlcAppData = await GetAppDataForIdling(dlcAppID, false).ConfigureAwait(false);
(uint PlayableAppID, DateTime IgnoredUntil) dlcAppData = await GetAppDataForIdling(dlcAppID, hoursPlayed, false).ConfigureAwait(false);
if (dlcAppData.PlayableAppID != 0) {
return (dlcAppData.PlayableAppID, DateTime.MinValue);
}
@@ -443,7 +452,7 @@ namespace ArchiSteamFarm {
}
if (!productInfoResultSet.Complete || productInfoResultSet.Failed) {
return (0, DateTime.MinValue);
return (0, DateTime.MaxValue);
}
return (appID, DateTime.MinValue);
@@ -452,11 +461,15 @@ namespace ArchiSteamFarm {
internal async Task<Dictionary<uint, HashSet<uint>>> GetAppIDsToPackageIDs(IEnumerable<uint> packageIDs) {
AsyncJobMultiple<SteamApps.PICSProductInfoCallback>.ResultSet productInfoResultSet;
await PICSSemaphore.WaitAsync().ConfigureAwait(false);
try {
productInfoResultSet = await SteamApps.PICSGetProductInfo(Enumerable.Empty<uint>(), packageIDs);
} catch (Exception e) {
ArchiLogger.LogGenericException(e);
ArchiLogger.LogGenericWarningException(e);
return null;
} finally {
PICSSemaphore.Release();
}
Dictionary<uint, HashSet<uint>> result = new Dictionary<uint, HashSet<uint>>();
@@ -510,6 +523,15 @@ namespace ArchiSteamFarm {
return result;
}
internal async Task IdleGames(IEnumerable<uint> gameIDs) {
if (gameIDs == null) {
ArchiLogger.LogNullError(nameof(gameIDs));
return;
}
await ArchiHandler.PlayGames(gameIDs, BotConfig.CustomGamePlayedWhileFarming).ConfigureAwait(false);
}
internal static async Task InitializeSteamConfiguration(ProtocolTypes protocolTypes, uint cellID, InMemoryServerListProvider serverListProvider) {
if (serverListProvider == null) {
ASF.ArchiLogger.LogNullError(nameof(serverListProvider));
@@ -580,7 +602,7 @@ namespace ArchiSteamFarm {
}
internal async Task OnFarmingFinished(bool farmedSomething) {
OnFarmingStopped();
await OnFarmingStopped().ConfigureAwait(false);
if (farmedSomething || !FirstTradeSent) {
FirstTradeSent = true;
@@ -601,7 +623,7 @@ namespace ArchiSteamFarm {
}
}
internal void OnFarmingStopped() => ResetGamesPlayed();
internal async Task OnFarmingStopped() => await ResetGamesPlayed().ConfigureAwait(false);
internal async Task OnNewConfigLoaded(ASF.BotConfigEventArgs args) {
if (args == null) {
@@ -635,17 +657,6 @@ namespace ArchiSteamFarm {
}
}
internal void PlayGame(uint gameID, string gameName = null) => PlayGames(gameID.ToEnumerable(), gameName);
internal void PlayGames(IEnumerable<uint> gameIDs, string gameName = null) {
if (gameIDs == null) {
ArchiLogger.LogNullError(nameof(gameIDs));
return;
}
ArchiHandler.PlayGames(gameIDs, gameName);
}
internal async Task<bool> RefreshSession() {
if (!IsConnectedAndLoggedOn) {
return false;
@@ -656,7 +667,7 @@ namespace ArchiSteamFarm {
try {
callback = await SteamUser.RequestWebAPIUserNonce();
} catch (Exception e) {
ArchiLogger.LogGenericException(e);
ArchiLogger.LogGenericWarningException(e);
await Connect(true).ConfigureAwait(false);
return false;
}
@@ -792,13 +803,13 @@ namespace ArchiSteamFarm {
return await ResponseBlacklistAdd(steamID, args[1], args[2]).ConfigureAwait(false);
}
return ResponseBlacklistAdd(steamID, args[1]);
return await ResponseBlacklistAdd(steamID, args[1]).ConfigureAwait(false);
case "!BLRM":
if (args.Length > 2) {
return await ResponseBlacklistRemove(steamID, args[1], args[2]).ConfigureAwait(false);
}
return ResponseBlacklistRemove(steamID, args[1]);
return await ResponseBlacklistRemove(steamID, args[1]).ConfigureAwait(false);
case "!FARM":
return await ResponseFarm(steamID, args[1]).ConfigureAwait(false);
case "!INPUT":
@@ -814,13 +825,13 @@ namespace ArchiSteamFarm {
return await ResponseIdleQueueAdd(steamID, args[1], args[2]).ConfigureAwait(false);
}
return ResponseIdleQueueAdd(steamID, args[1]);
return await ResponseIdleQueueAdd(steamID, args[1]).ConfigureAwait(false);
case "!IQRM":
if (args.Length > 2) {
return await ResponseIdleQueueRemove(steamID, args[1], args[2]).ConfigureAwait(false);
}
return ResponseIdleQueueRemove(steamID, args[1]);
return await ResponseIdleQueueRemove(steamID, args[1]).ConfigureAwait(false);
case "!LOOT":
return await ResponseLoot(steamID, args[1]).ConfigureAwait(false);
case "!LOOT^":
@@ -885,6 +896,16 @@ namespace ArchiSteamFarm {
return await ResponseStatus(steamID, args[1]).ConfigureAwait(false);
case "!STOP":
return await ResponseStop(steamID, args[1]).ConfigureAwait(false);
case "!TRANSFER":
if (args.Length > 3) {
return await ResponseTransfer(steamID, args[1], args[2], args[3]).ConfigureAwait(false);
}
if (args.Length > 2) {
return await ResponseTransfer(steamID, args[1], args[2]).ConfigureAwait(false);
}
return ResponseUnknown(steamID);
case "!UNPACK":
return await ResponseUnpackBoosters(steamID, args[1]).ConfigureAwait(false);
default:
@@ -929,7 +950,10 @@ namespace ArchiSteamFarm {
ArchiLogger.LogGenericInfo(Strings.BotAutomaticIdlingPauseTimeout);
StopFamilySharingInactivityTimer();
await CardsFarmer.Resume(false).ConfigureAwait(false);
if (!await CardsFarmer.Resume(false).ConfigureAwait(false)) {
await ResetGamesPlayed().ConfigureAwait(false);
}
}
private async Task CheckOccupationStatus() {
@@ -944,7 +968,10 @@ namespace ArchiSteamFarm {
ArchiLogger.LogGenericInfo(Strings.BotAccountFree);
PlayingWasBlocked = false;
await CardsFarmer.Resume(false).ConfigureAwait(false);
if (!await CardsFarmer.Resume(false).ConfigureAwait(false)) {
await ResetGamesPlayed().ConfigureAwait(false);
}
}
private async Task Connect(bool force = false) {
@@ -1123,7 +1150,7 @@ namespace ArchiSteamFarm {
try {
if (DateTime.UtcNow.Subtract(ArchiHandler.LastPacketReceived).TotalSeconds > MinHeartBeatTTL) {
await SteamApps.PICSGetProductInfo(0, null);
await SteamFriends.RequestProfileInfo(SteamClient.SteamID);
}
HeartBeatFailures = 0;
@@ -1145,7 +1172,7 @@ namespace ArchiSteamFarm {
}
}
private void ImportAuthenticator(string maFilePath) {
private async Task ImportAuthenticator(string maFilePath) {
if (HasMobileAuthenticator || !File.Exists(maFilePath)) {
return;
}
@@ -1153,7 +1180,8 @@ namespace ArchiSteamFarm {
ArchiLogger.LogGenericInfo(Strings.BotAuthenticatorConverting);
try {
BotDatabase.MobileAuthenticator = JsonConvert.DeserializeObject<MobileAuthenticator>(File.ReadAllText(maFilePath));
MobileAuthenticator authenticator = JsonConvert.DeserializeObject<MobileAuthenticator>(File.ReadAllText(maFilePath));
await BotDatabase.SetMobileAuthenticator(authenticator).ConfigureAwait(false);
File.Delete(maFilePath);
} catch (Exception e) {
ArchiLogger.LogGenericException(e);
@@ -1172,14 +1200,14 @@ namespace ArchiSteamFarm {
if (string.IsNullOrEmpty(DeviceID)) {
string deviceID = Program.GetUserInput(ASF.EUserInputType.DeviceID, BotName);
if (string.IsNullOrEmpty(deviceID)) {
BotDatabase.MobileAuthenticator = null;
await BotDatabase.SetMobileAuthenticator().ConfigureAwait(false);
return;
}
SetUserInput(ASF.EUserInputType.DeviceID, deviceID);
}
BotDatabase.CorrectMobileAuthenticatorDeviceID(DeviceID);
await BotDatabase.CorrectMobileAuthenticatorDeviceID(DeviceID).ConfigureAwait(false);
}
ArchiLogger.LogGenericInfo(Strings.BotAuthenticatorImportFinished);
@@ -1265,7 +1293,7 @@ namespace ArchiSteamFarm {
return;
}
ArchiLogger.LogGenericError(Strings.BotHeartBeatFailed);
ArchiLogger.LogGenericWarning(Strings.BotHeartBeatFailed);
Destroy(true);
RegisterBot(BotName);
}
@@ -1415,7 +1443,7 @@ namespace ArchiSteamFarm {
try {
await SteamFriends.SetPersonaState(EPersonaState.Online);
} catch (Exception e) {
ArchiLogger.LogGenericException(e);
ArchiLogger.LogGenericWarningException(e);
}
}
@@ -1573,7 +1601,7 @@ namespace ArchiSteamFarm {
goto case EResult.RateLimitExceeded;
}
BotDatabase.LoginKey = null;
await BotDatabase.SetLoginKey().ConfigureAwait(false);
ArchiLogger.LogGenericInfo(Strings.BotRemovedExpiredLoginKey);
break;
case EResult.NoConnection:
@@ -1708,6 +1736,20 @@ namespace ArchiSteamFarm {
return;
}
// Return early if this update doesn't bring anything new
if (callback.LicenseList.Count == OwnedPackageIDs.Count) {
if (callback.LicenseList.All(license => OwnedPackageIDs.ContainsKey(license.PackageID))) {
// Wait 2 seconds for eventual PlayingSessionStateCallback or SharedLibraryLockStatusCallback
await Task.Delay(2000).ConfigureAwait(false);
if (!await CardsFarmer.Resume(false).ConfigureAwait(false)) {
await ResetGamesPlayed().ConfigureAwait(false);
}
return;
}
}
OwnedPackageIDs.Clear();
foreach (SteamApps.LicenseListCallback.License license in callback.LicenseList) {
OwnedPackageIDs[license.PackageID] = (license.PaymentMethod, license.TimeCreated);
@@ -1719,15 +1761,13 @@ namespace ArchiSteamFarm {
}
}
await Task.Delay(1000).ConfigureAwait(false); // Wait a second for eventual PlayingSessionStateCallback or SharedLibraryLockStatusCallback
// Wait a second for eventual PlayingSessionStateCallback or SharedLibraryLockStatusCallback
await Task.Delay(1000).ConfigureAwait(false);
// Normally we ResetGamesPlayed() in OnFarmingStopped() but there is no farming event if CardsFarmer module is disabled
// Therefore, trigger extra ResetGamesPlayed(), but only in this specific case
if (CardsFarmer.Paused) {
ResetGamesPlayed();
await ResetGamesPlayed().ConfigureAwait(false);
}
// We trigger OnNewGameAdded() anyway, as CardsFarmer has other things to handle regardless of being Paused or not
await CardsFarmer.OnNewGameAdded().ConfigureAwait(false);
}
@@ -1810,15 +1850,15 @@ namespace ArchiSteamFarm {
ArchiLogger.LogGenericWarning(Strings.BotAccountLocked);
}
if ((callback.CellID != 0) && (Program.GlobalDatabase.CellID != callback.CellID)) {
Program.GlobalDatabase.CellID = callback.CellID;
if ((callback.CellID != 0) && (callback.CellID != Program.GlobalDatabase.CellID)) {
await Program.GlobalDatabase.SetCellID(callback.CellID).ConfigureAwait(false);
}
if (!HasMobileAuthenticator) {
// Support and convert 2FA files
string maFilePath = Path.Combine(SharedInfo.ConfigDirectory, callback.ClientSteamID.ConvertToUInt64() + ".maFile");
if (File.Exists(maFilePath)) {
ImportAuthenticator(maFilePath);
await ImportAuthenticator(maFilePath).ConfigureAwait(false);
}
}
@@ -1832,7 +1872,7 @@ namespace ArchiSteamFarm {
SetUserInput(ASF.EUserInputType.SteamParentalPIN, steamParentalPIN);
}
if (!await ArchiWebHandler.Init(callback.ClientSteamID, SteamClient.Universe, callback.WebAPIUserNonce, BotConfig.SteamParentalPIN).ConfigureAwait(false)) {
if (!await ArchiWebHandler.Init(callback.ClientSteamID, SteamClient.Universe, callback.WebAPIUserNonce, BotConfig.SteamParentalPIN, callback.VanityURL).ConfigureAwait(false)) {
if (!await RefreshSession().ConfigureAwait(false)) {
break;
}
@@ -1853,6 +1893,7 @@ namespace ArchiSteamFarm {
Statistics?.OnLoggedOn().Forget();
Trading.OnNewTrade().Forget();
break;
case EResult.InvalidPassword:
case EResult.NoConnection:
@@ -1885,7 +1926,7 @@ namespace ArchiSteamFarm {
}
}
private void OnLoginKey(SteamUser.LoginKeyCallback callback) {
private async void OnLoginKey(SteamUser.LoginKeyCallback callback) {
if (string.IsNullOrEmpty(callback?.LoginKey)) {
ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.LoginKey));
return;
@@ -1896,7 +1937,7 @@ namespace ArchiSteamFarm {
loginKey = CryptoHelper.Encrypt(BotConfig.PasswordFormat, loginKey);
}
BotDatabase.LoginKey = loginKey;
await BotDatabase.SetLoginKey(loginKey).ConfigureAwait(false);
SteamUser.AcceptNewLoginKey(callback);
}
@@ -2051,12 +2092,12 @@ namespace ArchiSteamFarm {
}
}
private void ResetGamesPlayed() {
if (!IsPlayingPossible || (FamilySharingInactivityTimer != null)) {
private async Task ResetGamesPlayed() {
if (!IsPlayingPossible || (FamilySharingInactivityTimer != null) || CardsFarmer.NowFarming) {
return;
}
ArchiHandler.PlayGames(BotConfig.GamesPlayedWhileIdle, BotConfig.CustomGamePlayedWhileIdle);
await ArchiHandler.PlayGames(BotConfig.GamesPlayedWhileIdle, BotConfig.CustomGamePlayedWhileIdle).ConfigureAwait(false);
}
private void ResetPlayingWasBlockedWithTimer() {
@@ -2192,7 +2233,7 @@ namespace ArchiSteamFarm {
try {
callback = await SteamApps.RequestFreeLicense(gameID);
} catch (Exception e) {
ArchiLogger.LogGenericException(e);
ArchiLogger.LogGenericWarningException(e);
response.Append(FormatBotResponse(string.Format(Strings.BotAddLicense, gameID, EResult.Timeout)));
break;
}
@@ -2295,7 +2336,7 @@ namespace ArchiSteamFarm {
foreach (string flag in flags) {
switch (flag.ToUpperInvariant()) {
case "FD":
redeemFlags |= ERedeemFlags.ForceDistribution;
redeemFlags |= ERedeemFlags.ForceDistributing;
break;
case "FF":
redeemFlags |= ERedeemFlags.ForceForwarding;
@@ -2304,7 +2345,7 @@ namespace ArchiSteamFarm {
redeemFlags |= ERedeemFlags.ForceKeepMissingGames;
break;
case "SD":
redeemFlags |= ERedeemFlags.SkipDistribution;
redeemFlags |= ERedeemFlags.SkipDistributing;
break;
case "SF":
redeemFlags |= ERedeemFlags.SkipForwarding;
@@ -2420,7 +2461,7 @@ namespace ArchiSteamFarm {
return null;
}
private string ResponseBlacklistAdd(ulong steamID, string targetsText) {
private async Task<string> ResponseBlacklistAdd(ulong steamID, string targetsText) {
if ((steamID == 0) || string.IsNullOrEmpty(targetsText)) {
ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(targetsText));
return null;
@@ -2445,7 +2486,7 @@ namespace ArchiSteamFarm {
return FormatBotResponse(string.Format(Strings.ErrorIsEmpty, nameof(targetIDs)));
}
BotDatabase.AddBlacklistedFromTradesSteamIDs(targetIDs);
await BotDatabase.AddBlacklistedFromTradesSteamIDs(targetIDs).ConfigureAwait(false);
return FormatBotResponse(Strings.Done);
}
@@ -2460,7 +2501,7 @@ namespace ArchiSteamFarm {
return IsOwner(steamID) ? FormatStaticResponse(string.Format(Strings.BotNotFound, botNames)) : null;
}
IEnumerable<Task<string>> tasks = bots.Select(bot => Task.Run(() => bot.ResponseBlacklistAdd(steamID, targetsText)));
IEnumerable<Task<string>> tasks = bots.Select(bot => bot.ResponseBlacklistAdd(steamID, targetsText));
ICollection<string> results;
switch (Program.GlobalConfig.OptimizationMode) {
@@ -2491,7 +2532,7 @@ namespace ArchiSteamFarm {
return IsOwner(steamID) ? FormatStaticResponse(string.Format(Strings.BotNotFound, botNames)) : null;
}
IEnumerable<Task<string>> tasks = bots.Select(bot => Task.Run(() => bot.ResponseBlacklistRemove(steamID, targetsText)));
IEnumerable<Task<string>> tasks = bots.Select(bot => bot.ResponseBlacklistRemove(steamID, targetsText));
ICollection<string> results;
switch (Program.GlobalConfig.OptimizationMode) {
@@ -2511,7 +2552,7 @@ namespace ArchiSteamFarm {
return responses.Count > 0 ? string.Join("", responses) : null;
}
private string ResponseBlacklistRemove(ulong steamID, string targetsText) {
private async Task<string> ResponseBlacklistRemove(ulong steamID, string targetsText) {
if ((steamID == 0) || string.IsNullOrEmpty(targetsText)) {
ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(targetsText));
return null;
@@ -2536,7 +2577,7 @@ namespace ArchiSteamFarm {
return FormatBotResponse(string.Format(Strings.ErrorIsEmpty, nameof(targetIDs)));
}
BotDatabase.RemoveBlacklistedFromTradesSteamIDs(targetIDs);
await BotDatabase.RemoveBlacklistedFromTradesSteamIDs(targetIDs).ConfigureAwait(false);
return FormatBotResponse(Strings.Done);
}
@@ -2573,7 +2614,10 @@ namespace ArchiSteamFarm {
return FormatBotResponse(Strings.BotNotConnected);
}
await CardsFarmer.StopFarming().ConfigureAwait(false);
if (CardsFarmer.NowFarming) {
await CardsFarmer.StopFarming().ConfigureAwait(false);
}
CardsFarmer.StartFarming().Forget();
return FormatBotResponse(Strings.Done);
@@ -2660,7 +2704,7 @@ namespace ArchiSteamFarm {
return result;
}
private string ResponseIdleQueueAdd(ulong steamID, string targetsText) {
private async Task<string> ResponseIdleQueueAdd(ulong steamID, string targetsText) {
if ((steamID == 0) || string.IsNullOrEmpty(targetsText)) {
ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(targetsText));
return null;
@@ -2685,7 +2729,7 @@ namespace ArchiSteamFarm {
return FormatBotResponse(string.Format(Strings.ErrorIsEmpty, nameof(appIDs)));
}
BotDatabase.AddIdlingPriorityAppIDs(appIDs);
await BotDatabase.AddIdlingPriorityAppIDs(appIDs).ConfigureAwait(false);
return FormatBotResponse(Strings.Done);
}
@@ -2700,7 +2744,7 @@ namespace ArchiSteamFarm {
return IsOwner(steamID) ? FormatStaticResponse(string.Format(Strings.BotNotFound, botNames)) : null;
}
IEnumerable<Task<string>> tasks = bots.Select(bot => Task.Run(() => bot.ResponseIdleQueueAdd(steamID, targetsText)));
IEnumerable<Task<string>> tasks = bots.Select(bot => bot.ResponseIdleQueueAdd(steamID, targetsText));
ICollection<string> results;
switch (Program.GlobalConfig.OptimizationMode) {
@@ -2731,7 +2775,7 @@ namespace ArchiSteamFarm {
return IsOwner(steamID) ? FormatStaticResponse(string.Format(Strings.BotNotFound, botNames)) : null;
}
IEnumerable<Task<string>> tasks = bots.Select(bot => Task.Run(() => bot.ResponseIdleQueueRemove(steamID, targetsText)));
IEnumerable<Task<string>> tasks = bots.Select(bot => bot.ResponseIdleQueueRemove(steamID, targetsText));
ICollection<string> results;
switch (Program.GlobalConfig.OptimizationMode) {
@@ -2751,7 +2795,7 @@ namespace ArchiSteamFarm {
return responses.Count > 0 ? string.Join("", responses) : null;
}
private string ResponseIdleQueueRemove(ulong steamID, string targetsText) {
private async Task<string> ResponseIdleQueueRemove(ulong steamID, string targetsText) {
if ((steamID == 0) || string.IsNullOrEmpty(targetsText)) {
ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(targetsText));
return null;
@@ -2776,7 +2820,7 @@ namespace ArchiSteamFarm {
return FormatBotResponse(string.Format(Strings.ErrorIsEmpty, nameof(appIDs)));
}
BotDatabase.RemoveIdlingPriorityAppIDs(appIDs);
await BotDatabase.RemoveIdlingPriorityAppIDs(appIDs).ConfigureAwait(false);
return FormatBotResponse(Strings.Done);
}
@@ -2993,7 +3037,7 @@ namespace ArchiSteamFarm {
try {
result = await SteamFriends.SetPersonaName(nickname);
} catch (Exception e) {
ArchiLogger.LogGenericException(e);
ArchiLogger.LogGenericWarningException(e);
return FormatBotResponse(Strings.WarningFailed);
}
@@ -3049,6 +3093,8 @@ namespace ArchiSteamFarm {
return (FormatBotResponse(Strings.BotNotConnected), false);
}
await LimitGiftsRequestsAsync().ConfigureAwait(false);
Dictionary<uint, string> ownedGames;
if (await ArchiWebHandler.HasValidApiKey().ConfigureAwait(false)) {
ownedGames = await ArchiWebHandler.GetOwnedGames(SteamID).ConfigureAwait(false);
@@ -3224,7 +3270,7 @@ namespace ArchiSteamFarm {
// We add extra delay because OnFarmingStopped() also executes PlayGames()
// Despite of proper order on our end, Steam network might not respect it
await Task.Delay(CallbackSleep).ConfigureAwait(false);
ArchiHandler.PlayGames(Enumerable.Empty<uint>(), BotConfig.CustomGamePlayedWhileIdle);
await ArchiHandler.PlayGames(Enumerable.Empty<uint>(), BotConfig.CustomGamePlayedWhileIdle).ConfigureAwait(false);
}
if (resumeInSeconds > 0) {
@@ -3298,7 +3344,7 @@ namespace ArchiSteamFarm {
await CardsFarmer.Pause(false).ConfigureAwait(false);
}
ArchiHandler.PlayGames(gameIDs);
await ArchiHandler.PlayGames(gameIDs).ConfigureAwait(false);
return FormatBotResponse(Strings.Done);
}
@@ -3385,7 +3431,7 @@ namespace ArchiSteamFarm {
}
bool forward = !redeemFlags.HasFlag(ERedeemFlags.SkipForwarding) && (redeemFlags.HasFlag(ERedeemFlags.ForceForwarding) || BotConfig.RedeemingPreferences.HasFlag(BotConfig.ERedeemingPreferences.Forwarding));
bool distribute = !redeemFlags.HasFlag(ERedeemFlags.SkipDistribution) && (redeemFlags.HasFlag(ERedeemFlags.ForceDistribution) || BotConfig.RedeemingPreferences.HasFlag(BotConfig.ERedeemingPreferences.Distributing));
bool distribute = !redeemFlags.HasFlag(ERedeemFlags.SkipDistributing) && (redeemFlags.HasFlag(ERedeemFlags.ForceDistributing) || BotConfig.RedeemingPreferences.HasFlag(BotConfig.ERedeemingPreferences.Distributing));
bool keepMissingGames = !redeemFlags.HasFlag(ERedeemFlags.SkipKeepMissingGames) && (redeemFlags.HasFlag(ERedeemFlags.ForceKeepMissingGames) || BotConfig.RedeemingPreferences.HasFlag(BotConfig.ERedeemingPreferences.KeepMissingGames));
string[] keysList = keys.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
@@ -3799,7 +3845,7 @@ namespace ArchiSteamFarm {
return (FormatBotResponse(Strings.BotStatusLocked), this);
}
if (CardsFarmer.CurrentGamesFarming.Count == 0) {
if (!CardsFarmer.NowFarming || (CardsFarmer.CurrentGamesFarming.Count == 0)) {
return (FormatBotResponse(Strings.BotStatusNotIdling), this);
}
@@ -3898,6 +3944,153 @@ namespace ArchiSteamFarm {
return responses.Count > 0 ? string.Join("", responses) : null;
}
private async Task<string> ResponseTransfer(ulong steamID, string mode, string botNameTo) {
if ((steamID == 0) || string.IsNullOrEmpty(botNameTo) || string.IsNullOrEmpty(mode)) {
ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(mode) + " || " + nameof(botNameTo));
return null;
}
if (!IsMaster(steamID)) {
return null;
}
if (!IsConnectedAndLoggedOn) {
return FormatBotResponse(Strings.BotNotConnected);
}
if (!LootingAllowed) {
return FormatBotResponse(Strings.BotLootingTemporarilyDisabled);
}
if (!Bots.TryGetValue(botNameTo, out Bot targetBot)) {
return IsOwner(steamID) ? FormatBotResponse(string.Format(Strings.BotNotFound, botNameTo)) : null;
}
if (targetBot.SteamID == 0) {
return FormatBotResponse(Strings.BotNotConnected);
}
if (targetBot.SteamID == SteamID) {
return FormatBotResponse(Strings.BotLootingYourself);
}
HashSet<Steam.Item.EType> transferTypes = new HashSet<Steam.Item.EType>();
string[] modes = mode.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string singleMode in modes) {
switch (singleMode.ToUpper()) {
case "A":
case "ALL":
foreach (Steam.Item.EType type in Enum.GetValues(typeof(Steam.Item.EType))) {
transferTypes.Add(type);
}
break;
case "BG":
case "BACKGROUND":
transferTypes.Add(Steam.Item.EType.ProfileBackground);
break;
case "BO":
case "BOOSTER":
transferTypes.Add(Steam.Item.EType.BoosterPack);
break;
case "C":
case "CARD":
transferTypes.Add(Steam.Item.EType.TradingCard);
break;
case "E":
case "EMOTICON":
transferTypes.Add(Steam.Item.EType.Emoticon);
break;
case "F":
case "FOIL":
transferTypes.Add(Steam.Item.EType.FoilTradingCard);
break;
case "G":
case "GEMS":
transferTypes.Add(Steam.Item.EType.SteamGems);
break;
case "U":
case "UNKNOWN":
transferTypes.Add(Steam.Item.EType.Unknown);
break;
default:
return FormatBotResponse(string.Format(Strings.ErrorIsInvalid, mode));
}
}
if (!LootingSemaphore.Wait(0)) {
return FormatBotResponse(Strings.BotLootingFailed);
}
try {
HashSet<Steam.Item> inventory = await ArchiWebHandler.GetMySteamInventory(true, transferTypes).ConfigureAwait(false);
if ((inventory == null) || (inventory.Count == 0)) {
return FormatBotResponse(string.Format(Strings.ErrorIsEmpty, nameof(inventory)));
}
string tradeToken = null;
if (SteamFriends.GetFriendRelationship(targetBot.SteamID) != EFriendRelationship.Friend) {
tradeToken = await targetBot.ArchiWebHandler.GetTradeToken().ConfigureAwait(false);
if (string.IsNullOrEmpty(tradeToken)) {
return FormatBotResponse(Strings.BotLootingFailed);
}
}
if (!await ArchiWebHandler.MarkSentTrades().ConfigureAwait(false)) {
return FormatBotResponse(Strings.BotLootingFailed);
}
if (!await ArchiWebHandler.SendTradeOffer(inventory, targetBot.SteamID, tradeToken).ConfigureAwait(false)) {
return FormatBotResponse(Strings.BotLootingFailed);
}
if (HasMobileAuthenticator) {
// Give Steam network some time to generate confirmations
await Task.Delay(3000).ConfigureAwait(false);
if (!await AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, targetBot.SteamID).ConfigureAwait(false)) {
return FormatBotResponse(Strings.BotLootingFailed);
}
}
} finally {
LootingSemaphore.Release();
}
return FormatBotResponse(Strings.BotLootingSuccess);
}
private static async Task<string> ResponseTransfer(ulong steamID, string botNames, string mode, string botNameTo) {
if ((steamID == 0) || string.IsNullOrEmpty(botNames) || string.IsNullOrEmpty(mode) || string.IsNullOrEmpty(botNameTo)) {
ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botNames) + " || " + nameof(mode) + " || " + nameof(botNameTo));
return null;
}
HashSet<Bot> bots = GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return IsOwner(steamID) ? FormatStaticResponse(string.Format(Strings.BotNotFound, botNames)) : null;
}
IEnumerable<Task<string>> tasks = bots.Select(bot => bot.ResponseTransfer(steamID, mode, botNameTo));
ICollection<string> results;
switch (Program.GlobalConfig.OptimizationMode) {
case GlobalConfig.EOptimizationMode.MinMemoryUsage:
results = new List<string>(bots.Count);
foreach (Task<string> task in tasks) {
results.Add(await task.ConfigureAwait(false));
}
break;
default:
results = await Task.WhenAll(tasks).ConfigureAwait(false);
break;
}
List<string> responses = new List<string>(results.Where(result => !string.IsNullOrEmpty(result)));
return responses.Count > 0 ? string.Join("", responses) : null;
}
private string ResponseUnknown(ulong steamID) {
if (steamID != 0) {
return IsOperator(steamID) ? FormatBotResponse(Strings.UnknownCommand) : null;
@@ -4077,7 +4270,7 @@ namespace ArchiSteamFarm {
TwoFactorCode = inputValue;
break;
default:
ASF.ArchiLogger.LogGenericWarning(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(inputType), inputType));
ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(inputType), inputType));
break;
}
}
@@ -4093,7 +4286,7 @@ namespace ArchiSteamFarm {
if (!HasMobileAuthenticator) {
string maFilePath = BotPath + ".maFile";
if (File.Exists(maFilePath)) {
ImportAuthenticator(maFilePath);
await ImportAuthenticator(maFilePath).ConfigureAwait(false);
}
}
@@ -4146,8 +4339,8 @@ namespace ArchiSteamFarm {
Validate = 1,
ForceForwarding = 2,
SkipForwarding = 4,
ForceDistribution = 8,
SkipDistribution = 16,
ForceDistributing = 8,
SkipDistributing = 16,
SkipInitial = 32,
ForceKeepMissingGames = 64,
SkipKeepMissingGames = 128

View File

@@ -172,6 +172,11 @@ namespace ArchiSteamFarm {
// This constructor is used only by deserializer
private BotConfig() { }
// Functions below are used for skipping serialization of sensitive fields in API response
public bool ShouldSerializeSteamLogin() => false;
public bool ShouldSerializeSteamParentalPIN() => false;
public bool ShouldSerializeSteamPassword() => false;
internal static BotConfig Load(string filePath) {
if (string.IsNullOrEmpty(filePath)) {
ASF.ArchiLogger.LogNullError(nameof(filePath));

View File

@@ -26,49 +26,25 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace ArchiSteamFarm {
internal sealed class BotDatabase {
internal sealed class BotDatabase : IDisposable {
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentHashSet<ulong> BlacklistedFromTradesSteamIDs = new ConcurrentHashSet<ulong>();
private readonly object FileLock = new object();
private readonly SemaphoreSlim FileSemaphore = new SemaphoreSlim(1, 1);
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentHashSet<uint> IdlingPriorityAppIDs = new ConcurrentHashSet<uint>();
internal string LoginKey {
get => _LoginKey;
[JsonProperty(PropertyName = "_LoginKey")]
internal string LoginKey { get; private set; }
set {
if (_LoginKey == value) {
return;
}
_LoginKey = value;
Save();
}
}
internal MobileAuthenticator MobileAuthenticator {
get => _MobileAuthenticator;
set {
if (_MobileAuthenticator == value) {
return;
}
_MobileAuthenticator = value;
Save();
}
}
[JsonProperty]
private string _LoginKey;
[JsonProperty]
private MobileAuthenticator _MobileAuthenticator;
[JsonProperty(PropertyName = "_MobileAuthenticator")]
internal MobileAuthenticator MobileAuthenticator { get; private set; }
private string FilePath;
@@ -79,43 +55,51 @@ namespace ArchiSteamFarm {
}
FilePath = filePath;
Save();
Save().Wait();
}
// This constructor is used only by deserializer
[SuppressMessage("ReSharper", "UnusedMember.Local")]
private BotDatabase() { }
internal void AddBlacklistedFromTradesSteamIDs(HashSet<ulong> steamIDs) {
public void Dispose() {
// Those are objects that are always being created if constructor doesn't throw exception
FileSemaphore.Dispose();
// Those are objects that might be null and the check should be in-place
MobileAuthenticator?.Dispose();
}
internal async Task AddBlacklistedFromTradesSteamIDs(HashSet<ulong> steamIDs) {
if ((steamIDs == null) || (steamIDs.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(steamIDs));
return;
}
if (BlacklistedFromTradesSteamIDs.AddRange(steamIDs)) {
Save();
await Save().ConfigureAwait(false);
}
}
internal void AddIdlingPriorityAppIDs(HashSet<uint> appIDs) {
internal async Task AddIdlingPriorityAppIDs(HashSet<uint> appIDs) {
if ((appIDs == null) || (appIDs.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(appIDs));
return;
}
if (IdlingPriorityAppIDs.AddRange(appIDs)) {
Save();
await Save().ConfigureAwait(false);
}
}
internal void CorrectMobileAuthenticatorDeviceID(string deviceID) {
internal async Task CorrectMobileAuthenticatorDeviceID(string deviceID) {
if (string.IsNullOrEmpty(deviceID) || (MobileAuthenticator == null)) {
ASF.ArchiLogger.LogNullError(nameof(deviceID) + " || " + nameof(MobileAuthenticator));
return;
}
if (MobileAuthenticator.CorrectDeviceID(deviceID)) {
Save();
await Save().ConfigureAwait(false);
}
}
@@ -170,49 +154,69 @@ namespace ArchiSteamFarm {
return botDatabase;
}
internal void RemoveBlacklistedFromTradesSteamIDs(HashSet<ulong> steamIDs) {
internal async Task RemoveBlacklistedFromTradesSteamIDs(HashSet<ulong> steamIDs) {
if ((steamIDs == null) || (steamIDs.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(steamIDs));
return;
}
if (BlacklistedFromTradesSteamIDs.RemoveRange(steamIDs)) {
Save();
await Save().ConfigureAwait(false);
}
}
internal void RemoveIdlingPriorityAppIDs(HashSet<uint> appIDs) {
internal async Task RemoveIdlingPriorityAppIDs(HashSet<uint> appIDs) {
if ((appIDs == null) || (appIDs.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(appIDs));
return;
}
if (IdlingPriorityAppIDs.RemoveRange(appIDs)) {
Save();
await Save().ConfigureAwait(false);
}
}
private void Save() {
internal async Task SetLoginKey(string value = null) {
if (value == LoginKey) {
return;
}
LoginKey = value;
await Save().ConfigureAwait(false);
}
internal async Task SetMobileAuthenticator(MobileAuthenticator value = null) {
if (value == MobileAuthenticator) {
return;
}
MobileAuthenticator = value;
await Save().ConfigureAwait(false);
}
private async Task Save() {
string json = JsonConvert.SerializeObject(this);
if (string.IsNullOrEmpty(json)) {
ASF.ArchiLogger.LogNullError(nameof(json));
return;
}
lock (FileLock) {
string newFilePath = FilePath + ".new";
string newFilePath = FilePath + ".new";
try {
File.WriteAllText(newFilePath, json);
await FileSemaphore.WaitAsync().ConfigureAwait(false);
if (File.Exists(FilePath)) {
File.Replace(newFilePath, FilePath, null);
} else {
File.Move(newFilePath, FilePath);
}
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
try {
await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false);
if (File.Exists(FilePath)) {
File.Replace(newFilePath, FilePath, null);
} else {
File.Move(newFilePath, FilePath);
}
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
} finally {
FileSemaphore.Release();
}
}
}

View File

@@ -38,7 +38,9 @@ using SteamKit2;
namespace ArchiSteamFarm {
internal sealed class CardsFarmer : IDisposable {
private const byte HoursToBump = 2; // How many hours are required for restricted accounts
internal const byte DaysForRefund = 14; // In how many days since payment we're allowed to refund
internal const byte HoursToBump = 2; // How many hours are required for restricted accounts
private const byte HoursToIgnore = 24; // How many hours we ignore unreleased appIDs and don't bother checking them again
private static readonly ConcurrentDictionary<uint, DateTime> IgnoredAppIDs = new ConcurrentDictionary<uint, DateTime>(); // Reserved for unreleased games
@@ -52,23 +54,25 @@ namespace ArchiSteamFarm {
[JsonProperty]
internal TimeSpan TimeRemaining => new TimeSpan(
Bot.BotConfig.CardDropsRestricted ? (int) Math.Ceiling(GamesToFarm.Count / (float) ArchiHandler.MaxGamesPlayedConcurrently) * HoursToBump : 0,
Bot.BotConfig.CardDropsRestricted ? (ushort) Math.Ceiling(GamesToFarm.Count / (float) ArchiHandler.MaxGamesPlayedConcurrently) * HoursToBump : 0,
30 * GamesToFarm.Sum(game => game.CardsRemaining),
0
);
private readonly Bot Bot;
private readonly SemaphoreSlim EventSemaphore = new SemaphoreSlim(1);
private readonly SemaphoreSlim FarmingSemaphore = new SemaphoreSlim(1);
private readonly ManualResetEventSlim FarmResetEvent = new ManualResetEventSlim(false);
private readonly SemaphoreSlim EventSemaphore = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim FarmingInitializationSemaphore = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim FarmingResetSemaphore = new SemaphoreSlim(0, 1);
private readonly Timer IdleFarmingTimer;
internal bool NowFarming { get; private set; }
[JsonProperty]
internal bool Paused { get; private set; }
private bool KeepFarming;
private bool NowFarming;
private bool ParsingScheduled;
private bool ShouldResumeFarming = true;
private bool StickyPause;
internal CardsFarmer(Bot bot) {
@@ -87,8 +91,9 @@ namespace ArchiSteamFarm {
public void Dispose() {
// Those are objects that are always being created if constructor doesn't throw exception
EventSemaphore.Dispose();
FarmingSemaphore.Dispose();
FarmResetEvent.Dispose();
FarmingInitializationSemaphore.Dispose();
FarmingResetSemaphore.Dispose();
GamesToFarm.Dispose();
// Those are objects that might be null and the check should be in-place
IdleFarmingTimer?.Dispose();
@@ -103,6 +108,8 @@ namespace ArchiSteamFarm {
}
internal async Task OnNewGameAdded() {
ShouldResumeFarming = true;
// We aim to have a maximum of 2 tasks, one already parsing, and one waiting in the queue
// This way we can call this function as many times as needed e.g. because of Steam events
lock (EventSemaphore) {
@@ -140,8 +147,19 @@ namespace ArchiSteamFarm {
internal async Task OnNewItemsNotification() {
if (NowFarming) {
FarmResetEvent.Set();
return;
await FarmingInitializationSemaphore.WaitAsync().ConfigureAwait(false);
try {
if (NowFarming) {
if (FarmingResetSemaphore.CurrentCount == 0) {
FarmingResetSemaphore.Release();
}
return;
}
} finally {
FarmingInitializationSemaphore.Release();
}
}
// If we're not farming, and we got new items, it's likely to be a booster pack or likewise
@@ -155,25 +173,36 @@ namespace ArchiSteamFarm {
}
Paused = true;
if (NowFarming) {
await StopFarming().ConfigureAwait(false);
if (!NowFarming) {
return;
}
await StopFarming().ConfigureAwait(false);
}
internal async Task Resume(bool userAction) {
internal async Task<bool> Resume(bool userAction) {
if (StickyPause) {
if (!userAction) {
Bot.ArchiLogger.LogGenericInfo(Strings.IgnoredStickyPauseEnabled);
return;
return false;
}
StickyPause = false;
}
Paused = false;
if (!NowFarming) {
await StartFarming().ConfigureAwait(false);
if (NowFarming) {
return true;
}
if (!userAction && !ShouldResumeFarming) {
return false;
}
await StartFarming().ConfigureAwait(false);
return true;
}
internal void SetInitialState(bool paused) => StickyPause = Paused = paused;
@@ -188,7 +217,7 @@ namespace ArchiSteamFarm {
return;
}
await FarmingSemaphore.WaitAsync().ConfigureAwait(false);
await FarmingInitializationSemaphore.WaitAsync().ConfigureAwait(false);
try {
if (NowFarming || Paused || !Bot.IsPlayingPossible) {
@@ -231,7 +260,7 @@ namespace ArchiSteamFarm {
KeepFarming = NowFarming = true;
Utilities.StartBackgroundFunction(Farm);
} finally {
FarmingSemaphore.Release();
FarmingInitializationSemaphore.Release();
}
}
@@ -240,7 +269,7 @@ namespace ArchiSteamFarm {
return;
}
await FarmingSemaphore.WaitAsync().ConfigureAwait(false);
await FarmingInitializationSemaphore.WaitAsync().ConfigureAwait(false);
try {
if (!NowFarming) {
@@ -248,9 +277,12 @@ namespace ArchiSteamFarm {
}
KeepFarming = false;
FarmResetEvent.Set();
for (byte i = 0; (i < 5) && NowFarming; i++) {
if (FarmingResetSemaphore.CurrentCount == 0) {
FarmingResetSemaphore.Release();
}
for (byte i = 0; (i < WebBrowser.MaxTries) && NowFarming; i++) {
await Task.Delay(1000).ConfigureAwait(false);
}
@@ -259,9 +291,9 @@ namespace ArchiSteamFarm {
}
Bot.ArchiLogger.LogGenericInfo(Strings.IdlingStopped);
Bot.OnFarmingStopped();
await Bot.OnFarmingStopped().ConfigureAwait(false);
} finally {
FarmingSemaphore.Release();
FarmingInitializationSemaphore.Release();
}
}
@@ -334,7 +366,7 @@ namespace ArchiSteamFarm {
continue;
}
if (GlobalConfig.GlobalBlacklist.Contains(appID) || Program.GlobalConfig.Blacklist.Contains(appID)) {
if (GlobalConfig.GamesBlacklist.Contains(appID) || GlobalConfig.SalesBlacklist.Contains(appID) || Program.GlobalConfig.Blacklist.Contains(appID)) {
// We have this appID blacklisted, so skip it
continue;
}
@@ -556,65 +588,72 @@ namespace ArchiSteamFarm {
// If we have restricted card drops, we use complex algorithm
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.ChosenFarmingAlgorithm, "Complex"));
while (GamesToFarm.Count > 0) {
HashSet<Game> playableGamesToFarmSolo = new HashSet<Game>();
foreach (Game game in GamesToFarm.Where(game => game.HoursPlayed >= HoursToBump)) {
if (await IsPlayableGame(game).ConfigureAwait(false)) {
playableGamesToFarmSolo.Add(game);
}
}
HashSet<Game> gamesToCheck = new HashSet<Game>(GamesToFarm.Count > 1 ? GamesToFarm.Where(game => game.HoursPlayed >= HoursToBump) : GamesToFarm);
if (playableGamesToFarmSolo.Count > 0) {
while (playableGamesToFarmSolo.Count > 0) {
Game playableGame = playableGamesToFarmSolo.First();
if (await FarmSolo(playableGame).ConfigureAwait(false)) {
playableGamesToFarmSolo.Remove(playableGame);
} else {
NowFarming = false;
return;
}
}
} else {
HashSet<Game> playableGamesToFarmMultiple = new HashSet<Game>();
foreach (Game game in GamesToFarm.Where(game => game.HoursPlayed < HoursToBump).OrderByDescending(game => game.HoursPlayed)) {
if (await IsPlayableGame(game).ConfigureAwait(false)) {
playableGamesToFarmMultiple.Add(game);
if (gamesToCheck.Count > 0) {
foreach (Game game in gamesToCheck) {
if (!await IsPlayableGame(game).ConfigureAwait(false)) {
GamesToFarm.Remove(game);
continue;
}
if (playableGamesToFarmMultiple.Count >= ArchiHandler.MaxGamesPlayedConcurrently) {
break;
if (await FarmSolo(game).ConfigureAwait(false)) {
continue;
}
}
if (FarmMultiple(playableGamesToFarmMultiple)) {
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.IdlingFinishedForGames, string.Join(", ", playableGamesToFarmMultiple.Select(game => game.AppID))));
} else {
NowFarming = false;
return;
}
continue;
}
gamesToCheck = new HashSet<Game>(GamesToFarm.OrderByDescending(game => game.HoursPlayed));
HashSet<Game> playableGamesToFarmMultiple = new HashSet<Game>();
foreach (Game game in gamesToCheck) {
if (!await IsPlayableGame(game).ConfigureAwait(false)) {
GamesToFarm.Remove(game);
continue;
}
playableGamesToFarmMultiple.Add(game);
if (playableGamesToFarmMultiple.Count >= ArchiHandler.MaxGamesPlayedConcurrently) {
break;
}
}
if (playableGamesToFarmMultiple.Count == 0) {
break;
}
if (await FarmMultiple(playableGamesToFarmMultiple).ConfigureAwait(false)) {
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.IdlingFinishedForGames, string.Join(", ", playableGamesToFarmMultiple.Select(game => game.AppID))));
} else {
NowFarming = false;
return;
}
}
} else {
// If we have unrestricted card drops, we use simple algorithm
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.ChosenFarmingAlgorithm, "Simple"));
while (GamesToFarm.Count > 0) {
Game playableGame = null;
foreach (Game game in GamesToFarm) {
HashSet<Game> gamesToCheck = new HashSet<Game>(GamesToFarm);
foreach (Game game in gamesToCheck) {
if (!await IsPlayableGame(game).ConfigureAwait(false)) {
GamesToFarm.Remove(game);
continue;
}
playableGame = game;
break;
}
if (playableGame != null) {
if (await FarmSolo(playableGame).ConfigureAwait(false)) {
if (await FarmSolo(game).ConfigureAwait(false)) {
continue;
}
}
NowFarming = false;
return;
NowFarming = false;
return;
}
}
}
} while ((await IsAnythingToFarm().ConfigureAwait(false)).GetValueOrDefault());
@@ -638,7 +677,7 @@ namespace ArchiSteamFarm {
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.WarningIdlingGameMismatch, game.AppID, game.GameName, game.PlayableAppID));
}
Bot.PlayGame(game.PlayableAppID, Bot.BotConfig.CustomGamePlayedWhileFarming);
await Bot.IdleGames(game.PlayableAppID.ToEnumerable()).ConfigureAwait(false);
DateTime endFarmingDate = DateTime.UtcNow.AddHours(Program.GlobalConfig.MaxFarmingTime);
bool? keepFarming = await ShouldFarm(game).ConfigureAwait(false);
@@ -646,8 +685,7 @@ namespace ArchiSteamFarm {
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.StillIdling, game.AppID, game.GameName));
DateTime startFarmingPeriod = DateTime.UtcNow;
if (FarmResetEvent.Wait(60 * 1000 * Program.GlobalConfig.FarmingDelay)) {
FarmResetEvent.Reset();
if (await FarmingResetSemaphore.WaitAsync(60 * 1000 * Program.GlobalConfig.FarmingDelay).ConfigureAwait(false)) {
success = KeepFarming;
}
@@ -665,7 +703,7 @@ namespace ArchiSteamFarm {
return success;
}
private bool FarmHours(ConcurrentHashSet<Game> games) {
private async Task<bool> FarmHours(ConcurrentHashSet<Game> games) {
if ((games == null) || (games.Count == 0)) {
Bot.ArchiLogger.LogNullError(nameof(games));
return false;
@@ -682,15 +720,14 @@ namespace ArchiSteamFarm {
return true;
}
Bot.PlayGames(games.Select(game => game.AppID), Bot.BotConfig.CustomGamePlayedWhileFarming);
await Bot.IdleGames(games.Select(game => game.PlayableAppID)).ConfigureAwait(false);
bool success = true;
while (maxHour < 2) {
while (maxHour < HoursToBump) {
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.StillIdlingList, string.Join(", ", games.Select(game => game.AppID))));
DateTime startFarmingPeriod = DateTime.UtcNow;
if (FarmResetEvent.Wait(60 * 1000 * Program.GlobalConfig.FarmingDelay)) {
FarmResetEvent.Reset();
if (await FarmingResetSemaphore.WaitAsync(60 * 1000 * Program.GlobalConfig.FarmingDelay).ConfigureAwait(false)) {
success = KeepFarming;
}
@@ -711,7 +748,7 @@ namespace ArchiSteamFarm {
return success;
}
private bool FarmMultiple(IEnumerable<Game> games) {
private async Task<bool> FarmMultiple(IEnumerable<Game> games) {
if (games == null) {
Bot.ArchiLogger.LogNullError(nameof(games));
return false;
@@ -721,7 +758,7 @@ namespace ArchiSteamFarm {
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.NowIdlingList, string.Join(", ", CurrentGamesFarming.Select(game => game.AppID))));
bool result = FarmHours(CurrentGamesFarming);
bool result = await FarmHours(CurrentGamesFarming).ConfigureAwait(false);
CurrentGamesFarming.Clear();
return result;
}
@@ -846,15 +883,17 @@ namespace ArchiSteamFarm {
}
if (GamesToFarm.Count == 0) {
ShouldResumeFarming = false;
return false;
}
ShouldResumeFarming = true;
SortGamesToFarm();
return true;
}
private async Task<bool> IsPlayableGame(Game game) {
(uint PlayableAppID, DateTime IgnoredUntil) appData = await Bot.GetAppDataForIdling(game.AppID).ConfigureAwait(false);
(uint PlayableAppID, DateTime IgnoredUntil) appData = await Bot.GetAppDataForIdling(game.AppID, game.HoursPlayed).ConfigureAwait(false);
if (appData.PlayableAppID != 0) {
game.PlayableAppID = appData.PlayableAppID;
return true;
@@ -926,7 +965,7 @@ namespace ArchiSteamFarm {
case BotConfig.EFarmingOrder.RedeemDateTimesDescending:
Dictionary<uint, DateTime> redeemDates = new Dictionary<uint, DateTime>(GamesToFarm.Count);
foreach (Game game in gamesToFarm) {
foreach (Game game in GamesToFarm) {
DateTime redeemDate = DateTime.MinValue;
if (Program.GlobalDatabase.AppIDsToPackageIDs.TryGetValue(game.AppID, out ConcurrentHashSet<uint> packageIDs)) {
// ReSharper disable once LoopCanBePartlyConvertedToQuery - C# 7.0 out can't be used within LINQ query yet | https://github.com/dotnet/roslyn/issues/15619

View File

@@ -31,12 +31,12 @@ namespace ArchiSteamFarm {
internal sealed class ConcurrentSortedHashSet<T> : IDisposable, IReadOnlyCollection<T>, ISet<T> {
public int Count {
get {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
return BackingCollection.Count;
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
}
@@ -44,159 +44,159 @@ namespace ArchiSteamFarm {
public bool IsReadOnly => false;
private readonly HashSet<T> BackingCollection = new HashSet<T>();
private readonly SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(1);
private readonly SemaphoreSlim CollectionSemaphore = new SemaphoreSlim(1, 1);
public bool Add(T item) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
return BackingCollection.Add(item);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public void Clear() {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
BackingCollection.Clear();
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public bool Contains(T item) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
return BackingCollection.Contains(item);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public void CopyTo(T[] array, int arrayIndex) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
BackingCollection.CopyTo(array, arrayIndex);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public void Dispose() => SemaphoreSlim.Dispose();
public void Dispose() => CollectionSemaphore.Dispose();
public void ExceptWith(IEnumerable<T> other) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
BackingCollection.ExceptWith(other);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public IEnumerator<T> GetEnumerator() => new ConcurrentEnumerator<T>(BackingCollection, SemaphoreSlim);
public IEnumerator<T> GetEnumerator() => new ConcurrentEnumerator<T>(BackingCollection, CollectionSemaphore);
public void IntersectWith(IEnumerable<T> other) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
BackingCollection.IntersectWith(other);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public bool IsProperSubsetOf(IEnumerable<T> other) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
return BackingCollection.IsProperSubsetOf(other);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public bool IsProperSupersetOf(IEnumerable<T> other) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
return BackingCollection.IsProperSupersetOf(other);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public bool IsSubsetOf(IEnumerable<T> other) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
return BackingCollection.IsSubsetOf(other);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public bool IsSupersetOf(IEnumerable<T> other) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
return BackingCollection.IsSupersetOf(other);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public bool Overlaps(IEnumerable<T> other) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
return BackingCollection.Overlaps(other);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public bool Remove(T item) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
return BackingCollection.Remove(item);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public bool SetEquals(IEnumerable<T> other) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
return BackingCollection.SetEquals(other);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public void SymmetricExceptWith(IEnumerable<T> other) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
BackingCollection.SymmetricExceptWith(other);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
public void UnionWith(IEnumerable<T> other) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
BackingCollection.UnionWith(other);
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
@@ -204,7 +204,7 @@ namespace ArchiSteamFarm {
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
internal void ReplaceWith(IEnumerable<T> other) {
SemaphoreSlim.Wait();
CollectionSemaphore.Wait();
try {
BackingCollection.Clear();
@@ -213,7 +213,7 @@ namespace ArchiSteamFarm {
BackingCollection.Add(item);
}
} finally {
SemaphoreSlim.Release();
CollectionSemaphore.Release();
}
}
}

View File

@@ -40,8 +40,8 @@ namespace ArchiSteamFarm {
internal const byte DefaultLoginLimiterDelay = 10;
internal const string UlongStringPrefix = "s_";
// This is hardcoded blacklist which should not be possible to change
internal static readonly HashSet<uint> GlobalBlacklist = new HashSet<uint> { 267420, 303700, 335590, 368020, 402590, 425280, 480730, 566020, 639900 };
internal static readonly HashSet<uint> GamesBlacklist = new HashSet<uint> { 402590 }; // Games with broken/unobtainable card drops
internal static readonly HashSet<uint> SalesBlacklist = new HashSet<uint> { 267420, 303700, 335590, 368020, 425280, 480730, 566020, 639900 }; // Steam Summer/Winter sales
[JsonProperty(Required = Required.DisallowNull)]
internal readonly bool AutoRestart = true;
@@ -49,6 +49,9 @@ namespace ArchiSteamFarm {
[JsonProperty(Required = Required.DisallowNull)]
internal readonly bool AutoUpdates = true;
[JsonProperty(Required = Required.DisallowNull)]
internal readonly byte BackgroundGCPeriod;
[SuppressMessage("ReSharper", "CollectionNeverUpdated.Global")]
[JsonProperty(Required = Required.DisallowNull)]
internal readonly HashSet<uint> Blacklist = new HashSet<uint>();
@@ -78,7 +81,7 @@ namespace ArchiSteamFarm {
#pragma warning restore 649
[JsonProperty(Required = Required.DisallowNull)]
internal readonly byte IdleFarmingPeriod = 3;
internal readonly byte IdleFarmingPeriod = 8;
[JsonProperty(Required = Required.DisallowNull)]
internal readonly byte InventoryLimiterDelay = 3;
@@ -102,7 +105,7 @@ namespace ArchiSteamFarm {
internal readonly bool Statistics = true;
[JsonProperty(Required = Required.DisallowNull)]
internal readonly ProtocolTypes SteamProtocols = ProtocolTypes.All;
internal readonly ProtocolTypes SteamProtocols = ProtocolTypes.WebSocket;
[JsonProperty(Required = Required.DisallowNull)]
internal readonly EUpdateChannel UpdateChannel = EUpdateChannel.Stable;

View File

@@ -42,24 +42,11 @@ namespace ArchiSteamFarm {
[JsonProperty(Required = Required.DisallowNull)]
internal readonly InMemoryServerListProvider ServerListProvider = new InMemoryServerListProvider();
private readonly object FileLock = new object();
private readonly SemaphoreSlim FileSemaphore = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim PackagesRefreshSemaphore = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim PackagesRefreshSemaphore = new SemaphoreSlim(1);
internal uint CellID {
get => _CellID;
set {
if ((value == 0) || (_CellID == value)) {
return;
}
_CellID = value;
Save();
}
}
[JsonProperty(Required = Required.DisallowNull)]
private uint _CellID;
[JsonProperty(PropertyName = "_CellID", Required = Required.DisallowNull)]
internal uint CellID { get; private set; }
private string FilePath;
@@ -70,13 +57,20 @@ namespace ArchiSteamFarm {
}
FilePath = filePath;
Save();
Save().Wait();
}
// This constructor is used only by deserializer
private GlobalDatabase() => ServerListProvider.ServerListUpdated += OnServerListUpdated;
public void Dispose() => ServerListProvider.ServerListUpdated -= OnServerListUpdated;
public void Dispose() {
// Events we registered
ServerListProvider.ServerListUpdated -= OnServerListUpdated;
// Those are objects that are always being created if constructor doesn't throw exception
FileSemaphore.Dispose();
PackagesRefreshSemaphore.Dispose();
}
internal static GlobalDatabase Load(string filePath) {
if (string.IsNullOrEmpty(filePath)) {
@@ -120,7 +114,7 @@ namespace ArchiSteamFarm {
return;
}
Dictionary<uint, HashSet<uint>> appIDsToPackageIDs = await bot.GetAppIDsToPackageIDs(missingPackageIDs);
Dictionary<uint, HashSet<uint>> appIDsToPackageIDs = await bot.GetAppIDsToPackageIDs(missingPackageIDs).ConfigureAwait(false);
if ((appIDsToPackageIDs == null) || (appIDsToPackageIDs.Count == 0)) {
return;
}
@@ -136,35 +130,46 @@ namespace ArchiSteamFarm {
}
}
Save();
await Save().ConfigureAwait(false);
} finally {
PackagesRefreshSemaphore.Release();
}
}
private void OnServerListUpdated(object sender, EventArgs e) => Save();
internal async Task SetCellID(uint value = 0) {
if (value == CellID) {
return;
}
private void Save() {
CellID = value;
await Save().ConfigureAwait(false);
}
private async void OnServerListUpdated(object sender, EventArgs e) => await Save().ConfigureAwait(false);
private async Task Save() {
string json = JsonConvert.SerializeObject(this);
if (string.IsNullOrEmpty(json)) {
ASF.ArchiLogger.LogNullError(nameof(json));
return;
}
lock (FileLock) {
string newFilePath = FilePath + ".new";
string newFilePath = FilePath + ".new";
try {
File.WriteAllText(newFilePath, json);
await FileSemaphore.WaitAsync().ConfigureAwait(false);
if (File.Exists(FilePath)) {
File.Replace(newFilePath, FilePath, null);
} else {
File.Move(newFilePath, FilePath);
}
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
try {
await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false);
if (File.Exists(FilePath)) {
File.Replace(newFilePath, FilePath, null);
} else {
File.Move(newFilePath, FilePath);
}
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
} finally {
FileSemaphore.Release();
}
}
}

View File

@@ -28,18 +28,21 @@ using System.Threading;
namespace ArchiSteamFarm {
internal static class Hacks {
private const byte GarbageCollectorDelay = 1;
private static Timer GarbageCollectionTimer;
private static Timer GarbageCompactionTimer;
internal static void Init() {
internal static void EnableBackgroundGC(byte period) {
if (period == 0) {
ASF.ArchiLogger.LogNullError(nameof(period));
return;
}
if (GarbageCollectionTimer == null) {
GarbageCollectionTimer = new Timer(
e => GC.Collect(),
null,
TimeSpan.FromSeconds(GarbageCollectorDelay), // Delay
TimeSpan.FromSeconds(GarbageCollectorDelay) // Period
TimeSpan.FromSeconds(period), // Delay
TimeSpan.FromSeconds(period) // Period
);
}
@@ -47,8 +50,8 @@ namespace ArchiSteamFarm {
GarbageCompactionTimer = new Timer(
e => GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce,
null,
TimeSpan.FromMinutes(GarbageCollectorDelay), // Delay
TimeSpan.FromMinutes(GarbageCollectorDelay) // Period
TimeSpan.FromMinutes(period), // Delay
TimeSpan.FromMinutes(period) // Period
);
}
}

View File

@@ -45,6 +45,14 @@ namespace ArchiSteamFarm {
return;
}
switch (host) {
case "0.0.0.0":
case "::":
// Silently map INADDR_ANY to match HttpListener expectations
host = "*";
break;
}
string url = "http://" + host + ":" + port + "/" + nameof(IPC) + "/";
HttpListener.Prefixes.Add(url);
}
@@ -167,12 +175,14 @@ namespace ArchiSteamFarm {
response.StatusCode = (ushort) statusCode;
}
response.AppendHeader("Access-Control-Allow-Origin", "null");
Encoding encoding = Encoding.UTF8;
response.ContentEncoding = encoding;
response.ContentType = "text/plain; charset=" + encoding.WebName;
string html = "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head><body><p>" + message + "</p></body></html>";
byte[] buffer = encoding.GetBytes(html);
byte[] buffer = encoding.GetBytes(message + Environment.NewLine);
response.ContentLength64 = buffer.Length;
await response.OutputStream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);

View File

@@ -673,5 +673,8 @@ StackTrace:
<value>Procházení fronty doporučení #{0} dokončeno.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>{0}/{1} botů již vlastní všechny uvedené hry.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -673,5 +673,8 @@ StackTrace:
<value>Fertig mit Löschung der Steam Entdeckungsliste #{0}.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>Es gibt {0}/{1}-Bots, die bereits alle geprüften Spiele besitzen.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -673,5 +673,8 @@ StackTrace:
<value>Ολοκληρώθηκε η εκκαθάριση σειράς ανακαλύψεων Steam #{0}.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>Υπάρχουν {0}/{1} bot που κατέχουν ήδη όλα τα παιχνίδια για τα οποία γίνεται έλεγχος.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -184,7 +184,9 @@ StackTrace :
<data name="ErrorUpdateCheckFailed" xml:space="preserve">
<value>Impossible de vérifier la dernière version !</value>
</data>
<data name="ErrorUpdateNoAssetForThisVersion" xml:space="preserve">
<value>Impossible de procéder à la mise à jour car il n'y a aucun fichier correspondant à la version actuelle ! La mise à jour automatique vers cette version n'est pas possible.</value>
</data>
<data name="ErrorUpdateNoAssets" xml:space="preserve">
<value>Impossible de procéder à une mise à jour parce que cette version ne contient aucun fichier !</value>
</data>
@@ -286,7 +288,10 @@ StackTrace :
<value>Veuillez entrer la valeur non documentée de {0} : </value>
<comment>{0} will be replaced by property name. Please note that this translation should end with space</comment>
</data>
<data name="UserInputIPCHost" xml:space="preserve">
<value>Veuillez saisir votre hôte IPC: </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="WarningUnknownValuePleaseReport" xml:space="preserve">
<value>{0} a reçu une valeur inconnue, veuillez le signaler : {1}</value>
<comment>{0} will be replaced by object's name, {1} will be replaced by value for that object</comment>
@@ -295,10 +300,20 @@ StackTrace :
<value>Jouer à plus de {0} jeux en même temps nest pas possible, seules les {0} premières entrées de {1} seront utilisées !</value>
<comment>{0} will be replaced by max number of games, {1} will be replaced by name of the configuration property</comment>
</data>
<data name="ErrorIPCAddressAccessDeniedException" xml:space="preserve">
<value>Le service IPC n'a pas pu démarrer en raison d'un refus d'accès AdressAccessDeniedException ! Si vous souhaitez utiliser le service IPC fourni par ASF, assurez-vous de lancer ASF en tant qu'administrateur ou avec les autorisations appropriées !</value>
</data>
<data name="IPCAnswered" xml:space="preserve">
<value>Réponse à la commande IPC: {0} avec {1}</value>
<comment>{0} will be replaced by IPC command, {1} will be replaced by IPC answer</comment>
</data>
<data name="IPCReady" xml:space="preserve">
<value>Serveur IPC prêt !</value>
</data>
<data name="IPCStarting" xml:space="preserve">
<value>Démarrage du serveur IPC sur {0}...</value>
<comment>{0} will be replaced by IPC hostname</comment>
</data>
<data name="BotAlreadyStopped" xml:space="preserve">
<value>Ce bot est déjà à l'arrêt !</value>
</data>
@@ -658,5 +673,8 @@ StackTrace :
<value>Fini de consulter la liste de découvertes #{0}.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>Il y a {0}/{1} bots qui possède déjà tous les jeux en cours de vérification.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -130,8 +130,7 @@
<comment>{0} will be replaced by content string. Please note that this string should include newline for formatting.</comment>
</data>
<data name="ErrorConfigPropertyInvalid" xml:space="preserve">
<value>ErrorPropertiConfiginvalid
{0} akan diubah dengan nama properti konfigurasi, {1} akan diubah dengan nilai invalid</value>
<value>Konfigurasi properti {0} tidak valid: {1}</value>
<comment>{0} will be replaced by name of the configuration property, {1} will be replaced by invalid value</comment>
</data>
<data name="ErrorEarlyFatalExceptionInfo" xml:space="preserve">
@@ -182,14 +181,19 @@
<data name="ErrorUpdateCheckFailed" xml:space="preserve">
<value>Tidak dapat memeriksa versi terbaru!</value>
</data>
<data name="ErrorUpdateNoAssetForThisVersion" xml:space="preserve">
<value>Tidak bisa melanjutkan dengan pembaruan karena tidak ada asset yang berkaitan dengan versi yang berjalan sekarang! Pembaruan otomatis ke versi tersebut tidak mungkin.</value>
</data>
<data name="ErrorUpdateNoAssets" xml:space="preserve">
<value>Tidak bisa melanjutkan update karena tak ada aset yang termasuk dalam versi tersebut!</value>
</data>
<data name="ErrorUserInputRunningInHeadlessMode" xml:space="preserve">
<value>Menerima permintaan untuk input pengguna, tetapi proses berjalan dalam mode Headless!</value>
</data>
<data name="ErrorIPCAccessDenied" xml:space="preserve">
<value>Menolak untuk menangani permintaan karena SteamOwnerID tidak diatur!</value>
<comment>SteamOwnerID is name of bot config property, it should not be translated</comment>
</data>
<data name="Exiting" xml:space="preserve">
<value>Menutup...</value>
</data>
@@ -239,9 +243,12 @@
<data name="UpdateCheckingNewVersion" xml:space="preserve">
<value>Sedang mengecek versi terbaru...</value>
</data>
<data name="UpdateDownloadingNewVersion" xml:space="preserve">
<value>Mengunduh versi baru: {0} ({1} MB)... Sambil menunggu, pertimbangkan untuk mengapresiasi seluruh kerja keras dengan mendonasi! :)</value>
<comment>{0} will be replaced by version string, {1} will be replaced by update size (in megabytes)</comment>
</data>
<data name="UpdateFinished" xml:space="preserve">
<value>Proses update selesai!</value>
<value>Proses pembaruan selesai!</value>
</data>
<data name="UpdateNewVersionAvailable" xml:space="preserve">
<value>Versi terbaru ASF tersedia! Pertimbangkan untuk di-update!</value>
@@ -278,19 +285,32 @@
<value>Masukkan suatu nilai yang tidak terdokumentasi {0}: </value>
<comment>{0} will be replaced by property name. Please note that this translation should end with space</comment>
</data>
<data name="UserInputIPCHost" xml:space="preserve">
<value>Masukkan host WCF Anda: </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="WarningUnknownValuePleaseReport" xml:space="preserve">
<value>Menerima nilai yang tidak diketahui untuk {0}, laporkan hal ini: {1}</value>
<value>Menerima nilai yang tidak diketahui untuk {0}, mohon laporkan ini: {1}</value>
<comment>{0} will be replaced by object's name, {1} will be replaced by value for that object</comment>
</data>
<data name="WarningTooManyGamesToPlay" xml:space="preserve">
<value>Tidak dapat bermain lebih dari {0} game secara bersamaan, hanya {0} entri pertama dari {1} game yang akan digunakan</value>
<comment>{0} will be replaced by max number of games, {1} will be replaced by name of the configuration property</comment>
</data>
<data name="ErrorIPCAddressAccessDeniedException" xml:space="preserve">
<value>Layanan WCF tidak bisa dimulai karena AddressAccessDeniedException! Jika anda ingin menggunakan layanan WCF yang disediakan ASF, jalankan ASF sebagai Administrator, atau berikan izin yang benar!</value>
</data>
<data name="IPCAnswered" xml:space="preserve">
<value>Jawaban untuk perintah IPC: {0} dengan: {1}</value>
<comment>{0} will be replaced by IPC command, {1} will be replaced by IPC answer</comment>
</data>
<data name="IPCReady" xml:space="preserve">
<value>Server IPC siap!</value>
</data>
<data name="IPCStarting" xml:space="preserve">
<value>Memulai server IPC di {0}...</value>
<comment>{0} will be replaced by IPC hostname</comment>
</data>
<data name="BotAlreadyStopped" xml:space="preserve">
<value>Bot ini sudah berhenti!</value>
</data>
@@ -317,14 +337,14 @@
<value>Mengecek halaman badge lainnya...</value>
</data>
<data name="ChosenFarmingAlgorithm" xml:space="preserve">
<value>Memilih Algoritma Idling: {0}</value>
<value>Memilih algoritma idling: {0}</value>
<comment>{0} will be replaced by the name of chosen idling algorithm</comment>
</data>
<data name="Done" xml:space="preserve">
<value>Selesai!</value>
</data>
<data name="GamesToIdle" xml:space="preserve">
<value>Kami memiliki total {0} permainan ({1} kartu) meninggalkan ke siaga (~{2} yang tersisa)...</value>
<value>Kita memiliki total {0} permainan ({1} kartu) tersisa untuk idle (~{2} tersisa)...</value>
<comment>{0} will be replaced by number of games, {1} will be replaced by number of cards, {2} will be replaced by translated TimeSpan string (such as "1 day, 5 hours and 30 minutes")</comment>
</data>
<data name="IdlingFinished" xml:space="preserve">
@@ -360,7 +380,7 @@
<comment>{0} will be replaced by list of the games (IDs, numbers), separated by a comma</comment>
</data>
<data name="PlayingNotAvailable" xml:space="preserve">
<value>Bermain sedang tidak tersedia, kami akan mencoba lagi nanti!</value>
<value>Bermain sedang tidak tersedia, kita akan coba lagi nanti!</value>
</data>
<data name="StillIdling" xml:space="preserve">
<value>Masih idling: {0} ({1})</value>
@@ -460,7 +480,7 @@
<comment>{0} will be replaced by logging off reason (string)</comment>
</data>
<data name="BotLoggedOn" xml:space="preserve">
<value>Berhasil Login!</value>
<value>Berhasil login!</value>
</data>
<data name="BotLoggingIn" xml:space="preserve">
<value>Sedang masuk...</value>
@@ -650,5 +670,8 @@
<value>Selesai membersihkan antrian penemuan Steam #{0}.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>Ada {0}/{1} bot yang sudah memiliki semua permainan yang sedang diperiksa.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -182,14 +182,19 @@
<data name="ErrorUpdateCheckFailed" xml:space="preserve">
<value>Non è stato possibile controllare la versione più recente!</value>
</data>
<data name="ErrorUpdateNoAssetForThisVersion" xml:space="preserve">
<value>Non posso procedere con l'aggiornamento perché non c'è una risorsa legata alla versione in esecuzione al momento! Un aggiornamento automatico a quella versione non è possibile.</value>
</data>
<data name="ErrorUpdateNoAssets" xml:space="preserve">
<value>Impossibile procedere con un aggiornamento poiché tale versione non include risorse!</value>
</data>
<data name="ErrorUserInputRunningInHeadlessMode" xml:space="preserve">
<value>Ricevuta una richiesta di input da parte dell'utente, ma il processo è in esecuzione in modalità headless!</value>
</data>
<data name="ErrorIPCAccessDenied" xml:space="preserve">
<value>Rifiutando di gestire la richiesta poiché SteamOwnerID non è stato impostato correttamente!</value>
<comment>SteamOwnerID is name of bot config property, it should not be translated</comment>
</data>
<data name="Exiting" xml:space="preserve">
<value>Uscita in corso...</value>
</data>
@@ -239,7 +244,10 @@
<data name="UpdateCheckingNewVersion" xml:space="preserve">
<value>Verifica della nuova versione...</value>
</data>
<data name="UpdateDownloadingNewVersion" xml:space="preserve">
<value>Scaricando la nuova versione: {0} ({1} MB)... Durante l'attesa, considera una donazione se apprezzi il lavoro svolto! :)</value>
<comment>{0} will be replaced by version string, {1} will be replaced by update size (in megabytes)</comment>
</data>
<data name="UpdateFinished" xml:space="preserve">
<value>Aggiornamento completato!</value>
</data>
@@ -267,7 +275,7 @@
<comment>Please note that this translation should end with space</comment>
</data>
<data name="UserInputSteamParentalPIN" xml:space="preserve">
<value>Si prega di inserire il PIN famigliare Steam: </value>
<value>Si prega di inserire il PIN famigliare di Steam: </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="UserInputSteamPassword" xml:space="preserve">
@@ -278,7 +286,10 @@
<value>Inserisci il valore non documentato {0}: </value>
<comment>{0} will be replaced by property name. Please note that this translation should end with space</comment>
</data>
<data name="UserInputIPCHost" xml:space="preserve">
<value>Inserisci il tuo host IPC: </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="WarningUnknownValuePleaseReport" xml:space="preserve">
<value>Ricevuto valore sconosciuto per {0}, si prega di segnalare: {1}</value>
<comment>{0} will be replaced by object's name, {1} will be replaced by value for that object</comment>
@@ -287,10 +298,20 @@
<value>Non è possibile giocare a più di {0} giochi contemporaneamente, verranno utilizzate solo le prime {0} voci di {1}!</value>
<comment>{0} will be replaced by max number of games, {1} will be replaced by name of the configuration property</comment>
</data>
<data name="ErrorIPCAddressAccessDeniedException" xml:space="preserve">
<value>Il servizio IPC non può essere avviato a causa di AddressAccessDeniedException! Se desideri utilizzare il servizio IPC fornito da ASF, considera la possibilità di avviare ASF come amministratore, o di dargli le autorizzazioni necessarie!</value>
</data>
<data name="IPCAnswered" xml:space="preserve">
<value>Risposto al comando IPC: {0} con: {1}</value>
<comment>{0} will be replaced by IPC command, {1} will be replaced by IPC answer</comment>
</data>
<data name="IPCReady" xml:space="preserve">
<value>Il server IPC è pronto!</value>
</data>
<data name="IPCStarting" xml:space="preserve">
<value>Avvio del server IPC in {0}...</value>
<comment>{0} will be replaced by IPC hostname</comment>
</data>
<data name="BotAlreadyStopped" xml:space="preserve">
<value>Questo bot è già stato arrestato!</value>
</data>
@@ -433,7 +454,7 @@
<comment>{0} will be replaced by translated TimeSpan string (such as "5 minutes")</comment>
</data>
<data name="BotAutomaticIdlingResumedAlready" xml:space="preserve">
<value>L'idling automatico è già attivo!</value>
<value>L'idling automatico è già stato ripreso!</value>
</data>
<data name="BotConnected" xml:space="preserve">
<value>Connesso a Steam!</value>
@@ -525,7 +546,7 @@
<value>Rimossa la chiave di accesso scaduta!</value>
</data>
<data name="BotStatusNotIdling" xml:space="preserve">
<value>Il Bot non sta trovando niente.</value>
<value>Il Bot non sta trovando niente su cui fare idling.</value>
</data>
<data name="BotStatusLimited" xml:space="preserve">
<value>Il Bot è un account limitato e non può ottenere carte tramite idling.</value>
@@ -534,7 +555,7 @@
<value>Il bot si stà connettendo alla rete di Steam.</value>
</data>
<data name="BotStatusNotRunning" xml:space="preserve">
<value>Bot non è in esecuzione.</value>
<value>Il bot non è in esecuzione.</value>
</data>
<data name="BotStatusPaused" xml:space="preserve">
<value>Il bot è in pausa o in esecuzione in modalità manuale.</value>
@@ -608,7 +629,7 @@
<comment>{0} will be replaced by culture code, such as "en-US", {1} will be replaced by completeness percentage, such as "78.5%"</comment>
</data>
<data name="IdlingGameNotPossible" xml:space="preserve">
<value>L'idling di {0} ({1}) è temporaneamente disabilitato, poiché ASF non è in grado di riprodurre questo gioco al momento.</value>
<value>L'idling di {0} ({1}) è temporaneamente disabilitato, poiché ASF non è in grado di eseguire questo gioco al momento.</value>
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
</data>
<data name="WarningIdlingGameMismatch" xml:space="preserve">
@@ -650,5 +671,8 @@
<value>Fine coda #{0} dell'elenco scoperte Steam.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>Ci sono {0}/{1} bots che posseggono già tutti i giochi che sono stati controllati.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -188,7 +188,10 @@
<data name="ErrorUserInputRunningInHeadlessMode" xml:space="preserve">
<value>ユーザー入力のリクエストを受け取りましたが、プロセスはheadlessモードで実行されています</value>
</data>
<data name="ErrorIPCAccessDenied" xml:space="preserve">
<value>SteamOwnerIDが設定されていないため、要求を処理しませんでした</value>
<comment>SteamOwnerID is name of bot config property, it should not be translated</comment>
</data>
<data name="Exiting" xml:space="preserve">
<value>終了中...</value>
</data>
@@ -238,7 +241,10 @@
<data name="UpdateCheckingNewVersion" xml:space="preserve">
<value>新しいバージョンをチェックしています...</value>
</data>
<data name="UpdateDownloadingNewVersion" xml:space="preserve">
<value>新しいバージョンをダウンロードしています: {0} ({1} MB) ... 待っている間、作者への寄付をご検討ください! :)</value>
<comment>{0} will be replaced by version string, {1} will be replaced by update size (in megabytes)</comment>
</data>
<data name="UpdateFinished" xml:space="preserve">
<value>アップデート完了!</value>
</data>
@@ -277,7 +283,10 @@
<value>{0} の文章化されていない値を入力してください: </value>
<comment>{0} will be replaced by property name. Please note that this translation should end with space</comment>
</data>
<data name="UserInputIPCHost" xml:space="preserve">
<value>あなたのIPCホストを入力してください: </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="WarningUnknownValuePleaseReport" xml:space="preserve">
<value>{0} に不明な値を受信しました。この問題を報告してください: {1}</value>
<comment>{0} will be replaced by object's name, {1} will be replaced by value for that object</comment>
@@ -286,10 +295,20 @@
<value>同時に{0} つより多くのゲームをプレイすることはできません。{1} から最初の{0} だけが使用されます!</value>
<comment>{0} will be replaced by max number of games, {1} will be replaced by name of the configuration property</comment>
</data>
<data name="ErrorIPCAddressAccessDeniedException" xml:space="preserve">
<value>AddressAccessDeniedExceptionのため、IPC サービスを開始できませんでしたASFによるIPC サービスを使用したい場合、ASFを管理者として起動するか、適切な権限を付与してください</value>
</data>
<data name="IPCAnswered" xml:space="preserve">
<value>IPC コマンド: {0} 返答:{1}</value>
<comment>{0} will be replaced by IPC command, {1} will be replaced by IPC answer</comment>
</data>
<data name="IPCReady" xml:space="preserve">
<value>IPC サーバーの準備ができました!</value>
</data>
<data name="IPCStarting" xml:space="preserve">
<value>IPC サーバーを {0} に開始中...</value>
<comment>{0} will be replaced by IPC hostname</comment>
</data>
<data name="BotAlreadyStopped" xml:space="preserve">
<value>このBotは既に停止しています</value>
</data>

View File

@@ -184,14 +184,19 @@ StackTrace:
<data name="ErrorUpdateCheckFailed" xml:space="preserve">
<value>최신 버전을 확인할 수 없습니다!</value>
</data>
<data name="ErrorUpdateNoAssetForThisVersion" xml:space="preserve">
<value>현재 실행 중인 버전과 관련된 자산이 없으므로 업데이트를 진행할 수 없습니다! 해당 버전으로 자동 업데이트가 불가능합니다.</value>
</data>
<data name="ErrorUpdateNoAssets" xml:space="preserve">
<value>해당 버전이 아무 내용도 포함되어 있지 않아 업데이트를 진행할 수 없습니다!</value>
</data>
<data name="ErrorUserInputRunningInHeadlessMode" xml:space="preserve">
<value>사용자 입력 요청을 받았지만, 프로세스는 Headless 모드로 실행 중입니다.</value>
</data>
<data name="ErrorIPCAccessDenied" xml:space="preserve">
<value>SteamOwnerID가 설정되지 않았기 때문에 요청을 거절합니다!</value>
<comment>SteamOwnerID is name of bot config property, it should not be translated</comment>
</data>
<data name="Exiting" xml:space="preserve">
<value>종료 중...</value>
</data>
@@ -241,7 +246,10 @@ StackTrace:
<data name="UpdateCheckingNewVersion" xml:space="preserve">
<value>새로운 버전 확인 중...</value>
</data>
<data name="UpdateDownloadingNewVersion" xml:space="preserve">
<value>새로운 버전을 내려받는 중: {0} ({1} MB).... 기다리는 동안, 이 프로그램이 고맙다면, 기부하는 것을 고려해보세요! :)</value>
<comment>{0} will be replaced by version string, {1} will be replaced by update size (in megabytes)</comment>
</data>
<data name="UpdateFinished" xml:space="preserve">
<value>업데이트 작업 완료!</value>
</data>
@@ -280,7 +288,10 @@ StackTrace:
<value>등록되지 않은 {0}의 값을 입력하세요: </value>
<comment>{0} will be replaced by property name. Please note that this translation should end with space</comment>
</data>
<data name="UserInputIPCHost" xml:space="preserve">
<value>IPC 호스트를 입력하세요: </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="WarningUnknownValuePleaseReport" xml:space="preserve">
<value>{0}의 알 수 없는 값을 받았습니다. 이것을 보고 바랍니다: {1}</value>
<comment>{0} will be replaced by object's name, {1} will be replaced by value for that object</comment>
@@ -289,10 +300,20 @@ StackTrace:
<value>동시에 {0}개 이상의 게임들을 플레이하는 것은 불가능합니다. {1}에 의해 단지 {0}개의 항목만 사용될 것입니다!</value>
<comment>{0} will be replaced by max number of games, {1} will be replaced by name of the configuration property</comment>
</data>
<data name="ErrorIPCAddressAccessDeniedException" xml:space="preserve">
<value>AddressAccessDeniedException로 인해 IPC 서비스가 시작될 수 없습니다! ASF에서 제공하는 IPC 서비스를 사용하고 싶다면, ASF를 관리자 모드로 실행하거나 적절한 권한을 줘야 합니다!</value>
</data>
<data name="IPCAnswered" xml:space="preserve">
<value>{0} IPC 명령에 대한 응답: {1}</value>
<comment>{0} will be replaced by IPC command, {1} will be replaced by IPC answer</comment>
</data>
<data name="IPCReady" xml:space="preserve">
<value>IPC 서버 준비 완료!</value>
</data>
<data name="IPCStarting" xml:space="preserve">
<value>{0} 에서 IPC 서버 시작...</value>
<comment>{0} will be replaced by IPC hostname</comment>
</data>
<data name="BotAlreadyStopped" xml:space="preserve">
<value>이 봇은 이미 중지되어 있습니다!</value>
</data>
@@ -652,5 +673,8 @@ StackTrace:
<value>스팀 맞춤 대기열 #{0}을 지웠습니다.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>게임을 확인 중인 {0}/{1} 개의 봇이 있습니다.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -670,5 +670,8 @@
<value>Baigta peržiūrėti Steam atradimo eilė #{0}.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>Šiuo metu {0}/{1} botai turi visus tikrinamus žaidimus.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -673,5 +673,8 @@ StackTrace:
<value>Steam-ontdekkingswachtrij voltooid #{0}.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>Er zijn {0}/{1} bots die de gecontroleerde spellen al in bezit hebben.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -673,5 +673,8 @@ StackTrace:
<value>Steam-ontdekkingswachtrij voltooid #{0}.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>Er zijn {0}/{1} bots die de gecontroleerde spellen al in bezit hebben.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -247,7 +247,7 @@ StackTrace:
<value>Verificando se há atualizações...</value>
</data>
<data name="UpdateDownloadingNewVersion" xml:space="preserve">
<value>Baixando uma nova versão: {0} ({1} MB)... Enquanto aguarda, considere doar se você aprecia o nosso trabalho! :)</value>
<value>Baixando nova versão: {0} ({1} MB)... Enquanto espera, considere doar caso aprecie o nosso trabalho! :)</value>
<comment>{0} will be replaced by version string, {1} will be replaced by update size (in megabytes)</comment>
</data>
<data name="UpdateFinished" xml:space="preserve">
@@ -289,7 +289,7 @@ StackTrace:
<comment>{0} will be replaced by property name. Please note that this translation should end with space</comment>
</data>
<data name="UserInputIPCHost" xml:space="preserve">
<value>Por favor insira o seu host IPC: </value>
<value>Por favor, insira o seu host IPC: </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="WarningUnknownValuePleaseReport" xml:space="preserve">
@@ -416,7 +416,7 @@ StackTrace:
<comment>{0} will be replaced by giftID (number)</comment>
</data>
<data name="BotAccountLimited" xml:space="preserve">
<value>Esta conta está limitada, processo de farm indisponível até que a restrição seja removida!</value>
<value>Esta conta é limitada, processo de coleta indisponível até que a restrição seja removida!</value>
</data>
<data name="BotAddLicense" xml:space="preserve">
<value>ID: {0} | Estado: {1}</value>
@@ -446,13 +446,13 @@ StackTrace:
<value>O processo de coleta automático foi pausado!</value>
</data>
<data name="BotAutomaticIdlingNowResumed" xml:space="preserve">
<value>O processo de farm automático foi retomado!</value>
<value>Coleta automática retomada!</value>
</data>
<data name="BotAutomaticIdlingPausedAlready" xml:space="preserve">
<value>A coleta automática de cartas já está pausada!</value>
</data>
<data name="BotAutomaticIdlingPausedWithCountdown" xml:space="preserve">
<value>Idle automático foi pausado! Você tem {0} para começar um jogo.</value>
<value>Coleta automática pausada! Você tem {0} para iniciar um jogo.</value>
<comment>{0} will be replaced by translated TimeSpan string (such as "5 minutes")</comment>
</data>
<data name="BotAutomaticIdlingResumedAlready" xml:space="preserve">
@@ -530,7 +530,7 @@ StackTrace:
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
</data>
<data name="BotRateLimitExceeded" xml:space="preserve">
<value>Limite de taxas excedido; vamos tentar novamente depois do intervalo de {0}...</value>
<value>Limite de tráfego excedido; tentaremos novamente após um intervalo de {0}...</value>
<comment>{0} will be replaced by translated TimeSpan string (such as "25 minutes")</comment>
</data>
<data name="BotReconnecting" xml:space="preserve">
@@ -563,7 +563,7 @@ StackTrace:
<value>Bot está pausado ou funcionando em modo manual.</value>
</data>
<data name="BotStatusPlayingNotAvailable" xml:space="preserve">
<value>Bot está sendo usado no momento.</value>
<value>Bot está sendo usado.</value>
</data>
<data name="BotUnableToLogin" xml:space="preserve">
<value>Não foi possível iniciar a sessão no Steam: {0}/{1}</value>
@@ -582,16 +582,16 @@ StackTrace:
<comment>{0} will be replaced by failure reason (string)</comment>
</data>
<data name="BotConnectionLost" xml:space="preserve">
<value>Conexão com a rede Steam perdida. Reconectando...</value>
<value>A conexão à rede Steam foi perdida. Reconectando...</value>
</data>
<data name="BotAccountFree" xml:space="preserve">
<value>A conta não está mais sendo usada: processo de coleta de cartas retomado!</value>
</data>
<data name="BotAccountOccupied" xml:space="preserve">
<value>A conta está sendo usada no momento, o ASF voltará a farmar quando ela estiver livre...</value>
<value>Conta em uso, o ASF retomará a coleta quando ela estiver disponível...</value>
</data>
<data name="BotAutomaticIdlingPauseTimeout" xml:space="preserve">
<value>A biblioteca compartilhada não foi iniciada no período de tempo especificado, o farm foi retomado!</value>
<value>A biblioteca compartilhada não foi iniciada no período de tempo especificado. O processo de coleta foi retomado!</value>
</data>
<data name="BotConnecting" xml:space="preserve">
<value>Conectando...</value>
@@ -666,12 +666,15 @@ StackTrace:
<comment>{0} will be replaced by number (in megabytes) of memory being used</comment>
</data>
<data name="ClearingDiscoveryQueue" xml:space="preserve">
<value>Limpando a fila de descoberta do Steam #{0}...</value>
<value>Limpando a lista de descobrimento #{0}...</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="DoneClearingDiscoveryQueue" xml:space="preserve">
<value>Limpeza da lista de descoberta da Steam #{0} concluída.</value>
<value>Limpeza da lista de descobrimento #{0} concluída.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>Há {0} de {1} bots que já possuem todos os jogos sendo verificados.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -674,5 +674,8 @@ inválidas, abortando!</value>
<value>Fila de exploração da Steam feita #{0}.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>Os bots {0}/{1} já possuem todos os jogos que está sendo verificados.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -673,5 +673,8 @@ StackTrace:
<value>S-a terminat ștergerea cozii pentru lista de descoperiri Steam #{0}.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>Există {0}/{1} boți care dețin deja toate jocurile verificate.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -247,7 +247,7 @@
<value>Проверка новой версии...</value>
</data>
<data name="UpdateDownloadingNewVersion" xml:space="preserve">
<value>Скачивание новой версии: {0} ({1} MB)... В ожидании, подумайте о пожертвовании, если вам нравится проделанная работа! :)</value>
<value>Скачивание новой версии: {0} ({1} MB)... А пока, подумайте о пожертвовании, если вам нравится наша работа! ;)</value>
<comment>{0} will be replaced by version string, {1} will be replaced by update size (in megabytes)</comment>
</data>
<data name="UpdateFinished" xml:space="preserve">
@@ -304,7 +304,7 @@
<value>Сервис IPC не может быть запущен, из-за "AddressAccessDeniedException" (исключение: отказ в доступе к адресу)! Если Вы желаете использовать сервис IPC, предоставляемый ASF, то попробуйте запустить ASF от имени администратора, или выдать необходимые права!</value>
</data>
<data name="IPCAnswered" xml:space="preserve">
<value>На команду IPC: {0} был ответ: {1}</value>
<value>На запрос IPC: {0} дан ответ: {1}</value>
<comment>{0} will be replaced by IPC command, {1} will be replaced by IPC answer</comment>
</data>
<data name="IPCReady" xml:space="preserve">
@@ -673,5 +673,8 @@
<value>Очищен список рекомендаций #{0}.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>Имеются {0} / {1} ботов, которые уже владеют всеми проверенными играми.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -152,7 +152,7 @@ StackTrace:
<comment>{0} will be replaced by URL of the request</comment>
</data>
<data name="ErrorGlobalConfigNotLoaded" xml:space="preserve">
<value>Den globala konfigurationen kunde inte laddas. Försäkra dig om att {0} existerar och är valid! Följ installations-guiden på wiki-sidan om du är förvirrad.</value>
<value>Den globala konfigurationen kunde inte laddas. Försäkra dig om att {0} existerar och är giltlig! Följ installationsguiden på wikisidan om du är förvirrad.</value>
<comment>{0} will be replaced by file's path</comment>
</data>
<data name="ErrorIsInvalid" xml:space="preserve">
@@ -184,14 +184,19 @@ StackTrace:
<data name="ErrorUpdateCheckFailed" xml:space="preserve">
<value>Kunde inte kontrollera senaste versionen!</value>
</data>
<data name="ErrorUpdateNoAssetForThisVersion" xml:space="preserve">
<value>Kunde inte fortsätta med uppdateringen eftersom det inte finns någon tillgång som relaterar till den nuvarande versionen! Automatisk uppdatering till denna version är inte möjlig.</value>
</data>
<data name="ErrorUpdateNoAssets" xml:space="preserve">
<value>Kunde inte fortsätta med en uppdatering för den versionen innehåller inte några filer!</value>
</data>
<data name="ErrorUserInputRunningInHeadlessMode" xml:space="preserve">
<value>Mottagit en begäran om användarens input, men processen körs i huvudlöst läge!</value>
</data>
<data name="ErrorIPCAccessDenied" xml:space="preserve">
<value>Vägrar att hantera begäran eftersom SteamOwnerID inte är inställt!</value>
<comment>SteamOwnerID is name of bot config property, it should not be translated</comment>
</data>
<data name="Exiting" xml:space="preserve">
<value>Stänger ner...</value>
</data>
@@ -205,7 +210,7 @@ StackTrace:
<value>Globala konfigurationsfilen har tagits bort!</value>
</data>
<data name="IgnoringTrade" xml:space="preserve">
<value>Avböjer byte: {0}</value>
<value>Ignorerar bytesförfrågan: {0}</value>
<comment>{0} will be replaced by trade number</comment>
</data>
<data name="LoggingIn" xml:space="preserve">
@@ -216,7 +221,7 @@ StackTrace:
<value>Inga bottar körs just nu, stänger ner...</value>
</data>
<data name="RefreshingOurSession" xml:space="preserve">
<value>Startar om våran session!</value>
<value>Startar om sessionen!</value>
</data>
<data name="RejectingTrade" xml:space="preserve">
<value>Avböjer bytesförfrågan: {0}</value>
@@ -236,12 +241,16 @@ StackTrace:
<value>Framgång!</value>
</data>
<data name="UnlockingParentalAccount" xml:space="preserve">
<value>Låser upp föräldrarnas konto...</value>
<value>Låser upp föräldrarkontot...</value>
</data>
<data name="UpdateCheckingNewVersion" xml:space="preserve">
<value>Söker efter senaste versionen...</value>
</data>
<data name="UpdateDownloadingNewVersion" xml:space="preserve">
<value>Laddar ner senaste versionen: {0} ({1} MB)... Medan du väntar, fundera på att donera om du uppskattar arbetet som görs!
:)</value>
<comment>{0} will be replaced by version string, {1} will be replaced by update size (in megabytes)</comment>
</data>
<data name="UpdateFinished" xml:space="preserve">
<value>Uppdateringsprocessen klar!</value>
</data>
@@ -253,7 +262,7 @@ StackTrace:
<comment>{0} will be replaced by current version, {1} will be replaced by remote version</comment>
</data>
<data name="UserInputDeviceID" xml:space="preserve">
<value>Vänligen ange din mobil-autentiserares enhets-ID (inklusive "android:"): </value>
<value>Vänligen ange mobil-autentiserarens enhets-ID (inklusive "android:"): </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="UserInputSteam2FA" xml:space="preserve">
@@ -280,7 +289,10 @@ StackTrace:
<value>Vänligen ange odokumenterade värde av {0}: </value>
<comment>{0} will be replaced by property name. Please note that this translation should end with space</comment>
</data>
<data name="UserInputIPCHost" xml:space="preserve">
<value>Vänligen ange din IPC Host: </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="WarningUnknownValuePleaseReport" xml:space="preserve">
<value>Tog emot okänt värde för {0}, vänligen rapportera detta: {1}</value>
<comment>{0} will be replaced by object's name, {1} will be replaced by value for that object</comment>
@@ -289,10 +301,20 @@ StackTrace:
<value>Går inte att spela mer än {0} spel samtidigt, bara första {0} poster från {1} kommer att användas!</value>
<comment>{0} will be replaced by max number of games, {1} will be replaced by name of the configuration property</comment>
</data>
<data name="ErrorIPCAddressAccessDeniedException" xml:space="preserve">
<value>IPC-tjänsten kunde inte startas pågrund av AddressAccessDeniedException! om du vill använda IPC-tjänsten som tillhandahålls av ASF, överväg att starta ASF som administratör, eller ge den rätt rättigheter!</value>
</data>
<data name="IPCAnswered" xml:space="preserve">
<value>Svarade till IPC kommandot {0} med: {1}</value>
<comment>{0} will be replaced by IPC command, {1} will be replaced by IPC answer</comment>
</data>
<data name="IPCReady" xml:space="preserve">
<value>IPC servern är redo!</value>
</data>
<data name="IPCStarting" xml:space="preserve">
<value>Startar IPC servern på {0}...</value>
<comment>{0} will be replaced by IPC hostname</comment>
</data>
<data name="BotAlreadyStopped" xml:space="preserve">
<value>Bot-instansen har redan stoppats!</value>
</data>
@@ -640,8 +662,20 @@ StackTrace:
<data name="WarningPreReleaseVersion" xml:space="preserve">
<value>Du använder en version som är nyare än den senast släppta versionen i din uppdateringskanal. Vänligen notera att förhandsversioner är avsedda för användare med förmågan att rapportera buggar, handskas med eventuella problem och viljan att ge feedback - Ingen teknisk support kommer att ges.</value>
</data>
<data name="BotStats" xml:space="preserve">
<value>Nuvarande minnesförbrukning: {0} MB.</value>
<comment>{0} will be replaced by number (in megabytes) of memory being used</comment>
</data>
<data name="ClearingDiscoveryQueue" xml:space="preserve">
<value>Genomgår Steam discovery-kön #{0}...</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="DoneClearingDiscoveryQueue" xml:space="preserve">
<value>Genomgått Steam discovery-kön #{0}.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>Det finns {0}/{1} bottar som redan äger samtliga spel som kontrollerats.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -673,5 +673,8 @@ Yığın izleme:
<value>Steam keşif kuyruğu temizlenmesi bitti #{0}.</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>{0}/{1} bot zaten sahip olunan tüm oyunları denetliyor.</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -181,14 +181,19 @@
<data name="ErrorUpdateCheckFailed" xml:space="preserve">
<value>无法检查最新版本 </value>
</data>
<data name="ErrorUpdateNoAssetForThisVersion" xml:space="preserve">
<value>无法继续更新,因为没有与当前正在运行的版本相关的版本! 无法自动更新到该版本。</value>
</data>
<data name="ErrorUpdateNoAssets" xml:space="preserve">
<value>不能进行更新,因为此版本没有任何资源!</value>
</data>
<data name="ErrorUserInputRunningInHeadlessMode" xml:space="preserve">
<value>收到一个用户输入请求,但进程目前正在以无显示模式运行 </value>
</data>
<data name="ErrorIPCAccessDenied" xml:space="preserve">
<value>拒绝处理该请求,因为未设置 SteamOwnerID </value>
<comment>SteamOwnerID is name of bot config property, it should not be translated</comment>
</data>
<data name="Exiting" xml:space="preserve">
<value>正在退出...</value>
</data>
@@ -213,7 +218,7 @@
<value>无账号正在运行,即将退出...</value>
</data>
<data name="RefreshingOurSession" xml:space="preserve">
<value>刷新会话 </value>
<value>正在刷新会话!</value>
</data>
<data name="RejectingTrade" xml:space="preserve">
<value>拒绝交易︰ {0}</value>
@@ -238,7 +243,10 @@
<data name="UpdateCheckingNewVersion" xml:space="preserve">
<value>正在检查新版本...</value>
</data>
<data name="UpdateDownloadingNewVersion" xml:space="preserve">
<value>更新中: {0} ({1} MB)...... 在你等待的时候,可以考虑趁现在进行捐赠!:)</value>
<comment>{0} will be replaced by version string, {1} will be replaced by update size (in megabytes)</comment>
</data>
<data name="UpdateFinished" xml:space="preserve">
<value>更新完毕</value>
</data>
@@ -277,7 +285,10 @@
<value>请输入非正式的值 {0}: </value>
<comment>{0} will be replaced by property name. Please note that this translation should end with space</comment>
</data>
<data name="UserInputIPCHost" xml:space="preserve">
<value>请输入你的IPC主机 </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="WarningUnknownValuePleaseReport" xml:space="preserve">
<value>收到的{0} 为未知值,请报告此值:{1}</value>
<comment>{0} will be replaced by object's name, {1} will be replaced by value for that object</comment>
@@ -286,10 +297,20 @@
<value>目前无法同时挂 {0} 个以上的游戏,只有 {1} 里面的前 {0} 个游戏可用!</value>
<comment>{0} will be replaced by max number of games, {1} will be replaced by name of the configuration property</comment>
</data>
<data name="ErrorIPCAddressAccessDeniedException" xml:space="preserve">
<value>由于目标地址访问受拒绝,无法启动 IPC 服务 如果你想要使用ASF提供的 IPC 服务,请用管理员身份运行,或者给予更高的权限。</value>
</data>
<data name="IPCAnswered" xml:space="preserve">
<value>IPC 命令响应︰ {0} 及 {1}</value>
<comment>{0} will be replaced by IPC command, {1} will be replaced by IPC answer</comment>
</data>
<data name="IPCReady" xml:space="preserve">
<value>IPC 服务已就绪 </value>
</data>
<data name="IPCStarting" xml:space="preserve">
<value>在 {0} 上的启动 IPC 服务...</value>
<comment>{0} will be replaced by IPC hostname</comment>
</data>
<data name="BotAlreadyStopped" xml:space="preserve">
<value>这个帐号已停止运行!</value>
</data>
@@ -320,10 +341,10 @@
<comment>{0} will be replaced by the name of chosen idling algorithm</comment>
</data>
<data name="Done" xml:space="preserve">
<value>完成</value>
<value>完成</value>
</data>
<data name="GamesToIdle" xml:space="preserve">
<value>共有{0} 个游戏(共计{1} 张卡) 等待挂卡(~还剩{2})...</value>
<value>共有 {0} 个游戏(共计 {1} 张卡) 等待挂卡(~还剩{2})...</value>
<comment>{0} will be replaced by number of games, {1} will be replaced by number of cards, {2} will be replaced by translated TimeSpan string (such as "1 day, 5 hours and 30 minutes")</comment>
</data>
<data name="IdlingFinished" xml:space="preserve">
@@ -342,13 +363,13 @@
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name, {2} will be replaced by number of cards left to idle</comment>
</data>
<data name="IdlingStopped" xml:space="preserve">
<value>挂卡已停止</value>
<value>已停止挂卡!</value>
</data>
<data name="IgnoredStickyPauseEnabled" xml:space="preserve">
<value>请求已忽略,因为强制暂停已开启</value>
</data>
<data name="NothingToIdle" xml:space="preserve">
<value>该账户已经无卡可挂</value>
<value>该账户已经无卡可挂</value>
</data>
<data name="NowIdling" xml:space="preserve">
<value>正在挂卡︰{0} ({1})</value>
@@ -474,7 +495,7 @@
<value>不能发送报价,因为没有定义主权限的用户!</value>
</data>
<data name="BotLootingNoLootableTypes" xml:space="preserve">
<value>你没有设置任何拾取类型!</value>
<value>你没有设置任何拾取类型</value>
</data>
<data name="BotLootingNowDisabled" xml:space="preserve">
<value>拾取现在已禁用!</value>
@@ -649,5 +670,8 @@
<value>已完成Steam探索队列 #{0}。</value>
<comment>{0} will be replaced by queue number</comment>
</data>
<data name="BotOwnsOverview" xml:space="preserve">
<value>{0}/{1} 个BOT已经拥有所有正在被检查的游戏。</value>
<comment>{0} will be replaced by number of bots that already own games being checked, {1} will be replaced by total number of bots that were checked during the process</comment>
</data>
</root>

View File

@@ -42,14 +42,14 @@ namespace ArchiSteamFarm {
private const byte CodeInterval = 30;
private static readonly char[] CodeCharacters = { '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'T', 'V', 'W', 'X', 'Y' };
private static readonly SemaphoreSlim TimeSemaphore = new SemaphoreSlim(1);
private static readonly SemaphoreSlim TimeSemaphore = new SemaphoreSlim(1, 1);
private static int? SteamTimeDifference;
// "ERROR" is being used by SteamDesktopAuthenticator
internal bool HasCorrectDeviceID => !string.IsNullOrEmpty(DeviceID) && !DeviceID.Equals("ERROR");
private readonly SemaphoreSlim ConfirmationsSemaphore = new SemaphoreSlim(1);
private readonly SemaphoreSlim ConfirmationsSemaphore = new SemaphoreSlim(1, 1);
#pragma warning disable 649
[JsonProperty(PropertyName = "identity_secret", Required = Required.Always)]

View File

@@ -23,8 +23,10 @@
*/
using System;
using System.IO;
using System.Runtime.InteropServices;
using ArchiSteamFarm.Localization;
using Mono.Unix;
namespace ArchiSteamFarm {
internal static class OS {
@@ -44,6 +46,16 @@ namespace ArchiSteamFarm {
}
}
internal static void UnixSetFileAccessExecutable(string path) {
if (!File.Exists(path) || !UnixFileSystemInfo.TryGetFileSystemEntry(path, out UnixFileSystemInfo entry)) {
return;
}
if (!entry.FileAccessPermissions.HasFlag(FileAccessPermissions.UserExecute)) {
entry.FileAccessPermissions = entry.FileAccessPermissions | FileAccessPermissions.UserExecute;
}
}
private static void DisableQuickEditMode() {
// http://stackoverflow.com/questions/30418886/how-and-why-does-quickedit-mode-in-command-prompt-freeze-applications
IntPtr consoleHandle = NativeMethods.GetStdHandle(NativeMethods.StandardInputHandle);

View File

@@ -31,8 +31,6 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Resources;
using System.Runtime;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Localization;
using NLog;
@@ -53,7 +51,11 @@ namespace ArchiSteamFarm {
internal static WebBrowser WebBrowser { get; private set; }
private static readonly object ConsoleLock = new object();
private static readonly ManualResetEventSlim ShutdownResetEvent = new ManualResetEventSlim(false);
// We need to keep this one assigned and not calculated on-demand
private static readonly string ProcessFileName = Process.GetCurrentProcess().MainModule.FileName;
private static readonly TaskCompletionSource<bool> ShutdownResetEvent = new TaskCompletionSource<bool>();
private static bool ShutdownSequenceInitialized;
@@ -82,33 +84,39 @@ namespace ArchiSteamFarm {
switch (userInputType) {
case ASF.EUserInputType.DeviceID:
Console.Write(Bot.FormatBotResponse(Strings.UserInputDeviceID, botName));
result = Console.ReadLine();
break;
case ASF.EUserInputType.IPCHostname:
Console.Write(Bot.FormatBotResponse(Strings.UserInputIPCHost, botName));
result = Console.ReadLine();
break;
case ASF.EUserInputType.Login:
Console.Write(Bot.FormatBotResponse(Strings.UserInputSteamLogin, botName));
result = Console.ReadLine();
break;
case ASF.EUserInputType.Password:
Console.Write(Bot.FormatBotResponse(Strings.UserInputSteamPassword, botName));
result = Utilities.ReadLineMasked();
break;
case ASF.EUserInputType.SteamGuard:
Console.Write(Bot.FormatBotResponse(Strings.UserInputSteamGuard, botName));
result = Console.ReadLine();
break;
case ASF.EUserInputType.SteamParentalPIN:
Console.Write(Bot.FormatBotResponse(Strings.UserInputSteamParentalPIN, botName));
result = Utilities.ReadLineMasked();
break;
case ASF.EUserInputType.TwoFactorAuthentication:
Console.Write(Bot.FormatBotResponse(Strings.UserInputSteam2FA, botName));
result = Console.ReadLine();
break;
default:
ASF.ArchiLogger.LogGenericWarning(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(userInputType), userInputType));
ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(userInputType), userInputType));
Console.Write(Bot.FormatBotResponse(string.Format(Strings.UserInputUnknown, userInputType), botName));
result = Console.ReadLine();
break;
}
result = Console.ReadLine();
if (!Console.IsOutputRedirected) {
Console.Clear(); // For security purposes
}
@@ -124,13 +132,15 @@ namespace ArchiSteamFarm {
return;
}
string executable = Process.GetCurrentProcess().MainModule.FileName;
string executableName = Path.GetFileNameWithoutExtension(executable);
// New process might want to start IPC before we in fact close
// Ensure that IPC is stopped before Process.Start()
IPC.Stop();
string executableName = Path.GetFileNameWithoutExtension(ProcessFileName);
IEnumerable<string> arguments = Environment.GetCommandLineArgs().Skip(executableName.Equals(SharedInfo.AssemblyName) ? 1 : 0);
try {
Process.Start(executable, string.Join(" ", arguments));
Process.Start(ProcessFileName, string.Join(" ", arguments));
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@@ -138,7 +148,7 @@ namespace ArchiSteamFarm {
// Give new process some time to take over the window (if needed)
await Task.Delay(2000).ConfigureAwait(false);
ShutdownResetEvent.Set();
ShutdownResetEvent.TrySetResult(true);
Environment.Exit(0);
}
@@ -155,7 +165,7 @@ namespace ArchiSteamFarm {
}
private static async Task InitASF(string[] args) {
ASF.ArchiLogger.LogGenericInfo("ASF V" + SharedInfo.Version);
ASF.ArchiLogger.LogGenericInfo("ASF V" + SharedInfo.Version + " (" + SharedInfo.ModuleVersion + ")");
await InitGlobalConfigAndLanguage().ConfigureAwait(false);
await InitGlobalDatabaseAndServices().ConfigureAwait(false);
@@ -231,8 +241,8 @@ namespace ArchiSteamFarm {
return;
}
if (GCSettings.IsServerGC) {
Hacks.Init();
if (GlobalConfig.BackgroundGCPeriod > 0) {
Hacks.EnableBackgroundGC(GlobalConfig.BackgroundGCPeriod);
}
if (!string.IsNullOrEmpty(GlobalConfig.CurrentCulture)) {
@@ -292,7 +302,7 @@ namespace ArchiSteamFarm {
if (!File.Exists(globalDatabaseFile)) {
ASF.ArchiLogger.LogGenericInfo(Strings.Welcome);
ASF.ArchiLogger.LogGenericWarning(Strings.WarningPrivacyPolicy);
await Task.Delay(15 * 1000).ConfigureAwait(false);
await Task.Delay(10 * 1000).ConfigureAwait(false);
}
GlobalDatabase = GlobalDatabase.Load(globalDatabaseFile);
@@ -318,36 +328,35 @@ namespace ArchiSteamFarm {
ShutdownSequenceInitialized = true;
if (Bot.Bots.Count == 0) {
return true;
}
if (Bot.Bots.Count > 0) {
IEnumerable<Task> tasks = Bot.Bots.Values.Select(bot => Task.Run(() => bot.Stop(false)));
IEnumerable<Task> tasks = Bot.Bots.Values.Select(bot => Task.Run(() => bot.Stop(false)));
switch (GlobalConfig.OptimizationMode) {
case GlobalConfig.EOptimizationMode.MinMemoryUsage:
foreach (Task task in tasks) {
await Task.WhenAny(task, Task.Delay(WebBrowser.MaxTries * 1000)).ConfigureAwait(false);
}
switch (GlobalConfig.OptimizationMode) {
case GlobalConfig.EOptimizationMode.MinMemoryUsage:
foreach (Task task in tasks) {
await Task.WhenAny(task, Task.Delay(WebBrowser.MaxTries * 1000)).ConfigureAwait(false);
}
break;
default:
await Task.WhenAny(Task.WhenAll(tasks), Task.Delay(Bot.Bots.Count * WebBrowser.MaxTries * 1000)).ConfigureAwait(false);
break;
}
break;
default:
await Task.WhenAny(Task.WhenAll(tasks), Task.Delay(Bot.Bots.Count * WebBrowser.MaxTries * 1000)).ConfigureAwait(false);
break;
// Extra second for Steam requests to go through
await Task.Delay(1000).ConfigureAwait(false);
}
LogManager.Flush();
return true;
}
private static void Main(string[] args) {
Init(args).Wait();
private static async Task Main(string[] args) {
// Initialize
await Init(args).ConfigureAwait(false);
// Wait for signal to shutdown
ShutdownResetEvent.Wait();
// We got a signal to shutdown
Exit().Wait();
// Wait for shutdown event
await ShutdownResetEvent.Task.ConfigureAwait(false);
}
private static void OnProcessExit(object sender, EventArgs e) => IPC.Stop();
@@ -427,7 +436,7 @@ namespace ArchiSteamFarm {
return;
}
ShutdownResetEvent.Set();
ShutdownResetEvent.TrySetResult(true);
}
}
}

View File

@@ -38,16 +38,8 @@ namespace ArchiSteamFarm {
internal readonly ProtocolTypes ProtocolTypes;
internal ServerRecordEndPoint(string host, ushort port, ProtocolTypes protocolTypes) {
if (string.IsNullOrEmpty(host)) {
throw new ArgumentNullException(nameof(host));
}
if (port == 0) {
throw new ArgumentNullException(nameof(port));
}
if (protocolTypes == 0) {
throw new ArgumentNullException(nameof(protocolTypes));
if (string.IsNullOrEmpty(host) || (port == 0) || (protocolTypes == 0)) {
throw new ArgumentNullException(nameof(host) + " || " + nameof(port) + " || " + nameof(protocolTypes));
}
Host = host;

View File

@@ -42,6 +42,7 @@ namespace ArchiSteamFarm {
internal const string UpdateDirectory = "_old";
internal const string VersionFile = AssemblyName + ".version";
internal static readonly Version Version = Assembly.GetEntryAssembly().GetName().Version;
internal static Guid ModuleVersion => Assembly.GetEntryAssembly().ManifestModule.ModuleVersionId;
internal static Version Version => Assembly.GetEntryAssembly().GetName().Version;
}
}

View File

@@ -40,7 +40,7 @@ namespace ArchiSteamFarm {
private const string URL = "https://" + SharedInfo.StatisticsServer;
private readonly Bot Bot;
private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1);
private readonly SemaphoreSlim RequestsSemaphore = new SemaphoreSlim(1, 1);
private DateTime LastAnnouncementCheck = DateTime.MinValue;
private DateTime LastHeartBeat = DateTime.MinValue;
@@ -49,7 +49,7 @@ namespace ArchiSteamFarm {
internal Statistics(Bot bot) => Bot = bot ?? throw new ArgumentNullException(nameof(bot));
public void Dispose() => Semaphore.Dispose();
public void Dispose() => RequestsSemaphore.Dispose();
internal async Task OnHeartBeat() {
// Request persona update if needed
@@ -62,7 +62,7 @@ namespace ArchiSteamFarm {
return;
}
await Semaphore.WaitAsync().ConfigureAwait(false);
await RequestsSemaphore.WaitAsync().ConfigureAwait(false);
try {
if (!ShouldSendHeartBeats || (DateTime.UtcNow < LastHeartBeat.AddMinutes(MinHeartBeatTTL))) {
@@ -80,7 +80,7 @@ namespace ArchiSteamFarm {
LastHeartBeat = DateTime.UtcNow;
}
} finally {
Semaphore.Release();
RequestsSemaphore.Release();
}
}
@@ -114,7 +114,7 @@ namespace ArchiSteamFarm {
}
}
await Semaphore.WaitAsync().ConfigureAwait(false);
await RequestsSemaphore.WaitAsync().ConfigureAwait(false);
try {
if (DateTime.UtcNow < LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL)) {
@@ -154,7 +154,7 @@ namespace ArchiSteamFarm {
ShouldSendHeartBeats = true;
}
} finally {
Semaphore.Release();
RequestsSemaphore.Release();
}
}
}

View File

@@ -56,6 +56,8 @@ namespace ArchiSteamFarm {
return;
}
base.Write(logEvent);
if (SteamID == 0) {
return;
}

View File

@@ -37,7 +37,7 @@ namespace ArchiSteamFarm {
private readonly Bot Bot;
private readonly ConcurrentHashSet<ulong> IgnoredTrades = new ConcurrentHashSet<ulong>();
private readonly SemaphoreSlim TradesSemaphore = new SemaphoreSlim(1);
private readonly SemaphoreSlim TradesSemaphore = new SemaphoreSlim(1, 1);
private bool ParsingScheduled;
@@ -213,9 +213,7 @@ namespace ArchiSteamFarm {
case ParseTradeResult.EResult.RejectedTemporarily:
if (result.Result == ParseTradeResult.EResult.RejectedPermanently) {
if (Bot.BotConfig.IsBotAccount) {
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.RejectingTrade, tradeOffer.TradeOfferID));
await Bot.ArchiWebHandler.DeclineTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false);
break;
goto case ParseTradeResult.EResult.RejectedAndBlacklisted;
}
IgnoredTrades.Add(tradeOffer.TradeOfferID);
@@ -223,8 +221,12 @@ namespace ArchiSteamFarm {
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.IgnoringTrade, tradeOffer.TradeOfferID));
break;
case ParseTradeResult.EResult.RejectedAndBlacklisted:
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.RejectingTrade, tradeOffer.TradeOfferID));
await Bot.ArchiWebHandler.DeclineTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false);
break;
default:
Bot.ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsInvalid, result.Result));
Bot.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(result.Result), result.Result));
return null;
}
@@ -245,7 +247,7 @@ namespace ArchiSteamFarm {
// Always deny trades from blacklisted steamIDs
if (Bot.IsBlacklistedFromTrades(tradeOffer.OtherSteamID64)) {
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.RejectedPermanently);
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.RejectedAndBlacklisted);
}
}
@@ -301,7 +303,7 @@ namespace ArchiSteamFarm {
// If user has a trade hold, we add extra logic
if (holdDuration.Value > 0) {
// If trade hold duration exceeds our max, or user asks for cards with short lifespan, reject the trade
if ((holdDuration.Value > Program.GlobalConfig.MaxTradeHoldDuration) || tradeOffer.ItemsToGive.Any(item => GlobalConfig.GlobalBlacklist.Contains(item.RealAppID))) {
if ((holdDuration.Value > Program.GlobalConfig.MaxTradeHoldDuration) || tradeOffer.ItemsToGive.Any(item => GlobalConfig.SalesBlacklist.Contains(item.RealAppID))) {
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.RejectedPermanently);
}
}
@@ -353,7 +355,8 @@ namespace ArchiSteamFarm {
AcceptedWithItemLose,
AcceptedWithoutItemLose,
RejectedTemporarily,
RejectedPermanently
RejectedPermanently,
RejectedAndBlacklisted
}
}
}

View File

@@ -29,6 +29,7 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Humanizer;
@@ -85,6 +86,31 @@ namespace ArchiSteamFarm {
}
}
internal static string ReadLineMasked(char mask = '*') {
StringBuilder result = new StringBuilder();
ConsoleKeyInfo keyInfo;
while ((keyInfo = Console.ReadKey(true)).Key != ConsoleKey.Enter) {
if (!char.IsControl(keyInfo.KeyChar)) {
result.Append(keyInfo.KeyChar);
Console.Write(mask);
} else if ((keyInfo.Key == ConsoleKey.Backspace) && (result.Length > 0)) {
result.Remove(result.Length - 1, 1);
if (Console.CursorLeft == 0) {
Console.SetCursorPosition(Console.BufferWidth - 1, Console.CursorTop - 1);
Console.Write(' ');
Console.SetCursorPosition(Console.BufferWidth - 1, Console.CursorTop - 1);
} else {
Console.Write("\b \b");
}
}
}
Console.WriteLine();
return result.ToString();
}
internal static void StartBackgroundAction(Action action, bool longRunning = true) {
if (action == null) {
ASF.ArchiLogger.LogNullError(nameof(action));

View File

@@ -34,7 +34,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace ArchiSteamFarm {
internal sealed class WebBrowser {
internal sealed class WebBrowser : IDisposable {
internal const byte MaxTries = 5; // Defines maximum number of recommended tries for a single request
private const byte ExtendedTimeoutMultiplier = 10; // Defines multiplier of timeout for WebBrowsers dealing with huge data (ASF update)
@@ -63,6 +63,8 @@ namespace ArchiSteamFarm {
HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd(SharedInfo.AssemblyName + "/" + SharedInfo.Version);
}
public void Dispose() => HttpClient.Dispose();
internal static void Init() {
// Set max connection limit from default of 2 to desired value
ServicePointManager.DefaultConnectionLimit = MaxConnections;
@@ -535,7 +537,17 @@ namespace ArchiSteamFarm {
ushort status = (ushort) responseMessage.StatusCode;
if ((status >= 300) && (status <= 399) && (maxRedirections > 0)) {
redirectUri = responseMessage.Headers.Location;
if (!redirectUri.IsAbsoluteUri) {
if (redirectUri.IsAbsoluteUri) {
switch (redirectUri.Scheme) {
case "http":
case "https":
break;
default:
// Invalid ones such as "steammobile"
return null;
}
} else {
redirectUri = new Uri(requestUri.GetLeftPart(UriPartial.Authority) + redirectUri);
}
} else {

View File

@@ -1,6 +1,7 @@
{
"AutoRestart": true,
"AutoUpdates": true,
"BackgroundGCPeriod": 0,
"Blacklist": [],
"ConnectionTimeout": 60,
"CurrentCulture": null,
@@ -8,7 +9,7 @@
"FarmingDelay": 15,
"GiftsLimiterDelay": 1,
"Headless": false,
"IdleFarmingPeriod": 3,
"IdleFarmingPeriod": 8,
"InventoryLimiterDelay": 3,
"IPCHost": "127.0.0.1",
"IPCPort": 1242,
@@ -18,6 +19,6 @@
"OptimizationMode": 0,
"Statistics": true,
"SteamOwnerID": 0,
"SteamProtocols": 7,
"SteamProtocols": 4,
"UpdateChannel": 1
}

File diff suppressed because one or more lines are too long

View File

@@ -10,39 +10,79 @@ configuration: Release
platform: Any CPU
clone_depth: 10
environment:
DOTNET_CHANNEL: release/2.0.0
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
RUNTIMES: generic win-x64 linux-x64 linux-arm osx-x64
matrix:
fast_finish: true
install:
- ps: >-
$ErrorActionPreference = 'Stop'
dotnet --info
$env:DOTNET_INSTALL_DIR = "$pwd\.dotnetcli"
New-Item '.\scripts\obtain' -ItemType 'directory' -Force
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1' -OutFile '.\scripts\obtain\dotnet-install.ps1'
.\scripts\obtain\dotnet-install.ps1 -Channel "$env:DOTNET_CHANNEL" -InstallDir "$env:DOTNET_INSTALL_DIR" -NoPath
$env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path"
before_build:
- ps: dotnet restore
build:
project: ArchiSteamFarm.sln
parallel: true
verbosity: minimal
- ps: >-
$ErrorActionPreference = 'Stop'
dotnet --info
dotnet restore
build_script:
- ps: >-
$ErrorActionPreference = 'Stop'
dotnet build -c "$env:CONFIGURATION" -o 'out\source' --no-restore /nologo
test_script:
- ps: >-
$ErrorActionPreference = 'Stop'
dotnet test ArchiSteamFarm.Tests -c "$env:CONFIGURATION" -o 'out\source' --no-build --no-restore
after_test:
- ps: >-
$ErrorActionPreference = 'Stop'
$PublishBlock = {
param($RUNTIME)
$RUNTIMES = 'generic', 'win-x64', 'linux-x64', 'linux-arm', 'osx-x64'
$ErrorActionPreference = 'Stop'
Set-Location -Path "$env:APPVEYOR_BUILD_FOLDER"
foreach ($RUNTIME in $RUNTIMES) {
if ($RUNTIME -eq 'generic') {
dotnet publish -c "$env:CONFIGURATION" -o "out\$RUNTIME"
} else {
dotnet publish -c "$env:CONFIGURATION" -r "$RUNTIME" -o "out\$RUNTIME"
}
if ($RUNTIME -eq 'generic') {
dotnet publish ArchiSteamFarm -c "$env:CONFIGURATION" -o "out\$RUNTIME" --no-restore /nologo
} else {
dotnet publish ArchiSteamFarm -c "$env:CONFIGURATION" -o "out\$RUNTIME" -r "$RUNTIME" --no-restore /nologo
}
Set-Content -Path "ArchiSteamFarm\out\$RUNTIME\ArchiSteamFarm.version" -Value "$RUNTIME"
Set-Content -Path "ArchiSteamFarm\out\$RUNTIME\ArchiSteamFarm.version" -Value "$RUNTIME"
7z a -bd -tzip -mm=Deflate64 -mx=5 "ArchiSteamFarm\out\ASF-$RUNTIME.zip" "$env:APPVEYOR_BUILD_FOLDER\ArchiSteamFarm\out\$RUNTIME\*"
Push-AppveyorArtifact "ArchiSteamFarm\out\ASF-$RUNTIME.zip" -FileName "ASF-$RUNTIME.zip" -DeploymentName "ASF-$RUNTIME.zip"
7z a -bd -tzip -mm=Deflate64 -mx=7 "ArchiSteamFarm\out\ASF-$RUNTIME.zip" "$env:APPVEYOR_BUILD_FOLDER\ArchiSteamFarm\out\$RUNTIME\*"
Push-AppveyorArtifact "ArchiSteamFarm\out\ASF-$RUNTIME.zip" -FileName "ASF-$RUNTIME.zip" -DeploymentName "ASF-$RUNTIME.zip"
}
foreach ($RUNTIME in $env:RUNTIMES.Split([char[]] $null, [System.StringSplitOptions]::RemoveEmptyEntries)) {
Start-Job -Name "$RUNTIME" -ScriptBlock $PublishBlock -ArgumentList "$RUNTIME"
}
Get-Job | Receive-Job -AutoRemoveJob -Wait
deploy:
- provider: GitHub
tag: $(appveyor_repo_tag_name)
release: ArchiSteamFarm V$(appveyor_repo_tag_name)
description: '**NOTICE:** Pre-releases are experimental versions that often contain unpatched bugs, work-in-progress features or rewritten implementations. If you don''t consider yourself advanced user, please download **[latest stable release](https://github.com/JustArchi/ArchiSteamFarm/releases/latest)** instead. Pre-release versions are dedicated to users who know how to report bugs, deal with issues and give feedback - no technical support will be given. Check out ASF **[release cycle](https://github.com/JustArchi/ArchiSteamFarm/wiki/Release-cycle)** if you''d like to learn more.\n\n---\n\nThis is automated AppVeyor GitHub deployment, human-readable changelog should be available soon. In the meantime please refer to **[GitHub commits](https://github.com/JustArchi/ArchiSteamFarm/commits/$(appveyor_repo_tag_name))**.\n\n---\n\nASF is available for free. If you''re grateful for what we''re doing, please consider donating. Developing ASF requires massive amount of time and knowledge, especially when it comes to Steam (and its problems). Even 1$ is highly appreciated and shows that you care!\n\n [![Patreon support](https://img.shields.io/badge/Patreon-support-yellow.svg)](https://www.patreon.com/JustArchi) [![Paypal.me donate](https://img.shields.io/badge/Paypal.me-donate-yellow.svg)](https://www.paypal.me/JustArchi/1usd) [![Paypal donate](https://img.shields.io/badge/Paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HD2P2P3WGS5Y4) [![Bitcoin donate](https://img.shields.io/badge/Bitcoin-donate-yellow.svg)](https://blockchain.info/payment_request?address=1Archi6M1r5b41Rvn1SY2FfJAzsrEUT7aT) [![Steam donate](https://img.shields.io/badge/Steam-donate-yellow.svg)](https://steamcommunity.com/tradeoffer/new/?partner=46697991&token=0ix2Ruv_)'
description: '### **NOTICE:** Pre-releases are experimental versions that often contain unpatched bugs, work-in-progress features or rewritten implementations. If you don''t consider yourself advanced user, please download **[latest stable release](https://github.com/JustArchi/ArchiSteamFarm/releases/latest)** instead. Pre-release versions are dedicated to users who know how to report bugs, deal with issues and give feedback - no technical support will be given. Check out ASF **[release cycle](https://github.com/JustArchi/ArchiSteamFarm/wiki/Release-cycle)** if you''d like to learn more.\n\n---\n\nThis is automated AppVeyor GitHub deployment, human-readable changelog should be available soon. In the meantime please refer to **[GitHub commits](https://github.com/JustArchi/ArchiSteamFarm/commits/$(appveyor_repo_tag_name))**.\n\n---\n\nASF is available for free. If you''re grateful for what we''re doing, please consider donating. Developing ASF requires massive amount of time and knowledge, especially when it comes to Steam (and its problems). Even 1$ is highly appreciated and shows that you care!\n\n [![Patreon support](https://img.shields.io/badge/Patreon-support-yellow.svg)](https://www.patreon.com/JustArchi) [![Paypal.me donate](https://img.shields.io/badge/Paypal.me-donate-yellow.svg)](https://www.paypal.me/JustArchi/1usd) [![Paypal donate](https://img.shields.io/badge/Paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HD2P2P3WGS5Y4) [![Bitcoin donate](https://img.shields.io/badge/Bitcoin-donate-yellow.svg)](https://blockchain.info/payment_request?address=1Archi6M1r5b41Rvn1SY2FfJAzsrEUT7aT) [![Steam donate](https://img.shields.io/badge/Steam-donate-yellow.svg)](https://steamcommunity.com/tradeoffer/new/?partner=46697991&token=0ix2Ruv_)'
auth_token:
secure: QC5gIDMvSpd43EG6qW8d1E3ZHiVU4aR7pbKQonXstjj/JtAABf5S1IbtoY4OsnOR
artifact: /.*/
@@ -62,7 +102,7 @@ notifications:
{
"avatar_url": "https://www.appveyor.com/assets/img/appveyor-logo-256.png",
"username": "AppVeyor",
"content": "[{{projectName}}:{{branch}}] {{commitMessage}} - {{committerName}} ({{commitId}}) | **{{status}}** | {{buildUrl}}"
"content": "[{{projectName}}:{{branch}}] {{commitMessage}} - {{committerName}} ({{commitId}}) | {{buildUrl}}/artifacts | **{{status}}**"
}
on_build_success: true
on_build_failure: true

2
cc.sh
View File

@@ -44,7 +44,7 @@ if [[ "$CLEAN" -eq 1 ]]; then
fi
dotnet restore
dotnet build -c "$BUILD" -o "$OUT" "${MSBUILD_ARGS[@]}"
dotnet build -c "$BUILD" -o "$OUT" --no-restore "${MSBUILD_ARGS[@]}"
echo
echo "Compilation finished successfully! :)"

View File

@@ -36,5 +36,5 @@ This tool is being used by ASF developers for synchronization of strings/transla
- `archi_download.bat` for downloading translations from Crowdin (typically last commit before release).
- `archi_sync.bat` for upload + download (tree sync).
- `archi_sync.bat` for upload + download (tree sync, e.g. when modifying/removing original strings).

View File

@@ -1,4 +1,5 @@
@echo off
pushd %~dp0
cd ..\\..
call crowdin -b master --identity tools\\crowdin-cli\\crowdin_identity.yaml download
pause

View File

@@ -1,4 +1,5 @@
@echo off
pushd %~dp0
cd ..\\..
call crowdin -b master --identity tools\\crowdin-cli\\crowdin_identity.yaml upload sources
call crowdin -b master --identity tools\\crowdin-cli\\crowdin_identity.yaml download

View File

@@ -1,4 +1,5 @@
@echo off
pushd %~dp0
cd ..\\..
call crowdin -b master --identity tools\\crowdin-cli\\crowdin_identity.yaml upload sources
pause

Binary file not shown.

View File

@@ -2,6 +2,7 @@
SETLOCAL
SET TEMPFILE=%TEMP%\tmpfile
pushd %~dp0
setx /M CROWDIN_HOME "%cd%"
setx /M PATH "%PATH%;%cd%"