Compare commits

..

58 Commits

Author SHA1 Message Date
Łukasz Domeradzki
d087aacbfb Closes #2532 2022-03-07 18:35:41 +01:00
Archi
1c0d2d88ed Address crowdin-cli 3.7.8 breaking change
And ensure it doesn't fail silently again the next time something like that happens

https://github.com/crowdin/crowdin-cli/issues/439
2022-03-07 13:28:32 +01:00
renovate[bot]
6b170c345d Update actions/upload-artifact action to v3 (#2530)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-03-03 20:39:53 +01:00
Archi
bc8a4a50d2 Bump 2022-03-03 14:51:41 +01:00
Archi
e025df3d9b Downgrade ASF-ui due to https://github.com/JustArchiNET/ASF-ui/issues/1556 2022-03-03 13:42:59 +01:00
renovate[bot]
5a97835531 Update actions/download-artifact action to v3 (#2529)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-03-02 22:33:21 +01:00
Renovate Bot
bce0557873 Update wiki commit hash to 98a9726 2022-03-02 17:07:56 +00:00
renovate[bot]
4c7cd204ce Update ASF-ui commit hash to 59d9442 (#2527)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-03-02 15:26:59 +01:00
Renovate Bot
7ba6b230df Update docker/login-action action to v1.14.1 2022-03-01 22:22:02 +00:00
renovate[bot]
6c4fba5173 Update actions/checkout action to v3 (#2526)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-03-01 20:41:47 +01:00
Renovate Bot
5cd6477b69 Update ASF-ui commit hash to 02d5b8d 2022-03-01 15:37:38 +00:00
Renovate Bot
1f5fbb5f92 Update crazy-max/ghaction-import-gpg action to v4.2.0 2022-03-01 10:35:48 +00:00
Renovate Bot
d0521ff9ca Update docker/login-action action to v1.14.0 2022-02-28 10:38:03 +00:00
Renovate Bot
1be15716fc Update ASF-ui commit hash to bdb5a1c 2022-02-26 03:47:28 +00:00
Archi
e00ee2cc55 Misc 2022-02-26 01:26:13 +01:00
Archi
8893fc8e70 Misc 2022-02-26 01:21:37 +01:00
renovate[bot]
86b41f0542 Update actions/setup-dotnet action to v2 (#2523)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-02-25 11:37:22 +01:00
Renovate Bot
a245c091a4 Update ASF-ui commit hash to 4b46137 2022-02-25 00:43:54 +00:00
Archi
abbe0cca22 Bump 2022-02-25 00:35:12 +01:00
Archi
d1c2b103b6 Closes #2522 2022-02-25 00:29:51 +01:00
Renovate Bot
9f1734efb7 Update ASF-ui commit hash to e35e350 2022-02-24 16:15:05 +00:00
renovate[bot]
729c2e889c Update actions/setup-node action to v3 (#2521)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-02-24 15:23:23 +01:00
Archi
a9edc7ad7a Bump 2022-02-24 14:17:11 +01:00
Renovate Bot
08a6486c00 Update actions/setup-dotnet action to v1.9.1 2022-02-24 09:23:50 +00:00
Renovate Bot
ca3bc1becd Update ASF-ui commit hash to 0113980 2022-02-22 11:53:12 +00:00
Renovate Bot
fe5028a399 Update crowdin/github-action action to v1.4.7 2022-02-18 16:53:00 +00:00
Archi
c1d9d04071 Rider cleanup & improvements 2022-02-18 15:40:33 +01:00
Renovate Bot
e5ae2abbf0 Update ASF-ui commit hash to 938820c 2022-02-18 12:29:14 +00:00
Archi
41fa5de5a8 Misc 2022-02-18 12:55:47 +01:00
Archi
697b78aa21 Don't expose SteamLogin in weak password warning
While not strictly a sensitive property, there is no good reason why we should print it in the log instead of a bot name, which is far less sensitive in nature.
2022-02-18 11:16:31 +01:00
Renovate Bot
1a7be0bac8 Update ASF-ui commit hash to 354a986 2022-02-18 02:25:11 +00:00
ArchiBot
9d88972ae0 Automatic translations update 2022-02-18 02:12:06 +00:00
Renovate Bot
aec4130afe Update ASF-ui commit hash to cb7478d 2022-02-17 18:36:45 +00:00
Renovate Bot
64228cd3d9 Update docker/login-action action to v1.13.0 2022-02-17 15:07:39 +00:00
Archi
3568a0e528 Make GetFirstSteamMasterID() public API 2022-02-17 13:50:45 +01:00
Archi
38c2b51f2b Make GetTradeToken() public API 2022-02-17 10:59:49 +01:00
Archi
450f365817 Expose GetProxyAccess() as public API 2022-02-17 10:54:55 +01:00
Renovate Bot
cf3f6aabdf Update ASF-ui commit hash to 898e3d5 2022-02-17 03:54:33 +00:00
Renovate Bot
842fb6e304 Update dependency Microsoft.NET.Test.Sdk to v17.1.0 2022-02-16 15:28:50 +00:00
Renovate Bot
a1169331aa Update ASF-ui commit hash to 6b45078 2022-02-16 03:39:15 +00:00
ArchiBot
2fb7d62e06 Automatic translations update 2022-02-16 02:13:41 +00:00
Renovate Bot
4e57153e91 Update ASF-ui commit hash to 9261c65 2022-02-15 21:39:10 +00:00
Renovate Bot
d2e78b6970 Update ASF-ui commit hash to d3543c3 2022-02-14 20:49:07 +00:00
Renovate Bot
ccef6554fe Update ASF-ui commit hash to 8d45f06 2022-02-13 18:06:17 +00:00
Renovate Bot
97875a87c2 Update ASF-ui commit hash to c42dc16 2022-02-13 03:58:51 +00:00
ArchiBot
2684f99563 Automatic translations update 2022-02-13 02:10:27 +00:00
Renovate Bot
a50318dc8b Update ASF-ui commit hash to 6b5e890 2022-02-12 22:05:07 +00:00
Renovate Bot
eeccc36fe4 Update ASF-ui commit hash to 7071136 2022-02-12 09:47:20 +00:00
Renovate Bot
1ead134578 Update ASF-ui commit hash to aa8a4af 2022-02-12 03:59:25 +00:00
ArchiBot
f03f8ebe70 Automatic translations update 2022-02-12 02:13:03 +00:00
Łukasz Domeradzki
aa8b360e1d Update README.md 2022-02-11 10:54:32 +01:00
Renovate Bot
8c22f9929c Update ASF-ui commit hash to 41e74a9 2022-02-11 03:03:36 +00:00
ArchiBot
3795b2de3a Automatic translations update 2022-02-11 02:10:40 +00:00
Archi
f4650fe570 Misc 2022-02-11 00:07:48 +01:00
Archi
fec57e0fff Preserve CachedCardCountsForGame across ASF runs 2022-02-11 00:05:43 +01:00
Archi
8e47a5906f Optimize SendCompletedSets() 2022-02-10 23:52:49 +01:00
Renovate Bot
f728ddf737 Update wiki commit hash to 27140b9 2022-02-10 19:43:12 +00:00
Archi
173cec5ef7 Bump 2022-02-10 20:14:51 +01:00
33 changed files with 582 additions and 259 deletions

1
.github/crowdin.yml vendored
View File

@@ -1,3 +1,4 @@
"base_path": ".."
"preserve_hierarchy": true
"files": [
{

View File

@@ -19,6 +19,14 @@
"allowedVersions": "<= 3.1",
"matchManagers": [ "nuget" ],
"matchPackageNames": [ "Microsoft.Extensions.Configuration.Json", "Microsoft.Extensions.Logging.Configuration" ]
},
{
// TODO: ASF-ui bug, https://github.com/JustArchiNET/ASF-ui/issues/1556
"matchManagers": ["git-submodules"],
"matchPackageNames": [
"ASF-ui"
],
"enabled": false
}
]
}

View File

@@ -19,12 +19,12 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.0
with:
submodules: recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v1.9.0
uses: actions/setup-dotnet@v2.0.0
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
@@ -38,9 +38,8 @@ jobs:
run: dotnet test ArchiSteamFarm.Tests -c "${{ matrix.configuration }}" -p:ContinuousIntegrationBuild=true -p:UseAppHost=false --nologo
- name: Upload latest strings for translation on Crowdin
continue-on-error: true
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.configuration == 'Release' && startsWith(matrix.os, 'ubuntu-') }}
uses: crowdin/github-action@1.4.6
uses: crowdin/github-action@1.4.7
with:
crowdin_branch_name: main
config: '.github/crowdin.yml'

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.0
with:
submodules: recursive

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.0
with:
submodules: recursive
@@ -23,14 +23,14 @@ jobs:
uses: docker/setup-buildx-action@v1.6.0
- name: Login to ghcr.io
uses: docker/login-action@v1.12.0
uses: docker/login-action@v1.14.1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v1.12.0
uses: docker/login-action@v1.14.1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.0
with:
submodules: recursive
@@ -24,14 +24,14 @@ jobs:
uses: docker/setup-buildx-action@v1.6.0
- name: Login to ghcr.io
uses: docker/login-action@v1.12.0
uses: docker/login-action@v1.14.1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v1.12.0
uses: docker/login-action@v1.14.1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.0
with:
submodules: recursive
@@ -24,14 +24,14 @@ jobs:
uses: docker/setup-buildx-action@v1.6.0
- name: Login to ghcr.io
uses: docker/login-action@v1.12.0
uses: docker/login-action@v1.14.1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v1.12.0
uses: docker/login-action@v1.14.1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@@ -19,18 +19,18 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
os: [macos-latest, ubuntu-latest, windows-2019]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.0
with:
submodules: recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v1.9.0
uses: actions/setup-dotnet@v2.0.0
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
@@ -38,7 +38,7 @@ jobs:
run: dotnet --info
- name: Setup Node.js with npm
uses: actions/setup-node@v2.5.1
uses: actions/setup-node@v3.0.0
with:
check-latest: true
node-version: ${{ env.NODE_JS_VERSION }}
@@ -340,7 +340,7 @@ jobs:
- name: Upload ASF-generic
continue-on-error: true
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3.0.0
with:
name: ${{ matrix.os }}_ASF-generic
path: out/ASF-generic.zip
@@ -348,49 +348,49 @@ jobs:
- name: Upload ASF-generic-netf
continue-on-error: true
if: startsWith(matrix.os, 'windows-')
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3.0.0
with:
name: ${{ matrix.os }}_ASF-generic-netf
path: out/ASF-generic-netf.zip
- name: Upload ASF-linux-arm
continue-on-error: true
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3.0.0
with:
name: ${{ matrix.os }}_ASF-linux-arm
path: out/ASF-linux-arm.zip
- name: Upload ASF-linux-arm64
continue-on-error: true
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3.0.0
with:
name: ${{ matrix.os }}_ASF-linux-arm64
path: out/ASF-linux-arm64.zip
- name: Upload ASF-linux-x64
continue-on-error: true
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3.0.0
with:
name: ${{ matrix.os }}_ASF-linux-x64
path: out/ASF-linux-x64.zip
- name: Upload ASF-osx-arm64
continue-on-error: true
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3.0.0
with:
name: ${{ matrix.os }}_ASF-osx-arm64
path: out/ASF-osx-arm64.zip
- name: Upload ASF-osx-x64
continue-on-error: true
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3.0.0
with:
name: ${{ matrix.os }}_ASF-osx-x64
path: out/ASF-osx-x64.zip
- name: Upload ASF-win-x64
continue-on-error: true
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3.0.0
with:
name: ${{ matrix.os }}_ASF-win-x64
path: out/ASF-win-x64.zip
@@ -402,61 +402,61 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.0
# TODO: It'd be perfect if we could match final artifacts to the platform they target, so e.g. linux build comes from the linux machine
# However, that is currently impossible due to https://github.com/dotnet/msbuild/issues/3897
# Therefore, we'll (sadly) pull artifacts from Windows machine only for now
- name: Download ASF-generic artifact from windows-latest
uses: actions/download-artifact@v2.1.0
- name: Download ASF-generic artifact from windows-2019
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-generic
name: windows-2019_ASF-generic
path: out
- name: Download ASF-generic-netf artifact from windows-latest
uses: actions/download-artifact@v2.1.0
- name: Download ASF-generic-netf artifact from windows-2019
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-generic-netf
name: windows-2019_ASF-generic-netf
path: out
- name: Download ASF-linux-arm artifact from windows-latest
uses: actions/download-artifact@v2.1.0
- name: Download ASF-linux-arm artifact from windows-2019
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-linux-arm
name: windows-2019_ASF-linux-arm
path: out
- name: Download ASF-linux-arm64 artifact from windows-latest
uses: actions/download-artifact@v2.1.0
- name: Download ASF-linux-arm64 artifact from windows-2019
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-linux-arm64
name: windows-2019_ASF-linux-arm64
path: out
- name: Download ASF-linux-x64 artifact from windows-latest
uses: actions/download-artifact@v2.1.0
- name: Download ASF-linux-x64 artifact from windows-2019
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-linux-x64
name: windows-2019_ASF-linux-x64
path: out
- name: Download ASF-osx-arm64 artifact from windows-latest
uses: actions/download-artifact@v2.1.0
- name: Download ASF-osx-arm64 artifact from windows-2019
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-osx-arm64
name: windows-2019_ASF-osx-arm64
path: out
- name: Download ASF-osx-x64 artifact from windows-latest
uses: actions/download-artifact@v2.1.0
- name: Download ASF-osx-x64 artifact from windows-2019
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-osx-x64
name: windows-2019_ASF-osx-x64
path: out
- name: Download ASF-win-x64 artifact from windows-latest
uses: actions/download-artifact@v2.1.0
- name: Download ASF-win-x64 artifact from windows-2019
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-win-x64
name: windows-2019_ASF-win-x64
path: out
- name: Import GPG key for signing
uses: crazy-max/ghaction-import-gpg@v4.1.0
uses: crazy-max/ghaction-import-gpg@v4.2.0
with:
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
@@ -474,14 +474,14 @@ jobs:
- name: Upload SHA512SUMS
continue-on-error: true
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3.0.0
with:
name: SHA512SUMS
path: out/SHA512SUMS
- name: Upload SHA512SUMS.sign
continue-on-error: true
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3.0.0
with:
name: SHA512SUMS.sign
path: out/SHA512SUMS.sign

View File

@@ -10,7 +10,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.0
with:
submodules: recursive
token: ${{ secrets.ARCHIBOT_GITHUB_TOKEN }}
@@ -26,7 +26,7 @@ jobs:
git reset --hard origin/master
- name: Download latest translations from Crowdin
uses: crowdin/github-action@1.4.6
uses: crowdin/github-action@1.4.7
with:
upload_sources: false
download_translations: true
@@ -38,7 +38,7 @@ jobs:
token: ${{ secrets.ASF_CROWDIN_API_TOKEN }}
- name: Import GPG key for signing
uses: crazy-max/ghaction-import-gpg@v4.1.0
uses: crazy-max/ghaction-import-gpg@v4.2.0
with:
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
git_config_global: true

2
ASF-ui

Submodule ASF-ui updated: ff4313357a...156992e88d

View File

@@ -150,7 +150,7 @@ internal sealed class GlobalCache : SerializableFile {
LastChangeNumber = currentChangeNumber;
foreach ((uint appID, SteamApps.PICSChangesCallback.PICSChangeData appData) in appChanges) {
if (!AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) || (appData.ChangeNumber <= previousChangeNumber)) {
if (!AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) || (previousChangeNumber >= appData.ChangeNumber)) {
continue;
}
@@ -184,7 +184,7 @@ internal sealed class GlobalCache : SerializableFile {
bool save = false;
foreach ((uint appID, uint changeNumber) in appChangeNumbers) {
if (AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) && (previousChangeNumber == changeNumber)) {
if (AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) && (previousChangeNumber >= changeNumber)) {
continue;
}
@@ -281,19 +281,38 @@ internal sealed class GlobalCache : SerializableFile {
ArgumentNullException.ThrowIfNull(packages);
ArgumentNullException.ThrowIfNull(depots);
bool save = false;
foreach ((uint appID, ulong token) in apps) {
if (SubmittedApps.TryGetValue(appID, out ulong previousToken) && (previousToken == token)) {
continue;
}
SubmittedApps[appID] = token;
save = true;
}
foreach ((uint packageID, ulong token) in packages) {
if (SubmittedPackages.TryGetValue(packageID, out ulong previousToken) && (previousToken == token)) {
continue;
}
SubmittedPackages[packageID] = token;
save = true;
}
foreach ((uint depotID, string key) in depots) {
if (SubmittedDepots.TryGetValue(depotID, out string? previousKey) && (previousKey == key)) {
continue;
}
SubmittedDepots[depotID] = key;
save = true;
}
Utilities.InBackground(Save);
if (save) {
Utilities.InBackground(Save);
}
}
private static bool IsValidDepotKey(string depotKey) {

View File

@@ -97,7 +97,10 @@
<value>Dokončeno načítání celkem {0} přístupových tokenů.</value>
<comment>{0} will be replaced by the number (total count) of app access tokens retrieved</comment>
</data>
<data name="BotRetrievingTotalDepots" xml:space="preserve">
<value>Načítání všech úložišť, celkem z {0} aplikací...</value>
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
</data>
<data name="BotRetrievingAppInfos" xml:space="preserve">
<value>Získávání {0} informací o aplikaci...</value>
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
@@ -106,14 +109,37 @@
<value>Načítání informací o aplikaci {0} bylo dokončeno.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Získávání {0} tokenů úložišť...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Načítání {0} přístupových tokenů bylo dokončeno.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
<value>Načítání všech tokenbů úložišť, celkem z {0} aplikací bylo dokončeno.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
</data>
<data name="SubmissionNoNewData" xml:space="preserve">
<value>Nejsou k dispozici žádné nové údaje k odeslání, vše je aktuální.</value>
</data>
<data name="SubmissionNoContributorSet" xml:space="preserve">
<value>Data nelze odeslat, protože neexistuje žádné platné SteamID, které bychom mohli klasifikovat jako přispěvatele. Zvažte nastavení {0} parametrů.</value>
<comment>{0} will be replaced by the name of the config property (e.g. "SteamOwnerID") that the user is expected to set</comment>
</data>
<data name="SubmissionInProgress" xml:space="preserve">
<value>Odesílání celkem registrovaných aplikací/balíčků/úložišť: {0}/{1}/{2}...</value>
<comment>{0} will be replaced by the number of app access tokens being submitted, {1} will be replaced by the number of package access tokens being submitted, {2} will be replaced by the number of depot keys being submitted</comment>
</data>
<data name="SubmissionFailedTooManyRequests" xml:space="preserve">
<value>Odeslání se nezdařilo z důvodu příliš mnoha odeslaných požadavků. Pokusíme se znovu přibližně za {0}.</value>
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="SubmissionSuccessful" xml:space="preserve">
<value>Data byla úspěšně odeslána. Server zaregistroval celkem nové aplikace/balíčky/úložiště: {0} ({1} ověřeno)/{2} ({3} ověřeno)/{4} ({5} ověřeno).</value>
<comment>{0} will be replaced by the number of new app access tokens that the server has registered, {1} will be replaced by the number of verified app access tokens that the server has registered, {2} will be replaced by the number of new package access tokens that the server has registered, {3} will be replaced by the number of verified package access tokens that the server has registered, {4} will be replaced by the number of new depot keys that the server has registered, {5} will be replaced by the number of verified depot keys that the server has registered</comment>
</data>
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
<value>Nové aplikace: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
@@ -130,9 +156,18 @@
<value>Ověřené balíčky: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulNewDepots" xml:space="preserve">
<value>Nová úložiště: {0}</value>
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedDepots" xml:space="preserve">
<value>Ověřená úložiště: {0}</value>
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
</data>
<data name="PluginSecretListInitialized" xml:space="preserve">
<value>{0} inicializován, žádný plugin nebude rozpoznávat: {1}.</value>
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
</data>
<data name="LoadingGlobalCache" xml:space="preserve">
<value>Načítání globální mezipaměti STD...</value>
</data>

View File

@@ -475,7 +475,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotS
return;
}
ulong contributorSteamID = ASF.GlobalConfig is { SteamOwnerID: > 0 } && new SteamID(ASF.GlobalConfig.SteamOwnerID).IsIndividualAccount ? ASF.GlobalConfig.SteamOwnerID : Bot.Bots.Values.Where(static bot => bot.SteamID > 0).OrderByDescending(static bot => bot.OwnedPackageIDs.Count).FirstOrDefault()?.SteamID ?? 0;
ulong contributorSteamID = ASF.GlobalConfig is { SteamOwnerID: > 0 } && new SteamID(ASF.GlobalConfig.SteamOwnerID).IsIndividualAccount ? ASF.GlobalConfig.SteamOwnerID : Bot.Bots.Values.Where(static bot => bot.SteamID > 0).MaxBy(static bot => bot.OwnedPackageIDs.Count)?.SteamID ?? 0;
if (contributorSteamID == 0) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionNoContributorSet, nameof(ASF.GlobalConfig.SteamOwnerID)));

View File

@@ -302,6 +302,7 @@
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNullPropagation/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNullPropagationWhenPossible/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UsePositionalDeconstructionPattern/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseStringInterpolationWhenPossible/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseThrowIfNullMethod/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseVerbatimString/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=WrongIndentSize/@EntryIndexedValue">WARNING</s:String>

View File

@@ -0,0 +1,126 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
namespace ArchiSteamFarm.Collections;
public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue> where TKey : notnull {
public event EventHandler? OnModified;
[PublicAPI]
public int Count => BackingDictionary.Count;
[PublicAPI]
public bool IsEmpty => BackingDictionary.IsEmpty;
public bool IsReadOnly => false;
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<TKey, TValue> BackingDictionary = new();
int ICollection<KeyValuePair<TKey, TValue>>.Count => BackingDictionary.Count;
int IReadOnlyCollection<KeyValuePair<TKey, TValue>>.Count => BackingDictionary.Count;
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => BackingDictionary.Keys;
ICollection<TKey> IDictionary<TKey, TValue>.Keys => BackingDictionary.Keys;
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => BackingDictionary.Values;
ICollection<TValue> IDictionary<TKey, TValue>.Values => BackingDictionary.Values;
public TValue this[TKey key] {
get => BackingDictionary[key];
set {
if (BackingDictionary.TryGetValue(key, out TValue? savedValue) && EqualityComparer<TValue>.Default.Equals(savedValue, value)) {
return;
}
BackingDictionary[key] = value;
OnModified?.Invoke(this, EventArgs.Empty);
}
}
public void Add(KeyValuePair<TKey, TValue> item) {
(TKey key, TValue value) = item;
Add(key, value);
}
public void Add(TKey key, TValue value) => TryAdd(key, value);
public void Clear() {
if (BackingDictionary.IsEmpty) {
return;
}
BackingDictionary.Clear();
OnModified?.Invoke(this, EventArgs.Empty);
}
public bool Contains(KeyValuePair<TKey, TValue> item) => ((ICollection<KeyValuePair<TKey, TValue>>) BackingDictionary).Contains(item);
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) => ((ICollection<KeyValuePair<TKey, TValue>>) BackingDictionary).CopyTo(array, arrayIndex);
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => BackingDictionary.GetEnumerator();
public bool Remove(KeyValuePair<TKey, TValue> item) {
ICollection<KeyValuePair<TKey, TValue>> collection = BackingDictionary;
if (!collection.Remove(item)) {
return false;
}
OnModified?.Invoke(this, EventArgs.Empty);
return true;
}
public bool Remove(TKey key) {
if (!BackingDictionary.TryRemove(key, out _)) {
return false;
}
OnModified?.Invoke(this, EventArgs.Empty);
return true;
}
bool IDictionary<TKey, TValue>.ContainsKey(TKey key) => BackingDictionary.ContainsKey(key);
bool IReadOnlyDictionary<TKey, TValue>.ContainsKey(TKey key) => BackingDictionary.ContainsKey(key);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
bool IReadOnlyDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) => BackingDictionary.TryGetValue(key, out value!);
bool IDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) => BackingDictionary.TryGetValue(key, out value!);
[PublicAPI]
public bool TryAdd(TKey key, TValue value) {
if (!BackingDictionary.TryAdd(key, value)) {
return false;
}
OnModified?.Invoke(this, EventArgs.Empty);
return true;
}
[PublicAPI]
public bool TryGetValue(TKey key, out TValue? value) => BackingDictionary.TryGetValue(key, out value);
}

View File

@@ -101,7 +101,9 @@
<value>{0} е невалиден!</value>
<comment>{0} will be replaced by object's name</comment>
</data>
<data name="ErrorNoBotsDefined" xml:space="preserve">
<value>Бота не е настроен. Забравихте ли да проверите конфигурацията на ASF? Следвайте инструкцийте в "wiki" ако срещате трудности.</value>
</data>
<data name="ErrorObjectIsNull" xml:space="preserve">
<value>{0} е нулев!</value>
<comment>{0} will be replaced by object's name</comment>
@@ -189,7 +191,10 @@
<value>Моля въведете Вашият 2FA код от Steam authenticator приложението: </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="UserInputSteamGuard" xml:space="preserve">
<value>Моля, въведете SteamGuard кода, изпратен на Вашият e-mail: </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="UserInputSteamLogin" xml:space="preserve">
<value>Моля, въведете Вашият Steam login: </value>
<comment>Please note that this translation should end with space</comment>
@@ -219,38 +224,86 @@
<value>Не е намерен бот с името {0}!</value>
<comment>{0} will be replaced by bot's name query (string)</comment>
</data>
<data name="BotStatusOverview" xml:space="preserve">
<value>Има {0}/{1} работещи бота, с общо {2} игри ({3} карти) оставащи за "фармене".</value>
<comment>{0} will be replaced by number of active bots, {1} will be replaced by total number of bots, {2} will be replaced by total number of games left to farm, {3} will be replaced by total number of cards left to farm</comment>
</data>
<data name="BotStatusIdling" xml:space="preserve">
<value>Ботът фарми играта: {0} ({1}, {2} оставащи карти) от общо {3} игри ({4} карти) остават за "фармене" (~{5} оставащо време).</value>
<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 farm, {3} will be replaced by total number of games to farm, {4} will be replaced by total number of cards to farm, {5} will be replaced by translated TimeSpan string (such as "1 day, 5 hours and 30 minutes")</comment>
</data>
<data name="BotStatusIdlingList" xml:space="preserve">
<value>Ботът фарми игрите: {0} от общо {1} игри ({2} карти) остават за "фармене" (~{3} оставащо време).</value>
<comment>{0} will be replaced by list of the games (IDs, numbers), {1} will be replaced by total number of games to farm, {2} will be replaced by total number of cards to farm, {3} will be replaced by translated TimeSpan string (such as "1 day, 5 hours and 30 minutes")</comment>
</data>
<data name="CheckingFirstBadgePage" xml:space="preserve">
<value>Проверяване на първата страница със значки...</value>
</data>
<data name="CheckingOtherBadgePages" xml:space="preserve">
<value>Проверяване на други страници със значки...</value>
</data>
<data name="ChosenFarmingAlgorithm" xml:space="preserve">
<value>Избран алгоритъм за фармене: {0}</value>
<comment>{0} will be replaced by the name of chosen farming algorithm</comment>
</data>
<data name="Done" xml:space="preserve">
<value>Готово!</value>
</data>
<data name="GamesToIdle" xml:space="preserve">
<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">
<value>Фарменето на карти приключи!</value>
</data>
<data name="IdlingFinishedForGame" xml:space="preserve">
<value>Фарменето на карти завърши: {0} ({1}) след {2} игра!</value>
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name, {2} will be replaced by translated TimeSpan string (such as "1 day, 5 hours and 30 minutes")</comment>
</data>
<data name="IdlingFinishedForGames" xml:space="preserve">
<value>Фарменето на карти завърши за игрите: {0}</value>
<comment>{0} will be replaced by list of the games (IDs, numbers), separated by a comma</comment>
</data>
<data name="IdlingStatusForGame" xml:space="preserve">
<value>Статус на фарменето за {0} ({1}): {2} карти остават</value>
<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 farm</comment>
</data>
<data name="IdlingStopped" xml:space="preserve">
<value>Фарменето е спряно!</value>
</data>
<data name="IgnoredPermanentPauseEnabled" xml:space="preserve">
<value>Отказване на това желание, тъй като е активирана постоянна пауза!</value>
</data>
<data name="NothingToIdle" xml:space="preserve">
<value>Няма нищо за фармене на този акаунт!</value>
</data>
<data name="NowIdling" xml:space="preserve">
<value>Сега се фарми: {0} ({1})</value>
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
</data>
<data name="NowIdlingList" xml:space="preserve">
<value>Сега се фарми: {0}</value>
<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>Игрането не е възможно в момента, ще пробваме по-късно!</value>
</data>
<data name="StillIdling" xml:space="preserve">
<value>Все още се фарми: {0} ({1})</value>
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
</data>
<data name="StillIdlingList" xml:space="preserve">
<value>Все още се фарми: {0}</value>
<comment>{0} will be replaced by list of the games (IDs, numbers), separated by a comma</comment>
</data>
<data name="StoppedIdling" xml:space="preserve">
<value>Фарменето прекратено: {0} ({1})</value>
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
</data>
<data name="StoppedIdlingList" xml:space="preserve">
<value>Фарменето прекратено: {0}</value>
<comment>{0} will be replaced by list of the games (IDs, numbers), separated by a comma</comment>
</data>
<data name="UnknownCommand" xml:space="preserve">
<value>Непозната команда!</value>
</data>
@@ -265,7 +318,9 @@
<value>Приемане на подарък: {0}...</value>
<comment>{0} will be replaced by giftID (number)</comment>
</data>
<data name="BotAccountLimited" xml:space="preserve">
<value>Този акаунт е ограничен, фарменето е невъзможен докато ограничението не бъде премахнато!</value>
</data>
<data name="BotAddLicense" xml:space="preserve">
<value>ID: {0} | Статус: {1}</value>
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by status string</comment>
@@ -287,10 +342,18 @@
<value>2FA Token: {0}</value>
<comment>{0} will be replaced by generated 2FA token (string)</comment>
</data>
<data name="BotAutomaticIdlingNowPaused" xml:space="preserve">
<value>Автоматичното фармене е паузирано!</value>
</data>
<data name="BotAutomaticIdlingNowResumed" xml:space="preserve">
<value>Автоматичното фармене е възобновено!</value>
</data>
<data name="BotAutomaticIdlingPausedAlready" xml:space="preserve">
<value>Автоматичното фармене е вече паузирано!</value>
</data>
<data name="BotAutomaticIdlingResumedAlready" xml:space="preserve">
<value>Автоматичното фармене е вече възобновено!</value>
</data>
<data name="BotConnected" xml:space="preserve">
<value>Свързан към Steam!</value>
</data>
@@ -347,7 +410,10 @@
<value>Вече се притежава: {0} | {1}</value>
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
</data>
<data name="BotPointsBalance" xml:space="preserve">
<value>Баланс на точките: {0}</value>
<comment>{0} will be replaced by the points balance value (integer)</comment>
</data>
<data name="BotRateLimitExceeded" xml:space="preserve">
<value>Ограничението надвишено, ще повторим след {0} на "отброяване"...</value>
<comment>{0} will be replaced by translated TimeSpan string (such as "25 minutes")</comment>
@@ -366,8 +432,12 @@
<data name="BotRemovedExpiredLoginKey" xml:space="preserve">
<value>Премахнат изтекъл ключ за вход!</value>
</data>
<data name="BotStatusNotIdling" xml:space="preserve">
<value>Бота не фарми нищо.</value>
</data>
<data name="BotStatusLimited" xml:space="preserve">
<value>Бота е ограничен и не може да "дропне" карти чрез фармене.</value>
</data>
<data name="BotStatusConnecting" xml:space="preserve">
<value>Ботът се свързва към мрежата на Steam.</value>
</data>
@@ -399,8 +469,12 @@
<data name="BotConnectionLost" xml:space="preserve">
<value>Връзката с мрежата на Steam е загубена. Повторно свързване...</value>
</data>
<data name="BotAccountFree" xml:space="preserve">
<value>Акаунтът вече не е зает: фарменето на карти е възобновено!</value>
</data>
<data name="BotAccountOccupied" xml:space="preserve">
<value>Акаунтът в момента се ползва: ASF ще възобнови фарменето на карти, когато е свободен...</value>
</data>
<data name="BotConnecting" xml:space="preserve">
<value>Свързване…</value>
</data>
@@ -438,7 +512,10 @@
<value>ASF ще се опита да ползва вашият предпочитан {0} език, но преводът за този език е само {1} завършен. Може би може да ни помогнете да подобрим ASF с превод на вашия език?</value>
<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>Фарменето на {0} ({1}) е временно забранено, тъй като ASF не е в състояние да "играе" тази игра в момента.</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">
<value>ASF откри несъответствие на ID за {0} ({1}) и ще използва ID на {2} вместо това.</value>
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name, {2} will be replaced by game's ID (number)</comment>
@@ -447,8 +524,12 @@
<value>{0} V{1}</value>
<comment>{0} will be replaced by program's name (e.g. "ASF"), {1} will be replaced by program's version (e.g. "1.0.0.0"). This string typically has nothing to translate and you should leave it as it is, unless you need to change the format, e.g. in RTL languages.</comment>
</data>
<data name="BotAccountLocked" xml:space="preserve">
<value>Този акаунт е заключен, фарменето на карти е недостъпно завинаги!</value>
</data>
<data name="BotStatusLocked" xml:space="preserve">
<value>Бота е заключен и не може да "дропне" карти чрез фармене.</value>
</data>
<data name="ErrorFunctionOnlyInHeadlessMode" xml:space="preserve">
<value>Тази функция е възможна само в headless режим!</value>
</data>
@@ -459,7 +540,9 @@
<data name="ErrorAccessDenied" xml:space="preserve">
<value>Достъпът отказан!</value>
</data>
<data name="WarningPreReleaseVersion" xml:space="preserve">
<value>Вие използвате версия, която е по-нова от последната версия за актуализация. Моля, имайте предвид, че предварителните версии са посветени на потребителите, които знаят как да докладват за бъгове, да се справят с проблеми и да дават обратна връзка - няма да ви бъде осигурена техническа подръжка.</value>
</data>
<data name="BotStats" xml:space="preserve">
<value>Текущо използване на паметта: {0} MB.
Работно време на процеса: {1}</value>
@@ -562,7 +645,10 @@
<value>Успешно извършени {0} потвърждения!</value>
<comment>{0} will be replaced by number of confirmations</comment>
</data>
<data name="BotExtraIdlingCooldown" xml:space="preserve">
<value>Изчакване до {0}, за да се гарантира, че е може да започне фарменето на карти...</value>
<comment>{0} will be replaced by translated TimeSpan string (such as "1 minute")</comment>
</data>
<data name="UpdateCleanup" xml:space="preserve">
<value>Изтриване на старите файлове след обновление...</value>
</data>

View File

@@ -105,7 +105,7 @@ StackTrace:
<comment>{0} will be replaced by object's name</comment>
</data>
<data name="ErrorNoBotsDefined" xml:space="preserve">
<value>Nenhum bot foi definido. Você esqueceu de configurar seu ASF? Confira o guia de "configuração" no wiki se você estiver confuso.</value>
<value>Nenhum bot definido. Você esqueceu de configurar o ASF? Confira o guia "Primeiros passos" na wiki caso esteja com dúvidas.</value>
</data>
<data name="ErrorObjectIsNull" xml:space="preserve">
<value>{0} é nulo!</value>
@@ -195,7 +195,7 @@ StackTrace:
<comment>Please note that this translation should end with space</comment>
</data>
<data name="UserInputSteamGuard" xml:space="preserve">
<value>Por favor digite o código do Steam Guard que foi enviado para o seu e-mail: </value>
<value>Insira o código do Steam Guard recebido por e-mail: </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="UserInputSteamLogin" xml:space="preserve">
@@ -692,27 +692,27 @@ Tempo de execução: {1}</value>
<comment>{0} will be replaced with the relative path to the affected config file</comment>
</data>
<data name="WarningWeakIPCPassword" xml:space="preserve">
<value>Sua senha IPC parece muito fraca. Considere escolher uma senha mais forte para melhorar sua segurança. Detalhe: {0}</value>
<value>A senha do servidor IPC é muito fraca. Escolha uma senha mais forte para aumentar a segurança. Detalhes: {0}</value>
<comment>{0} will be replaced by additional details about the password being considered weak</comment>
</data>
<data name="WarningWeakSteamPassword" xml:space="preserve">
<value>Sua senha Steam para '{0}' parece ser fraca. Considere escolher uma mais forte para aumentar a segurança. Detalhes: {1}</value>
<value>A senha da conta Steam "{0}" é muito fraca. Escolha uma senha mais forte para aumentar a segurança. Detalhes: {0}</value>
<comment>{0} will be replaced by either the affected bot name or the path to the bots configuration file, {1} will be replaced by additional details about the password being considered weak</comment>
</data>
<data name="WarningWeakCryptKey" xml:space="preserve">
<value>Sua chave de criptografia parece ser fraca. Considere escolher uma mais forte para melhorar sua segurança. Detalhe: {0}</value>
<value>A chave de criptografia é muito fraca. Escolha uma senha mais forte para aumentar a segurança. Detalhes: {0}</value>
<comment>{0} will be replaced by additional details about the encryption key being considered weak</comment>
</data>
<data name="WarningTooShortCryptKey" xml:space="preserve">
<value>Sua chave de criptografia é muito curta. É recomendado utilizar uma com no mínimo {0} caracteres.</value>
<value>A chave de criptografia é muito curta. Recomendamos usar uma chave que tenha pelo menos {0} bytes (caracteres) de comprimento.</value>
<comment>{0} will be replaced by the number of bytes (characters) recommended</comment>
</data>
<data name="WarningDefaultCryptKeyUsedForHashing" xml:space="preserve">
<value>Você está usando a configuração {0} de propriedade {1}, mas você não forneceu uma --cryptkey personalizada. Você deve fornecer uma --cryptkey personalizada para maior segurança.</value>
<value>Você está usando a configuração {0} da propriedade {1} sem uma --cryptkey personalizada. Forneça uma --cryptkey personalizada para aumentar a segurança.</value>
<comment>{0} will be replaced by the name of a particular setting (e.g. "SCrypt"), {1} will be replaced by the name of the property (e.g. "IPCPassword")</comment>
</data>
<data name="WarningDefaultCryptKeyUsedForEncryption" xml:space="preserve">
<value>Você está utilizando {0} configuração de {1}, mas você não forneceu uma senha de criptografia personalizada. Isto anula totalmente a proteção, porque o Archi Steam Farm é forçado a utilizar sua própria senha (pública). Você deverá fornecer uma senha de criptografia personalizada para utilizar os benefícios de segurança fornecidos por esta configuração.</value>
<value>Você está usando a configuração {0} da propriedade {1} sem uma --cryptkey personalizada. O ASF será forçado a usar a chave padrão, que é menos segura e inutiliza a proteção configurada. Forneça uma --cryptkey personalizada para se beneficiar da segurança adicional.</value>
<comment>{0} will be replaced by the name of a particular setting (e.g. "AES"), {1} will be replaced by the name of the property (e.g. "SteamPassword")</comment>
</data>
<data name="WarningRunningAsRoot" xml:space="preserve">

View File

@@ -697,7 +697,7 @@ Process uptime: {1}</value>
</data>
<data name="WarningWeakSteamPassword" xml:space="preserve">
<value>Your Steam password for '{0}' seems to be weak. Consider choosing a stronger one for increased security. Details: {1}</value>
<comment>{0} will be replaced by either the affected bot name or the path to the bots configuration file, {1} will be replaced by additional details about the password being considered weak</comment>
<comment>{0} will be replaced by the affected bot name, {1} will be replaced by additional details about the password being considered weak</comment>
</data>
<data name="WarningWeakCryptKey" xml:space="preserve">
<value>Your encryption key seems to be weak. Consider choosing a stronger one for increased security. Details: {0}</value>

View File

@@ -299,8 +299,14 @@
<value>Все ще фармиться: {0}</value>
<comment>{0} will be replaced by list of the games (IDs, numbers), separated by a comma</comment>
</data>
<data name="StoppedIdling" xml:space="preserve">
<value>Зупинено фарм: {0} ({1})</value>
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
</data>
<data name="StoppedIdlingList" xml:space="preserve">
<value>Зупинено фарм: {0}</value>
<comment>{0} will be replaced by list of the games (IDs, numbers), separated by a comma</comment>
</data>
<data name="UnknownCommand" xml:space="preserve">
<value>Невідома команда!</value>
</data>
@@ -315,7 +321,9 @@
<value>Прийняття подарунку: {0}...</value>
<comment>{0} will be replaced by giftID (number)</comment>
</data>
<data name="BotAccountLimited" xml:space="preserve">
<value>Цей акаунт обмежений, фарм буде недоступним доки ви не позбудетесь обмежень!</value>
</data>
<data name="BotAddLicense" xml:space="preserve">
<value>ID: {0} | Стан: {1}</value>
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by status string</comment>

View File

@@ -194,7 +194,10 @@ StackTrace:
<value>Xin vui lòng nhập mã 2FA từ ứng dụng Steam Authenticator của bạn: </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="UserInputSteamGuard" xml:space="preserve">
<value>Vui lòng nhập mã SteamGuard đã được gửi tới email của bạn: </value>
<comment>Please note that this translation should end with space</comment>
</data>
<data name="UserInputSteamLogin" xml:space="preserve">
<value>Xin vui lòng nhập tài khoản Steam của bạn: </value>
<comment>Please note that this translation should end with space</comment>

View File

@@ -601,7 +601,7 @@ public sealed class Bot : IAsyncDisposable {
}
if (itemsPerSet < itemsPerClassID.Count) {
throw new InvalidOperationException($"{nameof(inventory)} && {nameof(amountsToExtract)}");
throw new InvalidOperationException($"{nameof(itemsPerSet)} < {nameof(itemsPerClassID)}");
}
if (itemsPerSet > itemsPerClassID.Count) {
@@ -1307,9 +1307,7 @@ public sealed class Bot : IAsyncDisposable {
OrderedDictionary gamesToRedeemInBackground = new();
using (StreamReader reader = new(filePath)) {
string? line;
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) {
while (await reader.ReadLineAsync().ConfigureAwait(false) is { } line) {
if (line.Length == 0) {
continue;
}
@@ -1397,7 +1395,7 @@ public sealed class Bot : IAsyncDisposable {
return;
}
(BotConfig? botConfig, _) = await BotConfig.Load(configFile).ConfigureAwait(false);
(BotConfig? botConfig, _) = await BotConfig.Load(configFile, BotName).ConfigureAwait(false);
if (botConfig == null) {
await Destroy().ConfigureAwait(false);
@@ -1499,7 +1497,7 @@ public sealed class Bot : IAsyncDisposable {
return;
}
(BotConfig? botConfig, string? latestJson) = await BotConfig.Load(configFilePath).ConfigureAwait(false);
(BotConfig? botConfig, string? latestJson) = await BotConfig.Load(configFilePath, botName).ConfigureAwait(false);
if (botConfig == null) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorBotConfigInvalid, configFilePath));
@@ -1831,9 +1829,7 @@ public sealed class Bot : IAsyncDisposable {
try {
using StreamReader reader = new(filePath);
string? line;
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) {
while (await reader.ReadLineAsync().ConfigureAwait(false) is { } line) {
if (line.Length == 0) {
continue;
}
@@ -3373,21 +3369,42 @@ public sealed class Bot : IAsyncDisposable {
}
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), List<uint>> inventorySets = Trading.GetInventorySets(inventory);
// Filter appIDs that can't possibly be completed due to having less cards than smallest badges possible
appIDs.IntersectWith(inventorySets.Where(static kv => kv.Value.Count >= MinCardsPerBadge).Select(static kv => kv.Key.RealAppID));
if (appIDs.Count == 0) {
return;
}
Dictionary<uint, byte>? cardCountPerAppID = await LoadCardsPerSet(appIDs).ConfigureAwait(false);
Dictionary<uint, byte>? cardsCountPerAppID = await LoadCardsPerSet(appIDs).ConfigureAwait(false);
if ((cardCountPerAppID == null) || (cardCountPerAppID.Count == 0)) {
if (cardsCountPerAppID == null) {
return;
}
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), (uint Sets, byte CardsPerSet)> itemsToTakePerInventorySet = inventorySets.Where(kv => appIDs.Contains(kv.Key.RealAppID)).ToDictionary(static kv => kv.Key, kv => (kv.Value[0], cardCountPerAppID[kv.Key.RealAppID]));
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), (uint Sets, byte CardsPerSet)> itemsToTakePerInventorySet = new();
if (itemsToTakePerInventorySet.Values.All(static value => value.Sets == 0)) {
foreach (((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) key, List<uint>? amounts) in inventorySets.Where(set => appIDs.Contains(set.Key.RealAppID))) {
if (!cardsCountPerAppID.TryGetValue(key.RealAppID, out byte cardsCount) || (cardsCount == 0)) {
throw new InvalidOperationException(nameof(cardsCount));
}
if (amounts.Count < cardsCount) {
// Filter results that can't be completed due to not having enough cards available (now that we know how much exactly)
continue;
}
uint minimumOwnedAmount = amounts[0];
if (minimumOwnedAmount == 0) {
throw new InvalidOperationException(nameof(minimumOwnedAmount));
}
itemsToTakePerInventorySet[key] = (minimumOwnedAmount, cardsCount);
}
if (itemsToTakePerInventorySet.Count == 0) {
return;
}

View File

@@ -130,6 +130,37 @@ public sealed class ArchiHandler : ClientMsgHandler {
return body.games.ToDictionary(static game => (uint) game.appid, static game => game.name);
}
[PublicAPI]
public async Task<string?> GetTradeToken() {
if (Client == null) {
throw new InvalidOperationException(nameof(Client));
}
if (!Client.IsConnected) {
return null;
}
CEcon_GetTradeOfferAccessToken_Request request = new();
SteamUnifiedMessages.ServiceMethodResponse response;
try {
response = await UnifiedEconService.SendMessage(x => x.GetTradeOfferAccessToken(request)).ToLongRunningTask().ConfigureAwait(false);
} catch (Exception e) {
ArchiLogger.LogGenericWarningException(e);
return null;
}
if (response.Result != EResult.OK) {
return null;
}
CEcon_GetTradeOfferAccessToken_Response body = response.GetDeserializedResponse<CEcon_GetTradeOfferAccessToken_Response>();
return body.trade_offer_access_token;
}
public override void HandleMsg(IPacketMsg packetMsg) {
ArgumentNullException.ThrowIfNull(packetMsg);
@@ -401,36 +432,6 @@ public sealed class ArchiHandler : ClientMsgHandler {
return body.privacy_settings;
}
internal async Task<string?> GetTradeToken() {
if (Client == null) {
throw new InvalidOperationException(nameof(Client));
}
if (!Client.IsConnected) {
return null;
}
CEcon_GetTradeOfferAccessToken_Request request = new();
SteamUnifiedMessages.ServiceMethodResponse response;
try {
response = await UnifiedEconService.SendMessage(x => x.GetTradeOfferAccessToken(request)).ToLongRunningTask().ConfigureAwait(false);
} catch (Exception e) {
ArchiLogger.LogGenericWarningException(e);
return null;
}
if (response.Result != EResult.OK) {
return null;
}
CEcon_GetTradeOfferAccessToken_Response body = response.GetDeserializedResponse<CEcon_GetTradeOfferAccessToken_Response>();
return body.trade_offer_access_token;
}
internal async Task<string?> GetTwoFactorDeviceIdentifier(ulong steamID) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));

View File

@@ -20,7 +20,6 @@
// limitations under the License.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
@@ -67,8 +66,6 @@ public sealed class ArchiWebHandler : IDisposable {
[PublicAPI]
public static Uri SteamStoreURL => new("https://store.steampowered.com");
private static readonly ConcurrentDictionary<uint, byte> CachedCardCountsForGame = new();
private static ushort WebLimiterDelay => ASF.GlobalConfig?.WebLimiterDelay ?? GlobalConfig.DefaultWebLimiterDelay;
[PublicAPI]
@@ -1705,7 +1702,7 @@ public sealed class ArchiWebHandler : IDisposable {
throw new ArgumentOutOfRangeException(nameof(appID));
}
if (CachedCardCountsForGame.TryGetValue(appID, out byte result)) {
if (ASF.GlobalDatabase?.CardCountsPerGame.TryGetValue(appID, out byte result) == true) {
return result;
}
@@ -1727,7 +1724,7 @@ public sealed class ArchiWebHandler : IDisposable {
return 0;
}
CachedCardCountsForGame.TryAdd(appID, result);
ASF.GlobalDatabase?.CardCountsPerGame.TryAdd(appID, result);
return result;
}

View File

@@ -74,9 +74,7 @@ internal static class SteamChatMessage {
using StringReader stringReader = new(message);
string? line;
while ((line = await stringReader.ReadLineAsync().ConfigureAwait(false)) != null) {
while (await stringReader.ReadLineAsync().ConfigureAwait(false) is { } line) {
// Special case for empty newline
if (line.Length == 0) {
if (messagePart.Length > prefixLength) {

View File

@@ -107,6 +107,19 @@ public sealed class Actions : IAsyncDisposable {
return (success, token, success ? Strings.Success : Strings.WarningFailed);
}
[PublicAPI]
public ulong GetFirstSteamMasterID() {
ulong steamMasterID = Bot.BotConfig.SteamUserPermissions.Where(kv => (kv.Key > 0) && (kv.Key != Bot.SteamID) && new SteamID(kv.Key).IsIndividualAccount && (kv.Value == BotConfig.EAccess.Master)).Select(static kv => kv.Key).OrderBy(static steamID => steamID).FirstOrDefault();
if (steamMasterID > 0) {
return steamMasterID;
}
ulong steamOwnerID = ASF.GlobalConfig?.SteamOwnerID ?? GlobalConfig.DefaultSteamOwnerID;
return (steamOwnerID > 0) && new SteamID(steamOwnerID).IsIndividualAccount ? steamOwnerID : 0;
}
[PublicAPI]
public async Task<IDisposable> GetTradingLock() {
await TradingSemaphore.WaitAsync().ConfigureAwait(false);
@@ -506,18 +519,6 @@ public sealed class Actions : IAsyncDisposable {
internal void OnDisconnected() => HandledGifts.Clear();
private ulong GetFirstSteamMasterID() {
ulong steamMasterID = Bot.BotConfig.SteamUserPermissions.Where(kv => (kv.Key > 0) && (kv.Key != Bot.SteamID) && new SteamID(kv.Key).IsIndividualAccount && (kv.Value == BotConfig.EAccess.Master)).Select(static kv => kv.Key).OrderBy(static steamID => steamID).FirstOrDefault();
if (steamMasterID > 0) {
return steamMasterID;
}
ulong steamOwnerID = ASF.GlobalConfig?.SteamOwnerID ?? GlobalConfig.DefaultSteamOwnerID;
return (steamOwnerID > 0) && new SteamID(steamOwnerID).IsIndividualAccount ? steamOwnerID : 0;
}
private static async Task LimitGiftsRequestsAsync() {
if (ASF.GiftsSemaphore == null) {
throw new InvalidOperationException(nameof(ASF.GiftsSemaphore));

View File

@@ -81,6 +81,30 @@ public sealed class Commands {
return $"<{SharedInfo.ASF}> {response}";
}
[PublicAPI]
public static EAccess GetProxyAccess(Bot bot, EAccess access, ulong steamID = 0) {
// The objective here should be simple, calculating effective access of the user
// Problem is, history already proved nothing in this damn file is as simple as it seems
// We use this function for proxying commands such as !status 2 sent to bot 1, which should use 2's user access instead
ArgumentNullException.ThrowIfNull(bot);
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
// If we got executed with owner access or lack steamID entirely, then this is effective access
if ((access >= EAccess.Owner) || (steamID == 0)) {
return access;
}
// Otherwise, effective access is the access of the user on target bot, whatever that would be, not this one
return bot.GetAccess(steamID);
}
[PublicAPI]
public async Task<string?> Response(EAccess access, string message, ulong steamID = 0) {
if (!Enum.IsDefined(access)) {
@@ -504,29 +528,6 @@ public sealed class Commands {
return gamesOwned;
}
private static EAccess ProxyAccess(Bot bot, EAccess access, ulong steamID = 0) {
// The objective here should be simple, calculating effective access of the user
// Problem is, history already proved nothing in this damn file is as simple as it seems
// We use this function for proxying commands such as !status 2 sent to bot 1, which should use 2's user access instead
ArgumentNullException.ThrowIfNull(bot);
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
// If we got executed with owner access or lack steamID entirely, then this is effective access
if ((access >= EAccess.Owner) || (steamID == 0)) {
return access;
}
// Otherwise, effective access is the access of the user on target bot, whatever that would be, not this one
return bot.GetAccess(steamID);
}
private async Task<string?> Response2FA(EAccess access) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
@@ -556,7 +557,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.Response2FA(ProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.Response2FA(GetProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -600,7 +601,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.Response2FAConfirm(ProxyAccess(bot, access, steamID), confirm))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.Response2FAConfirm(GetProxyAccess(bot, access, steamID), confirm))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -698,7 +699,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseAddLicense(ProxyAccess(bot, access, steamID), query))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseAddLicense(GetProxyAccess(bot, access, steamID), query))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -762,7 +763,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseAdvancedLoot(ProxyAccess(bot, access, steamID), appID, contextID))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseAdvancedLoot(GetProxyAccess(bot, access, steamID), appID, contextID))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -877,7 +878,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseAdvancedRedeem(ProxyAccess(bot, access, steamID), options, keys, steamID))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseAdvancedRedeem(GetProxyAccess(bot, access, steamID), options, keys, steamID))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -991,7 +992,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNameTo)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseAdvancedTransfer(ProxyAccess(bot, access, steamID), appID, contextID, targetBot))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseAdvancedTransfer(GetProxyAccess(bot, access, steamID), appID, contextID, targetBot))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1027,7 +1028,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseBackgroundGamesRedeemer(ProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseBackgroundGamesRedeemer(GetProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1111,7 +1112,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseFarm(ProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseFarm(GetProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1141,7 +1142,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseFarmingBlacklist(ProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseFarmingBlacklist(GetProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1212,7 +1213,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseFarmingBlacklistAdd(ProxyAccess(bot, access, steamID), targetAppIDs)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseFarmingBlacklistAdd(GetProxyAccess(bot, access, steamID), targetAppIDs)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1278,7 +1279,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseFarmingBlacklistRemove(ProxyAccess(bot, access, steamID), targetAppIDs)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseFarmingBlacklistRemove(GetProxyAccess(bot, access, steamID), targetAppIDs)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1308,7 +1309,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseFarmingQueue(ProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseFarmingQueue(GetProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1386,7 +1387,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseFarmingQueueAdd(ProxyAccess(bot, access, steamID), targetAppIDs)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseFarmingQueueAdd(GetProxyAccess(bot, access, steamID), targetAppIDs)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1457,7 +1458,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseFarmingQueueRemove(ProxyAccess(bot, access, steamID), targetAppIDs)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseFarmingQueueRemove(GetProxyAccess(bot, access, steamID), targetAppIDs)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1553,7 +1554,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseInput(ProxyAccess(bot, access, steamID), propertyName, inputValue)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseInput(GetProxyAccess(bot, access, steamID), propertyName, inputValue)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1593,7 +1594,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseLevel(ProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseLevel(GetProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1637,7 +1638,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseLoot(ProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseLoot(GetProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1705,7 +1706,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseLootByRealAppIDs(ProxyAccess(bot, access, steamID), realAppIDsText, exclude))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseLootByRealAppIDs(GetProxyAccess(bot, access, steamID), realAppIDsText, exclude))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1735,7 +1736,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseMatchActivelyBlacklist(ProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseMatchActivelyBlacklist(GetProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1793,7 +1794,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseMatchActivelyBlacklistAdd(ProxyAccess(bot, access, steamID), targetAppIDs)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseMatchActivelyBlacklistAdd(GetProxyAccess(bot, access, steamID), targetAppIDs)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1851,7 +1852,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseMatchActivelyBlacklistRemove(ProxyAccess(bot, access, steamID), targetAppIDs)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseMatchActivelyBlacklistRemove(GetProxyAccess(bot, access, steamID), targetAppIDs)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -1902,7 +1903,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseNickname(ProxyAccess(bot, access, steamID), nickname)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseNickname(GetProxyAccess(bot, access, steamID), nickname)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -2080,7 +2081,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<(string? Response, Dictionary<string, string>? OwnedGames)> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseOwns(ProxyAccess(bot, access, steamID), query))).ConfigureAwait(false);
IList<(string? Response, Dictionary<string, string>? OwnedGames)> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseOwns(GetProxyAccess(bot, access, steamID), query))).ConfigureAwait(false);
List<(string Response, Dictionary<string, string> OwnedGames)> validResults = new(results.Where(static result => !string.IsNullOrEmpty(result.Response) && (result.OwnedGames != null))!);
@@ -2148,7 +2149,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponsePause(ProxyAccess(bot, access, steamID), permanent, resumeInSecondsText))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponsePause(GetProxyAccess(bot, access, steamID), permanent, resumeInSecondsText))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -2245,7 +2246,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponsePlay(ProxyAccess(bot, access, steamID), targetGameIDs))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponsePlay(GetProxyAccess(bot, access, steamID), targetGameIDs))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -2285,7 +2286,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponsePointsBalance(ProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponsePointsBalance(GetProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -2445,7 +2446,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponsePrivacy(ProxyAccess(bot, access, steamID), privacySettingsText))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponsePrivacy(GetProxyAccess(bot, access, steamID), privacySettingsText))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -2730,7 +2731,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseRedeem(ProxyAccess(bot, access, steamID), keysText, steamID, redeemFlags))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseRedeem(GetProxyAccess(bot, access, steamID), keysText, steamID, redeemFlags))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -2770,7 +2771,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseReset(ProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseReset(GetProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -2820,7 +2821,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseResume(ProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseResume(GetProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -2856,7 +2857,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseStart(ProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseStart(GetProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -2935,7 +2936,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<(string? Response, Bot Bot)> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseStatus(ProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
IList<(string? Response, Bot Bot)> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseStatus(GetProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
List<(string Response, Bot Bot)> validResults = new(results.Where(static result => !string.IsNullOrEmpty(result.Response))!);
@@ -2979,7 +2980,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseStop(ProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseStop(GetProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -3009,7 +3010,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseTradingBlacklist(ProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseTradingBlacklist(GetProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -3067,7 +3068,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseTradingBlacklistAdd(ProxyAccess(bot, access, steamID), targetSteamIDs)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseTradingBlacklistAdd(GetProxyAccess(bot, access, steamID), targetSteamIDs)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -3125,7 +3126,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseTradingBlacklistRemove(ProxyAccess(bot, access, steamID), targetSteamIDs)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseTradingBlacklistRemove(GetProxyAccess(bot, access, steamID), targetSteamIDs)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -3191,7 +3192,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseTransfer(ProxyAccess(bot, access, steamID), botNameTo))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseTransfer(GetProxyAccess(bot, access, steamID), botNameTo))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -3321,7 +3322,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNameTo)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseTransferByRealAppIDs(ProxyAccess(bot, access, steamID), realAppIDs, targetBot, exclude))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseTransferByRealAppIDs(GetProxyAccess(bot, access, steamID), realAppIDs, targetBot, exclude))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -3387,7 +3388,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseUnpackBoosters(ProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseUnpackBoosters(GetProxyAccess(bot, access, steamID)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
@@ -3443,7 +3444,7 @@ public sealed class Commands {
return access >= EAccess.Owner ? FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseWalletBalance(ProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseWalletBalance(GetProxyAccess(bot, access, steamID))))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);

View File

@@ -540,7 +540,7 @@ public sealed class BotConfig {
return result;
}
internal static async Task<(BotConfig? BotConfig, string? LatestJson)> Load(string filePath) {
internal static async Task<(BotConfig? BotConfig, string? LatestJson)> Load(string filePath, string? botName = null) {
if (string.IsNullOrEmpty(filePath)) {
throw new ArgumentNullException(nameof(filePath));
}
@@ -600,7 +600,11 @@ public sealed class BotConfig {
(bool isWeak, string? reason) = Utilities.TestPasswordStrength(decryptedSteamPassword!, disallowedValues);
if (isWeak) {
ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningWeakSteamPassword, !string.IsNullOrEmpty(botConfig.SteamLogin) ? botConfig.SteamLogin! : filePath, reason));
if (string.IsNullOrEmpty(botName)) {
botName = Path.GetFileNameWithoutExtension(filePath);
}
ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningWeakSteamPassword, botName, reason));
}
}
);

View File

@@ -28,6 +28,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Collections;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.Localization;
@@ -48,6 +49,9 @@ public sealed class GlobalDatabase : SerializableFile {
[PublicAPI]
public IReadOnlyDictionary<uint, (uint ChangeNumber, ImmutableHashSet<uint>? AppIDs)> PackagesDataReadOnly => PackagesData;
[JsonProperty(Required = Required.DisallowNull)]
internal readonly ObservableConcurrentDictionary<uint, byte> CardCountsPerGame = new();
[JsonProperty(Required = Required.DisallowNull)]
internal readonly InMemoryServerListProvider ServerListProvider = new();
@@ -107,7 +111,10 @@ public sealed class GlobalDatabase : SerializableFile {
}
[JsonConstructor]
private GlobalDatabase() => ServerListProvider.ServerListUpdated += OnObjectModified;
private GlobalDatabase() {
CardCountsPerGame.OnModified += OnObjectModified;
ServerListProvider.ServerListUpdated += OnObjectModified;
}
[PublicAPI]
public void DeleteFromJsonStorage(string key) {
@@ -159,6 +166,9 @@ public sealed class GlobalDatabase : SerializableFile {
[UsedImplicitly]
public bool ShouldSerializeBackingLastChangeNumber() => LastChangeNumber != 0;
[UsedImplicitly]
public bool ShouldSerializeCardCountsPerGame() => !CardCountsPerGame.IsEmpty;
[UsedImplicitly]
public bool ShouldSerializeKeyValueJsonStorage() => !KeyValueJsonStorage.IsEmpty;
@@ -174,6 +184,7 @@ public sealed class GlobalDatabase : SerializableFile {
protected override void Dispose(bool disposing) {
if (disposing) {
// Events we registered
CardCountsPerGame.OnModified -= OnObjectModified;
ServerListProvider.ServerListUpdated -= OnObjectModified;
// Those are objects that are always being created if constructor doesn't throw exception

View File

@@ -10,4 +10,11 @@
<assembly fullname="System.Composition.AttributedModel">
<type fullname="System.Composition.ExportAttribute" />
</assembly>
<!-- This is needed for our STD plugin -->
<assembly fullname="System.Linq">
<type fullname="System.Linq.Enumerable">
<method name="MaxBy" />
</type>
</assembly>
</linker>

View File

@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>5.2.3.3</Version>
<Version>5.2.3.7</Version>
</PropertyGroup>
<PropertyGroup>

View File

@@ -6,7 +6,7 @@
<PackageVersion Include="Humanizer" Version="2.14.1" />
<PackageVersion Include="JetBrains.Annotations" Version="2021.3.0" />
<PackageVersion Include="Markdig.Signed" Version="0.27.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageVersion Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageVersion Include="MSTest.TestFramework" Version="2.2.8" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
@@ -27,7 +27,7 @@
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
<PackageVersion Include="JustArchiNET.Madness" Version="3.2.1" />
<PackageVersion Include="JustArchiNET.Madness" Version="3.3.0" />
<PackageVersion Include="Microsoft.AspNetCore.Cors" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />

View File

@@ -43,20 +43,20 @@
## Description
ASF is a C# application with primary purpose of idling Steam cards from multiple accounts simultaneously. Unlike Idle Master which works only for one account at given time, while requiring Steam client running in the background and launching additional processes imitating "game playing" status, ASF doesn't require any Steam client running in the background, doesn't launch any additional processes and is made to handle unlimited Steam accounts at once. In addition to that, it's meant to be run on servers or other desktop-less machines, and features full cross-OS support, which makes it possible to launch on any operating system with .NET Core runtime, such as Windows, Linux and OS X. ASF is possible thanks to gigantic amount of work done in marvelous **[SteamKit2](https://github.com/SteamRE/SteamKit)** library.
ASF is a C# application with primary purpose of farming Steam cards from multiple accounts simultaneously. Unlike Idle Master which works only for one account at given time, while requiring Steam client running in the background and launching additional processes imitating "game playing" status, ASF doesn't require any Steam client running in the background, doesn't launch any additional processes and is made to handle unlimited Steam accounts at once. In addition to that, it's meant to be run on servers or other desktop-less machines, and features full cross-OS support, which makes it possible to launch on any operating system with .NET Core runtime, such as Windows, Linux and OS X. ASF is possible thanks to gigantic amount of work done in marvelous **[SteamKit2](https://github.com/SteamRE/SteamKit)** library.
Today, ASF is one of the most versatile Steam power tools, allowing you to make use of many features that were implemented over time. Apart from idling Steam cards, which remains the primary focus, ASF includes bunch of features on its own, such as a possibility to use it as Steam authenticator or chat logger. In addition to that, ASF includes plugin system, thanks to which anybody can further extend it to his/her needs.
Today, ASF is one of the most versatile Steam power tools, allowing you to make use of many features that were implemented over time. Apart from farming Steam cards, which remains the primary focus, ASF includes bunch of features on its own, such as a possibility to use it as Steam authenticator or chat logger. In addition to that, ASF includes plugin system, thanks to which anybody can further extend it to his/her needs.
---
### Core features
- Automatic idling of available games with card drops using any number of active accounts
- Automatic farming of available games with card drops using any number of active accounts
- No requirement of running or even having official Steam client installed
- Guarantee of being VAC-free, focus on security and privacy
- Complex error-reporting mechanism, reliability even during Steam issues and other networking quirks
- Flexible cards idling algorithm, pushing the performance to the maximum while still allowing a lot of customization
- Offline idling, enabling you to skip in-game status and stop confusing your friends with fake playing status
- Flexible cards farming algorithm, pushing the performance to the maximum while still allowing a lot of customization
- Offline farming, enabling you to skip in-game status and stop confusing your friends with fake playing status
- Advanced support for Steam accounts, including ability to redeem keys, redeem gifts, accept trades, send messages and more
- Support for latest Steam security features, including SteamGuard, SteamParental and 2-factor authentication
- Unique ASF 2FA mechanism allowing ASF to act as a mobile authenticator, if needed

2
wiki

Submodule wiki updated: 9c878c9bfa...98a9726544