mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2025-12-16 22:40:30 +00:00
Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f4a6e6e67 | ||
|
|
9718245793 | ||
|
|
9af542597c | ||
|
|
6391a5f79c | ||
|
|
20c0ba28ef | ||
|
|
f01740a3db | ||
|
|
a89c6e630c | ||
|
|
0b37323a5f | ||
|
|
14799c2733 | ||
|
|
031386ffb9 | ||
|
|
b561c3fc17 | ||
|
|
644f95e96c | ||
|
|
864c909760 | ||
|
|
aacf491a0c | ||
|
|
5ce219eb6e | ||
|
|
f1578a5174 | ||
|
|
1d7dbe3791 | ||
|
|
5c121bee75 | ||
|
|
6be0cff505 | ||
|
|
6281e258da | ||
|
|
68110f2038 | ||
|
|
f423f35c62 | ||
|
|
56246d3853 | ||
|
|
f6cbd67206 | ||
|
|
720214016b | ||
|
|
55d7ccd28b | ||
|
|
6d1ea0b20c | ||
|
|
40e8d85359 | ||
|
|
e397b7f7cc | ||
|
|
4090b40ac7 | ||
|
|
05df0ee725 | ||
|
|
eaf56d0221 | ||
|
|
1fc9b96f83 | ||
|
|
462020f842 | ||
|
|
b4e8e24921 | ||
|
|
84129da691 | ||
|
|
141673409f | ||
|
|
2ad10a5946 | ||
|
|
495b7594f9 | ||
|
|
1727492e07 | ||
|
|
82e38d78eb | ||
|
|
55af650da5 | ||
|
|
a062e98f2d | ||
|
|
8b8671c679 | ||
|
|
5e4f6a7926 | ||
|
|
57b2c8f1cf | ||
|
|
02e7f2144f | ||
|
|
c13fd10bd8 | ||
|
|
7fe0b4499b | ||
|
|
94160bdc42 | ||
|
|
c01c0f7cd1 | ||
|
|
65b06ae3b9 | ||
|
|
5d443f3ed2 | ||
|
|
6a6c903d7d | ||
|
|
52f5ef2a39 | ||
|
|
b92a4ea505 | ||
|
|
8ae9db3a34 | ||
|
|
388c72052c | ||
|
|
e965899395 | ||
|
|
5605e04f0a | ||
|
|
953103719c | ||
|
|
ea53fd0a87 | ||
|
|
f6770ea1c9 | ||
|
|
561b2a3566 | ||
|
|
d24e6d0302 | ||
|
|
98491a4562 | ||
|
|
08d7b9deb0 | ||
|
|
b904b1062f | ||
|
|
ad187c0d88 | ||
|
|
6a035f4832 | ||
|
|
021e8d2ad9 | ||
|
|
61609574d0 | ||
|
|
7b7f1518a6 | ||
|
|
6aa5a633e4 | ||
|
|
6f80ee9faa | ||
|
|
99e8df318c | ||
|
|
2ea334c62e | ||
|
|
27d0b7427a | ||
|
|
62c20c331e | ||
|
|
51d59f0f66 | ||
|
|
ab90c9dc68 | ||
|
|
f7c8b871b3 | ||
|
|
678b32f318 | ||
|
|
62221fd6b8 | ||
|
|
f932be1395 | ||
|
|
d8fd1035c3 | ||
|
|
09e8a52811 | ||
|
|
0b1032199b | ||
|
|
3e5dfb3174 | ||
|
|
4ada8595bd | ||
|
|
bc64c43748 | ||
|
|
f4f7935d4c | ||
|
|
aacc0a1720 | ||
|
|
e5ee909b96 | ||
|
|
f3f444d0bd | ||
|
|
43d18b6d49 | ||
|
|
f24be67c8c | ||
|
|
6ce4f2941b |
26
.travis.yml
26
.travis.yml
@@ -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,20 +31,23 @@ 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
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0-preview-20170727-01" />
|
||||
<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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<AssemblyVersion>3.0.0.7</AssemblyVersion>
|
||||
<FileVersion>3.0.0.7</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>
|
||||
|
||||
@@ -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;
|
||||
@@ -86,6 +86,7 @@ namespace ArchiSteamFarm {
|
||||
PublicInventorySemaphore.Dispose();
|
||||
SessionSemaphore.Dispose();
|
||||
TradeTokenSemaphore.Dispose();
|
||||
WebBrowser.Dispose();
|
||||
}
|
||||
|
||||
internal async Task<bool> AcceptTradeOffer(ulong tradeID) {
|
||||
@@ -482,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;
|
||||
@@ -494,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);
|
||||
@@ -1178,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,21 +41,20 @@ 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
|
||||
private const ushort MaxSteamMessageLength = 2048;
|
||||
private const byte MaxTwoFactorCodeFailures = 3;
|
||||
private const byte MinHeartBeatTTL = GlobalConfig.DefaultConnectionTimeout; // Assume client is responsive for at least that amount of seconds
|
||||
private const byte PICSCooldownInMiliseconds = 200; // We might need to tune this further
|
||||
|
||||
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;
|
||||
@@ -74,16 +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 PICSSemaphore = 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;
|
||||
@@ -232,22 +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();
|
||||
LootingSemaphore.Dispose();
|
||||
PICSSemaphore.Dispose();
|
||||
Trading.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) {
|
||||
@@ -317,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) {
|
||||
@@ -343,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);
|
||||
}
|
||||
@@ -358,13 +359,10 @@ namespace ArchiSteamFarm {
|
||||
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 {
|
||||
Task.Run(async () => {
|
||||
await Task.Delay(PICSCooldownInMiliseconds).ConfigureAwait(false);
|
||||
PICSSemaphore.Release();
|
||||
}).Forget();
|
||||
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
|
||||
@@ -444,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);
|
||||
}
|
||||
@@ -454,7 +452,7 @@ namespace ArchiSteamFarm {
|
||||
}
|
||||
|
||||
if (!productInfoResultSet.Complete || productInfoResultSet.Failed) {
|
||||
return (0, DateTime.MinValue);
|
||||
return (0, DateTime.MaxValue);
|
||||
}
|
||||
|
||||
return (appID, DateTime.MinValue);
|
||||
@@ -468,13 +466,10 @@ namespace ArchiSteamFarm {
|
||||
try {
|
||||
productInfoResultSet = await SteamApps.PICSGetProductInfo(Enumerable.Empty<uint>(), packageIDs);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericException(e);
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
return null;
|
||||
} finally {
|
||||
Task.Run(async () => {
|
||||
await Task.Delay(PICSCooldownInMiliseconds).ConfigureAwait(false);
|
||||
PICSSemaphore.Release();
|
||||
}).Forget();
|
||||
PICSSemaphore.Release();
|
||||
}
|
||||
|
||||
Dictionary<uint, HashSet<uint>> result = new Dictionary<uint, HashSet<uint>>();
|
||||
@@ -528,15 +523,13 @@ namespace ArchiSteamFarm {
|
||||
return result;
|
||||
}
|
||||
|
||||
internal void IdleGame(uint gameID) => IdleGames(gameID.ToEnumerable());
|
||||
|
||||
internal void IdleGames(IEnumerable<uint> gameIDs) {
|
||||
internal async Task IdleGames(IEnumerable<uint> gameIDs) {
|
||||
if (gameIDs == null) {
|
||||
ArchiLogger.LogNullError(nameof(gameIDs));
|
||||
return;
|
||||
}
|
||||
|
||||
ArchiHandler.PlayGames(gameIDs, BotConfig.CustomGamePlayedWhileFarming);
|
||||
await ArchiHandler.PlayGames(gameIDs, BotConfig.CustomGamePlayedWhileFarming).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal static async Task InitializeSteamConfiguration(ProtocolTypes protocolTypes, uint cellID, InMemoryServerListProvider serverListProvider) {
|
||||
@@ -609,7 +602,7 @@ namespace ArchiSteamFarm {
|
||||
}
|
||||
|
||||
internal async Task OnFarmingFinished(bool farmedSomething) {
|
||||
OnFarmingStopped();
|
||||
await OnFarmingStopped().ConfigureAwait(false);
|
||||
|
||||
if (farmedSomething || !FirstTradeSent) {
|
||||
FirstTradeSent = true;
|
||||
@@ -630,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) {
|
||||
@@ -674,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;
|
||||
}
|
||||
@@ -810,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":
|
||||
@@ -832,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^":
|
||||
@@ -903,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:
|
||||
@@ -947,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() {
|
||||
@@ -962,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) {
|
||||
@@ -1141,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;
|
||||
@@ -1163,7 +1172,7 @@ namespace ArchiSteamFarm {
|
||||
}
|
||||
}
|
||||
|
||||
private void ImportAuthenticator(string maFilePath) {
|
||||
private async Task ImportAuthenticator(string maFilePath) {
|
||||
if (HasMobileAuthenticator || !File.Exists(maFilePath)) {
|
||||
return;
|
||||
}
|
||||
@@ -1171,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);
|
||||
@@ -1190,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);
|
||||
@@ -1283,7 +1293,7 @@ namespace ArchiSteamFarm {
|
||||
return;
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericError(Strings.BotHeartBeatFailed);
|
||||
ArchiLogger.LogGenericWarning(Strings.BotHeartBeatFailed);
|
||||
Destroy(true);
|
||||
RegisterBot(BotName);
|
||||
}
|
||||
@@ -1433,7 +1443,7 @@ namespace ArchiSteamFarm {
|
||||
try {
|
||||
await SteamFriends.SetPersonaState(EPersonaState.Online);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericException(e);
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1591,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:
|
||||
@@ -1726,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);
|
||||
@@ -1737,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);
|
||||
}
|
||||
|
||||
@@ -1828,15 +1850,15 @@ namespace ArchiSteamFarm {
|
||||
ArchiLogger.LogGenericWarning(Strings.BotAccountLocked);
|
||||
}
|
||||
|
||||
if (callback.CellID != 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1871,6 +1893,7 @@ namespace ArchiSteamFarm {
|
||||
|
||||
Statistics?.OnLoggedOn().Forget();
|
||||
Trading.OnNewTrade().Forget();
|
||||
|
||||
break;
|
||||
case EResult.InvalidPassword:
|
||||
case EResult.NoConnection:
|
||||
@@ -1903,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;
|
||||
@@ -1914,7 +1937,7 @@ namespace ArchiSteamFarm {
|
||||
loginKey = CryptoHelper.Encrypt(BotConfig.PasswordFormat, loginKey);
|
||||
}
|
||||
|
||||
BotDatabase.LoginKey = loginKey;
|
||||
await BotDatabase.SetLoginKey(loginKey).ConfigureAwait(false);
|
||||
SteamUser.AcceptNewLoginKey(callback);
|
||||
}
|
||||
|
||||
@@ -2069,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() {
|
||||
@@ -2210,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;
|
||||
}
|
||||
@@ -2313,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;
|
||||
@@ -2322,7 +2345,7 @@ namespace ArchiSteamFarm {
|
||||
redeemFlags |= ERedeemFlags.ForceKeepMissingGames;
|
||||
break;
|
||||
case "SD":
|
||||
redeemFlags |= ERedeemFlags.SkipDistribution;
|
||||
redeemFlags |= ERedeemFlags.SkipDistributing;
|
||||
break;
|
||||
case "SF":
|
||||
redeemFlags |= ERedeemFlags.SkipForwarding;
|
||||
@@ -2438,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;
|
||||
@@ -2463,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);
|
||||
}
|
||||
|
||||
@@ -2478,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) {
|
||||
@@ -2509,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) {
|
||||
@@ -2529,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;
|
||||
@@ -2554,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);
|
||||
}
|
||||
|
||||
@@ -2591,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);
|
||||
@@ -2678,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;
|
||||
@@ -2703,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);
|
||||
}
|
||||
|
||||
@@ -2718,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) {
|
||||
@@ -2749,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) {
|
||||
@@ -2769,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;
|
||||
@@ -2794,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);
|
||||
}
|
||||
|
||||
@@ -3011,7 +3037,7 @@ namespace ArchiSteamFarm {
|
||||
try {
|
||||
result = await SteamFriends.SetPersonaName(nickname);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericException(e);
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
return FormatBotResponse(Strings.WarningFailed);
|
||||
}
|
||||
|
||||
@@ -3067,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);
|
||||
@@ -3242,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) {
|
||||
@@ -3316,7 +3344,7 @@ namespace ArchiSteamFarm {
|
||||
await CardsFarmer.Pause(false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
ArchiHandler.PlayGames(gameIDs);
|
||||
await ArchiHandler.PlayGames(gameIDs).ConfigureAwait(false);
|
||||
return FormatBotResponse(Strings.Done);
|
||||
}
|
||||
|
||||
@@ -3403,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);
|
||||
@@ -3817,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);
|
||||
}
|
||||
|
||||
@@ -3916,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;
|
||||
@@ -4095,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;
|
||||
}
|
||||
}
|
||||
@@ -4111,7 +4286,7 @@ namespace ArchiSteamFarm {
|
||||
if (!HasMobileAuthenticator) {
|
||||
string maFilePath = BotPath + ".maFile";
|
||||
if (File.Exists(maFilePath)) {
|
||||
ImportAuthenticator(maFilePath);
|
||||
await ImportAuthenticator(maFilePath).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4164,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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.IdleGame(game.PlayableAppID);
|
||||
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.IdleGames(games.Select(game => game.PlayableAppID));
|
||||
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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 n’est 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,15 +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>Os bots {0}/{1} já possuem todos os jogos que estão sendo verificados.</value>
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -218,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>
|
||||
@@ -341,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">
|
||||
@@ -363,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>
|
||||
@@ -495,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>
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ namespace ArchiSteamFarm {
|
||||
return;
|
||||
}
|
||||
|
||||
base.Write(logEvent);
|
||||
|
||||
if (SteamID == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,11 +537,19 @@ 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);
|
||||
}
|
||||
|
||||
ASF.ArchiLogger.LogGenericDebug("Asked for <" + requestUri + ">, got unsafely redirected to <" + responseMessage.Headers.Location + ">, resolved URI to: <" + redirectUri + ">");
|
||||
} else {
|
||||
if (!Debugging.IsDebugBuild) {
|
||||
return null;
|
||||
|
||||
@@ -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
70
appveyor.yml
70
appveyor.yml
@@ -10,34 +10,74 @@ 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)
|
||||
|
||||
2
cc.sh
2
cc.sh
@@ -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! :)"
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@echo off
|
||||
pushd %~dp0
|
||||
cd ..\\..
|
||||
call crowdin -b master --identity tools\\crowdin-cli\\crowdin_identity.yaml download
|
||||
pause
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
@@ -2,6 +2,7 @@
|
||||
SETLOCAL
|
||||
SET TEMPFILE=%TEMP%\tmpfile
|
||||
|
||||
pushd %~dp0
|
||||
setx /M CROWDIN_HOME "%cd%"
|
||||
setx /M PATH "%PATH%;%cd%"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user