Compare commits

..

848 Commits

Author SHA1 Message Date
JustArchi
f3229fa45f Implement AngleSharp.XPath breaking changes
Bump of B warranted, more in the release notes
2022-08-06 18:51:32 +02:00
ArchiBot
5c6ca3fee2 Automatic translations update 2022-08-06 02:36:13 +00:00
JustArchi
179affd49c Update Bug-report.yml 2022-08-06 01:14:23 +02:00
JustArchi
79a4638eea Bump 2022-08-05 21:37:01 +02:00
JustArchi
feede84577 Update renovate.json5 2022-08-05 21:36:35 +02:00
JustArchi
f3f71cfb27 Merge branch 'main' of https://github.com/JustArchiNET/ArchiSteamFarm 2022-08-05 21:32:21 +02:00
JustArchi
a785ae3536 Revert "Resolve AngleSharp.XPath issue"
This reverts commit 661786adf2.
2022-08-05 21:32:18 +02:00
renovate[bot]
8663bd1eb4 Update ASF-ui digest to f486cd1 2022-08-05 16:14:24 +00:00
renovate[bot]
b869df538a Update docker/build-push-action action to v3.1.1 2022-08-05 13:50:55 +00:00
renovate[bot]
e8e7d0f1cb Update crowdin/github-action action to v1.4.11 2022-08-04 23:30:41 +00:00
renovate[bot]
bea85a3014 Update ASF-ui digest to a93aaf2 2022-08-04 05:20:18 +00:00
renovate[bot]
07972e3714 Update ASF-ui digest to a03be42 2022-08-03 11:06:31 +00:00
renovate[bot]
13141f35a7 Update ASF-ui digest to 223a661 2022-08-02 13:55:45 +00:00
ArchiBot
47ace2e526 Automatic translations update 2022-07-28 02:39:35 +00:00
renovate[bot]
f44556a863 Update wiki digest to 5f4f2f9 2022-07-27 11:23:49 +00:00
ArchiBot
fa34c853b7 Automatic translations update 2022-07-27 02:46:04 +00:00
renovate[bot]
6b6c60de92 Update ASF-ui digest to 841999d 2022-07-26 04:47:20 +00:00
ArchiBot
4d5152a6ae Automatic translations update 2022-07-26 02:47:11 +00:00
renovate[bot]
d74ae770fe Update ASF-ui digest to e42cb55 2022-07-25 22:16:26 +00:00
renovate[bot]
b8d7f24d50 Update ASF-ui digest to 50009bc 2022-07-25 19:39:20 +00:00
ArchiBot
5063bda7ae Automatic translations update 2022-07-25 02:43:15 +00:00
renovate[bot]
946308366a Update crazy-max/ghaction-import-gpg action to v5.1.0 2022-07-24 19:46:56 +00:00
renovate[bot]
7e84d26d5c Update ASF-ui digest to e0613bf 2022-07-24 19:46:34 +00:00
renovate[bot]
7cae68f14a Update ASF-ui digest to 516ab2e 2022-07-24 03:54:30 +00:00
ArchiBot
f0a2c26a1a Automatic translations update 2022-07-24 02:43:39 +00:00
renovate[bot]
d146525d9c Update ASF-ui digest to cb81c98 2022-07-23 23:49:09 +00:00
Łukasz Domeradzki
0b2cdb63b2 Update SUPPORT.md 2022-07-23 21:16:39 +02:00
renovate[bot]
c7a1713066 Update ASF-ui digest to 91f8e02 2022-07-23 09:31:31 +00:00
ArchiBot
cf23819b48 Automatic translations update 2022-07-23 02:39:11 +00:00
renovate[bot]
78d5234047 Update ASF-ui digest to a21832a 2022-07-22 14:32:26 +00:00
ArchiBot
a0d7ef5856 Automatic translations update 2022-07-22 02:42:34 +00:00
JustArchi
1dc2b1e06e Misc 2022-07-21 16:50:15 +02:00
JustArchi
70fe873eaf Bump 2022-07-21 12:06:38 +02:00
JustArchi
56754053c3 Correct overlay for dockerfiles 2022-07-21 10:41:35 +02:00
JustArchi
cb883cf235 Bump 2022-07-20 22:22:37 +02:00
JustArchi
661786adf2 Resolve AngleSharp.XPath issue 2022-07-20 22:17:08 +02:00
ArchiBot
6c63b6db68 Automatic translations update 2022-07-20 02:41:09 +00:00
renovate[bot]
dfdb0a22a0 Update swashbuckle-aspnetcore monorepo to v6.4.0 2022-07-19 21:28:18 +00:00
Sebastian Göls
41ecfb1d02 Move copying of overlay files to ArchiSteamFarm.csproj (#2650)
* Move copying of overlay files to ArchiSteamFarm.csproj

* Fix build on Windows

* Try to make it more reliable

* Update ArchiSteamFarm.csproj

* Update ArchiSteamFarm.csproj

* Revert "Update ArchiSteamFarm.csproj"

This reverts commit ba41b2e3c1.

* Rename

Co-authored-by: JustArchi <JustArchi@JustArchi.net>
2022-07-19 22:20:18 +02:00
renovate[bot]
25b29cd56e Update docker/build-push-action action to v3.1.0 2022-07-19 15:59:00 +00:00
renovate[bot]
ab5fb6dd1b Update ASF-ui digest to 60a692f 2022-07-19 04:27:13 +00:00
renovate[bot]
75e4557da3 Update dependency NLog.Web.AspNetCore to v5.1.0 2022-07-18 20:41:52 +00:00
renovate[bot]
95c3658197 Update ASF-ui digest to 7ec7898 2022-07-18 14:56:43 +00:00
ArchiBot
26e7f7deb5 Automatic translations update 2022-07-18 02:43:39 +00:00
renovate[bot]
f003dcda0b Update wiki digest to 5974dbb 2022-07-17 13:04:41 +00:00
renovate[bot]
296318060f Update ASF-ui digest to 06d59c3 2022-07-17 01:05:02 +00:00
renovate[bot]
f80b114892 Update ASF-ui digest to 463b788 2022-07-16 19:05:04 +00:00
JustArchi
604652c03d Make PluginsCore public 2022-07-16 18:00:42 +02:00
renovate[bot]
2420d34b22 Update ASF-ui digest to 4e98e21 2022-07-15 19:46:13 +00:00
JustArchi
21a5793c45 Take into account that git is special snowflake 2022-07-15 21:45:18 +02:00
JustArchi
888b45c919 Include commit hash for docker builds of ASF-ui
Originally spotted at https://github.com/JustArchiNET/ASF-ui/issues/1589
2022-07-15 21:33:28 +02:00
renovate[bot]
35bf243f1a Update ASF-ui digest to c694963 2022-07-15 06:56:42 +00:00
ArchiBot
f4c1dededc Automatic translations update 2022-07-15 02:47:02 +00:00
renovate[bot]
d5f355a2bc Update actions/setup-node action to v3.4.1 2022-07-14 14:49:14 +00:00
ArchiBot
eb9b5dd025 Automatic translations update 2022-07-14 02:43:58 +00:00
renovate[bot]
2cec35f911 Update swashbuckle-aspnetcore monorepo to v6.3.2 2022-07-13 22:50:35 +00:00
renovate[bot]
7d4438b089 Update ASF-ui digest to 60d2fe0 2022-07-13 12:15:56 +00:00
ArchiBot
2782329549 Automatic translations update 2022-07-13 02:41:49 +00:00
renovate[bot]
14ac124e0c Update dotnet monorepo to v3.1.27 2022-07-12 14:35:31 +00:00
renovate[bot]
ec07d23cc4 Update ASF-ui digest to e7e7192 2022-07-12 05:32:17 +00:00
ArchiBot
3299ba8c10 Automatic translations update 2022-07-12 02:46:54 +00:00
renovate[bot]
4eb09f950d Update ASF-ui digest to c9ca06e 2022-07-11 21:20:43 +00:00
JustArchi
144a1d1574 Do not trigger ProfileUri workaround if request was for profile 2022-07-11 23:15:29 +02:00
renovate[bot]
05f3aada38 Update actions/setup-node action to v3.4.0 2022-07-11 16:08:49 +00:00
JustArchi
9240500e2c Bump 2022-07-11 18:07:03 +02:00
renovate[bot]
98768970a7 Update ASF-ui digest to b76040f 2022-07-11 05:27:44 +00:00
ArchiBot
7e41e530e7 Automatic translations update 2022-07-11 02:39:31 +00:00
renovate[bot]
97198fd435 Update ASF-ui digest to 8d93cf5 2022-07-10 11:01:21 +00:00
renovate[bot]
e8b83b8ad4 Update ASF-ui digest to 5b632ba 2022-07-10 03:47:31 +00:00
ArchiBot
929a1dfd82 Automatic translations update 2022-07-10 02:43:57 +00:00
renovate[bot]
ea7bad2868 Update ASF-ui digest to 23585b6 2022-07-09 22:06:59 +00:00
renovate[bot]
71f54a79f3 Update ASF-ui digest to 5409364 2022-07-09 13:28:26 +00:00
JustArchi
0cd7b10c9a Rewrite AWH requests public API
Breaking change that needs recompilation but doesn't need code edits, actually make maxTries work as they should and are expected to, with session refresh being controlled by a new boolean instead
2022-07-09 00:51:52 +02:00
renovate[bot]
42fb71f856 Update ASF-ui digest to 8b7e253 2022-07-08 18:49:30 +00:00
JustArchi
7b3ae25d58 Misc 2022-07-08 19:20:29 +02:00
JustArchi
2961975f05 Misc 2022-07-08 19:17:43 +02:00
JustArchi
06843ebf9f Misc 2022-07-08 19:16:58 +02:00
JustArchi
6ca395795c Move network group logic into plugins core
This will allow plugin creators to make use of network groups
2022-07-08 19:16:29 +02:00
JustArchi
0e5490cc3a Allow plugin creators to initialize their own limiters 2022-07-08 19:11:27 +02:00
renovate[bot]
39cc6e6ea6 Update crowdin/github-action action to v1.4.10 2022-07-08 15:08:45 +00:00
renovate[bot]
70544d1d76 Update ASF-ui digest to aed6536 2022-07-08 12:16:11 +00:00
ArchiBot
b1e9a53adc Automatic translations update 2022-07-08 02:42:04 +00:00
renovate[bot]
72e59e7271 Update ASF-ui digest to 64548c6 2022-07-07 23:37:04 +00:00
renovate[bot]
1ae3517374 Update ASF-ui digest to 5dd4507 2022-07-07 05:28:04 +00:00
ArchiBot
975b3b89f3 Automatic translations update 2022-07-07 02:45:33 +00:00
renovate[bot]
a982520844 Update ASF-ui digest to dafec4e 2022-07-06 14:07:46 +00:00
renovate[bot]
0ed4c7536a Update ASF-ui digest to 360b16c 2022-07-05 11:12:49 +00:00
renovate[bot]
6e360b7e1a Update ASF-ui digest to 782c965 2022-07-04 17:27:25 +00:00
renovate[bot]
17c79904e4 Update ASF-ui digest to 745af45 2022-07-04 04:05:38 +00:00
ArchiBot
0fcbc8c402 Automatic translations update 2022-07-04 02:45:38 +00:00
renovate[bot]
9e0d44bee2 Update ASF-ui digest to 1310fcd 2022-07-03 23:35:20 +00:00
renovate[bot]
a093f24a9d Update ASF-ui digest to 0709f06 2022-07-03 14:19:14 +00:00
renovate[bot]
e6a51bae55 Update ASF-ui digest to 15c0ccf 2022-07-03 05:10:36 +00:00
ArchiBot
a992d0c3cd Automatic translations update 2022-07-03 02:40:51 +00:00
JustArchi
a0e5ae8f46 Bump 2022-07-03 01:22:24 +02:00
JustArchi
c4a46fbdde Misc 2022-07-03 01:22:02 +02:00
Łukasz Domeradzki
d899dbc18c Add NLog/File endpoint (#2639)
* Add log endpoint

* Update LogController.cs

* Address netf breaking

* Fixes & feedback

* THIS IS MADNESS

* Revert "THIS IS MADNESS"

This reverts commit 8359960314.

* Solve netf madness differently
2022-07-03 01:20:43 +02:00
renovate[bot]
04e14293ef Update wiki digest to 55fc787 2022-07-02 18:37:09 +00:00
renovate[bot]
18a1b0a883 Update ASF-ui digest to bcd2d66 2022-07-02 10:09:30 +00:00
ArchiBot
5ca028ef47 Automatic translations update 2022-07-02 02:40:27 +00:00
renovate[bot]
43ec8f9566 Update wiki digest to d78047f 2022-07-01 23:27:03 +00:00
JustArchi
32f5b3a1c5 Bump 2022-07-02 00:37:42 +02:00
JustArchi
196afbf276 Merge branch 'fix' 2022-07-02 00:32:45 +02:00
JustArchi
1f0e4c9058 Bump 2022-07-02 00:32:31 +02:00
JustArchi
0ded9698b2 Fix custom game name not being displayed at all
In original change I totally forgot custom game ACTUALLY must be the first on the list, otherwise it never works properly.

So use exactly the same logic, but with custom name being first (if possible to fit)
2022-07-02 00:16:26 +02:00
renovate[bot]
9825f007c0 Update ASF-ui digest to 319696a 2022-07-01 15:40:47 +00:00
JustArchi
bc38ba478d Fix Archi brain damage 2022-07-01 13:29:47 +02:00
JustArchi
2f22757fea Be more optimistic about session checks
We can't check for session with every request, allow at least 10 seconds of optimistic assumption as otherwise we're spamming the servers too much
2022-07-01 13:15:45 +02:00
JustArchi
3ff0468926 Avoid excessive 2FA delays when waitIfNeeded without specifying IDs 2022-07-01 13:13:39 +02:00
ArchiBot
a6a973468c Automatic translations update 2022-07-01 02:47:18 +00:00
renovate[bot]
4aa1604dfb Update ASF-ui digest to 8c7498a 2022-06-30 05:49:32 +00:00
ArchiBot
44c7fcd131 Automatic translations update 2022-06-30 02:41:51 +00:00
ArchiBot
ce610ab24d Automatic translations update 2022-06-29 02:42:10 +00:00
renovate[bot]
c76f17c5c7 Update ASF-ui digest to 955afdf 2022-06-28 06:05:35 +00:00
renovate[bot]
30b4e006dc Update ASF-ui digest to e519f42 2022-06-28 00:48:08 +00:00
renovate[bot]
39621ed46e Update ASF-ui digest to 70fe63b 2022-06-27 17:45:00 +00:00
JustArchi
e532b57369 Thanks netf 2022-06-27 14:32:50 +02:00
JustArchi
b117c5164d Misc 2022-06-27 14:17:38 +02:00
JustArchi
be5a6bc27a Misc 2022-06-27 12:28:06 +02:00
ArchiBot
0ecb04e62c Automatic translations update 2022-06-27 02:40:39 +00:00
renovate[bot]
0af9f99923 Update ASF-ui digest to 0992da0 2022-06-26 05:13:28 +00:00
ArchiBot
cd0078e83e Automatic translations update 2022-06-26 02:42:45 +00:00
renovate[bot]
10cedad0ee Update ASF-ui digest to a116352 2022-06-25 05:12:08 +00:00
ArchiBot
693f4edbe5 Automatic translations update 2022-06-25 02:41:53 +00:00
renovate[bot]
ed44ad030e Update ASF-ui digest to 3b53132 2022-06-24 22:04:31 +00:00
JustArchi
d338477e5c Very important CatAPI fixes 2022-06-24 23:19:31 +02:00
renovate[bot]
053cb5fc03 Update wiki digest to be4f1cc 2022-06-24 15:50:12 +00:00
renovate[bot]
a01ac6641e Update ASF-ui digest to e5cf327 2022-06-24 12:59:57 +00:00
renovate[bot]
3deb560e5e Update ASF-ui digest to b11aacc 2022-06-23 21:45:29 +00:00
JustArchi
1861add350 Bump 2022-06-23 10:45:51 +02:00
JustArchi
83fac5b115 Misc 2022-06-22 20:52:41 +02:00
ArchiBot
23e49dafbc Automatic translations update 2022-06-22 02:40:09 +00:00
JustArchi
b71462b151 Merge branch 'il-warnings' 2022-06-21 11:10:26 +02:00
ArchiBot
88d3b19196 Automatic translations update 2022-06-21 02:37:14 +00:00
JustArchi
237f23e965 Attempt to silence excessive IL warnings 2022-06-20 21:37:03 +02:00
JustArchi
776755d3ab Change GamesPlayedWhileIdle to ImmutableList
Similar to FarmingOrders, the order of elements might be relevant in regards to games displayed e.g. in recently played.
2022-06-20 21:08:24 +02:00
JustArchi
e1e464b4e7 Misc 2022-06-20 20:33:33 +02:00
JustArchi
d590a30f20 Do not retry on empty HTTP content
This is totally valid for handling errors, and 200 OK responses should always carry one (even if empty)
2022-06-19 21:42:22 +02:00
JustArchi
1d520d9071 Update Madness, solving recent CI failure 2022-06-19 19:19:50 +02:00
JustArchi
772607b680 Use recommended async dispose pattern
Funny enough, non-breaking API changes since all of those classes are sealed.
2022-06-19 18:24:52 +02:00
ArchiBot
82750352e2 Automatic translations update 2022-06-19 02:43:14 +00:00
JustArchi
3e5a6a7b32 Bump 2022-06-18 14:15:57 +02:00
JustArchi
5f803cf725 Use Uri.UnescapeDataString() instead of WebUtility.HtmlDecode() 2022-06-18 14:15:14 +02:00
ArchiBot
ffdc0e89e8 Automatic translations update 2022-06-18 02:36:44 +00:00
Renovate Bot
b608083bfc Update ASF-ui digest to 9927878 2022-06-17 00:06:28 +00:00
Sebastian Göls
ebdb17d71b Add latest sale id to blacklist (#2621) 2022-06-16 16:49:54 +02:00
ArchiBot
8877468bd4 Automatic translations update 2022-06-16 02:37:58 +00:00
JustArchi
c753ed24cd CAN WE STOP ALREADY 2022-06-15 19:16:25 +02:00
JustArchi
dcebde55a2 Make xpoo happy 2022-06-15 19:09:03 +02:00
JustArchi
bd0f1779d6 Do not emit new game added event without a clear need
This callback is emitted by Steam with new licenses available, but also in many other situations, such as when somebody else logs in into the account (e.g. Steam deck, another PC), when somebody deletes licenses, and even on PICS changes from time to time, not to mention other cases I forgot/don't know about.

It seems silly to restart cards farmer every time, I believe we can assume that user won't delete license for a game we're farming, and even if he does, we'll catch that in 30 minutes or so anyway, while not restarting the farming every time Steam feels like sending us licenses.
2022-06-15 18:50:32 +02:00
JustArchi
164d9330f0 Do not emit shutdown event when changing the config 2022-06-15 18:44:07 +02:00
JustArchi
7c1c0d61b4 Misc 2022-06-15 16:23:00 +02:00
JustArchi
2aab79ec52 Bump 2022-06-15 16:13:33 +02:00
JustArchi
917df358e8 Fix logging module corruption on IPC startup failure
Failure of IPC startup currently corrupts ASF logging mechanism, and since ASF expects that logging mechanism to be operative afterwards, it crashes in internal code the moment it requires user input.

Fix that, and add additional safety safeguards in case we have a legit lack of logging configuration.

https://steamcommunity.com/groups/archiasf/discussions/1/3422187248450123255/
2022-06-15 16:08:03 +02:00
ArchiBot
5b62e19a80 Automatic translations update 2022-06-15 02:42:06 +00:00
Renovate Bot
cb99c916dd Update dotnet monorepo to v3.1.26 2022-06-15 02:29:47 +00:00
Renovate Bot
2596c74d2e Update wiki digest to 6209fe6 2022-06-14 21:00:30 +00:00
Renovate Bot
8147dacae7 Update ASF-ui digest to 218535a 2022-06-13 18:20:10 +00:00
ArchiBot
3ad324ebea Automatic translations update 2022-06-13 02:38:24 +00:00
ArchiBot
6e34c14aef Automatic translations update 2022-06-12 02:38:48 +00:00
Renovate Bot
96c9ab34d6 Update wiki digest to d4b1e4a 2022-06-11 19:26:00 +00:00
Renovate Bot
fa6649305e Update ASF-ui digest to 34b05c2 2022-06-11 17:06:36 +00:00
JustArchi
c0a3b67ebf Bump 2022-06-11 18:54:49 +02:00
JustArchi
2a845ab46f Of course I had to forgot about something 2022-06-11 18:52:17 +02:00
JustArchi
5ca6e41691 Bump 2022-06-11 18:17:59 +02:00
JustArchi
6eb9b9b26d Misc 2022-06-11 18:15:46 +02:00
JustArchi
95a6cef6db Take into account trade hold durations from both sides
Previous implementations had several problems when dealing with trade holds:
- User could've configured max trade hold duration to 0, and still accept trades with hold when he's the cause of it, this is unwanted
- There is virtually no way to ensure that the other party is willing to accept our trade hold even if we allow it, so expose that detail.

This precisely answers quite rather but not impossible situation of having a trade hold with ASF 2FA, e.g. due to moving authenticator, but we're also now ready for "perpetual" trade hold with ASF 2FA, in case it ever happened.

This way, we:
- Expose to other users our max trade hold duration preference
- Users validate both their own and listed user's trade hold, and if it exceeds either their own or user's limit, bot is not considered for matching
- It also resolves problem of accepting trade offers from other people when we're on trade hold ourselves
2022-06-11 18:07:06 +02:00
JustArchi
688e1cea83 Misc style updates 2022-06-11 16:14:51 +02:00
Renovate Bot
40a479b1df Update ASF-ui digest to 574bfd6 2022-06-09 17:29:38 +00:00
ArchiBot
b535886959 Automatic translations update 2022-06-09 02:36:47 +00:00
Renovate Bot
45d0a8a9c1 Update ASF-ui digest to d22e99c 2022-06-08 05:23:14 +00:00
JustArchi
3ee2ded814 Closes #2599 2022-06-07 20:46:41 +02:00
Renovate Bot
a2b5f80f40 Update ASF-ui digest to 99609fd 2022-06-07 04:34:37 +00:00
ArchiBot
0fffbdaa52 Automatic translations update 2022-06-07 02:32:27 +00:00
JustArchi
2ec764f8ec Rewrite WebBrowser errors handling 2022-06-06 23:28:35 +02:00
JustArchi
c67aecacbc Misc 2022-06-06 23:27:22 +02:00
JustArchi
ae5b9cdc0d Merge branch 'main' of https://github.com/JustArchiNET/ArchiSteamFarm 2022-06-06 21:54:08 +02:00
JustArchi
0eab63bab5 Do not check against profile uri in AWH when caller wants redirections
It's no longer our responsibility
2022-06-06 21:54:04 +02:00
Renovate Bot
1aea4c0550 Update actions/setup-node action to v3.3.0 2022-06-06 16:12:35 +00:00
JustArchi
16c4bed95f Bump 2022-06-06 13:30:00 +02:00
JustArchi
8948817d55 Misc 2FA enhancements 2022-06-06 13:29:21 +02:00
JustArchi
6ff943aeaa Move timezoneOffset cookie back into session
It might still make sense in order to:
a) Postpone timezoneoffset calculation until we actually get logged in, which gives machine more time for re-initialization
b) Refresh it in case our timezone changes, or DST gets enabled/disabled
2022-06-06 13:23:28 +02:00
Renovate Bot
0576bbd3aa Update ASF-ui digest to 8ba8738 2022-06-06 02:41:44 +00:00
ArchiBot
6e70956ee9 Automatic translations update 2022-06-06 02:36:02 +00:00
Renovate Bot
77409699f0 Update wiki digest to 6647581 2022-06-05 18:08:58 +00:00
Sebastian Göls
dae3e93031 Make use of "or" keyword in switch statements (#2598) 2022-06-05 15:59:34 +02:00
JustArchi
3041850b92 Bump 2022-06-05 12:46:24 +02:00
JustArchi
7c00d8d03d Misc 2FA enhancements 2022-06-05 12:39:04 +02:00
JustArchi
aedad9d5b3 Closes #2596 2022-06-05 12:15:42 +02:00
JustArchi
ff7f661197 Misc 2022-06-04 22:46:37 +02:00
JustArchi
e57cc21b89 Update .gitignore 2022-06-04 22:06:01 +02:00
JustArchi
06bfe01087 Misc 2022-06-04 21:41:07 +02:00
JustArchi
bcceb0c39c Downgrade AngleSharp.XPath due to https://github.com/AngleSharp/AngleSharp.XPath/issues/36 2022-06-04 21:41:02 +02:00
Renovate Bot
996ee66554 Update ASF-ui digest to 6dcd14d 2022-06-04 09:57:02 +00:00
renovate[bot]
dad19956aa Update dependency AngleSharp.XPath to v2 (#2595)
* Update dependency AngleSharp.XPath to v2

* Update GitHub.cs

* netf fixes

Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: JustArchi <JustArchi@JustArchi.net>
2022-06-04 11:56:36 +02:00
Renovate Bot
beeda2777d Update ASF-ui digest to 293db61 2022-06-03 06:16:50 +00:00
ArchiBot
53e06a7392 Automatic translations update 2022-06-03 02:32:08 +00:00
Renovate Bot
9a1d4913a0 Update ASF-ui digest to a53bdb7 2022-06-02 18:43:12 +00:00
ArchiBot
c65b40b45b Automatic translations update 2022-06-02 02:39:09 +00:00
Renovate Bot
23647f2e39 Update wiki digest to d48f2bf 2022-06-01 22:07:32 +00:00
JustArchi
feb7a72bd1 Bump 2022-06-01 21:14:13 +02:00
Łukasz Domeradzki
7fe5989f5d Rewrite Steam time to ulongs (#2594)
My latest "research" resulted in learning that under the hood, Steam unix time seconds is bullet-proof not only for year 2038 but for uint range as well. While I do not expect to be alive by 2106, let alone ASF still being operative, it makes sense to base our time on the correct backend implementation regardless.

Small breaking change for people using `GetUnixTime()`.
2022-06-01 21:13:50 +02:00
Łukasz Domeradzki
715ed034df Unify WebBrowser API in regards to nullable bodies (#2593)
* Unify logic for nullable bodies

* Update ArchiWebHandler.cs

* Misc
2022-06-01 21:13:40 +02:00
JustArchi
d82df0074f Bump 2022-06-01 16:47:24 +02:00
JustArchi
03c2ba049e Fix NLog
Without this, default ASP.NET console logger is still active, even if we don't want it
2022-06-01 16:45:53 +02:00
Renovate Bot
03bce5dd71 Update ASF-ui digest to 5c7d999 2022-05-31 22:28:53 +00:00
ArchiBot
023e38d5e0 Automatic translations update 2022-05-29 02:39:26 +00:00
JustArchi
6178b12bb1 Fix invalid STD retry on 429 without json body
It's getting more and more complicated... We have places where we accept errors but still want relevant JSON body (most of the Steam error-places), and now we also have a place where we expected error to not carry one. Moreover, we still want to account for invalid JSON body on 2xx and retry on them.

So let's make the code even more complicated than it already is by adding yet another endpoint that does exactly the same what the other endpoint does BUT allows us us to optionally accept null/invalid body on success/errors/both, lol. I hate myself.

Maybe we can obsolete the first endpoint eventually and stick with just the second?
2022-05-28 20:41:52 +02:00
ArchiBot
df95b82b10 Automatic translations update 2022-05-28 02:29:19 +00:00
Renovate Bot
93ac0b4e4a Update ASF-ui digest to c42a9b8 2022-05-27 04:03:04 +00:00
ArchiBot
38fc3ba6a3 Automatic translations update 2022-05-27 02:39:00 +00:00
Renovate Bot
2e87b78b45 Update wiki digest to c5694c9 2022-05-26 17:44:51 +00:00
Renovate Bot
dae256f069 Update ASF-ui digest to fa42805 2022-05-26 15:10:16 +00:00
renovate[bot]
8452c46c47 Update crazy-max/ghaction-import-gpg action to v5 (#2592)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-05-26 17:09:43 +02:00
Sebastian Göls
68e30b43c2 Use ArgumentNullException.ThrowIfNull when possible (#2591)
* Misc.

* Fix mistake
2022-05-26 13:29:12 +02:00
JustArchi
c9b1e46013 Avoid crash when executing STD command with disabled STD plugin 2022-05-26 11:47:11 +02:00
JustArchi
fd9770d78e Bump 2022-05-26 11:05:59 +02:00
Renovate Bot
a4374389b8 Update dependency ConfigureAwaitChecker.Analyzer to v5.0.0.1 2022-05-26 03:12:12 +00:00
ArchiBot
082cab42df Automatic translations update 2022-05-26 02:38:09 +00:00
Renovate Bot
3ff80b37f3 Update ASF-ui digest to 1bf5b24 2022-05-25 20:21:25 +00:00
JustArchi
07c354f9e7 Commit the most misc optimization in history
I found it accidentally, lol
2022-05-25 20:04:54 +02:00
Sebastian Göls
b83f8fc669 Update Program.cs (#2589) 2022-05-25 17:18:06 +02:00
Renovate Bot
d6a2f53ab0 Update wiki digest to f26dbc5 2022-05-25 11:51:00 +00:00
JustArchi
0261623ea9 Expose FinalUri in BasicResponse for plugins usage 2022-05-24 12:25:43 +02:00
JustArchi
b5ca484c2b Add ReturnRedirections for plugins usage
This will allow caller to handle redirections manually
2022-05-24 12:13:54 +02:00
Renovate Bot
a7c30e4878 Update ASF-ui digest to a790c6c 2022-05-22 03:51:41 +00:00
Renovate Bot
f26a4ae864 Update ASF-ui digest to 24ff55f 2022-05-21 05:09:18 +00:00
Renovate Bot
c2018b53a5 Update actions/upload-artifact action to v3.1.0 2022-05-20 20:28:10 +00:00
JustArchi
55421bb29f Throw on lack of previousMethodName
I don't believe we should support those calls for anything that doesn't supply it. Actually add another layer of safeguards.
2022-05-20 21:36:22 +02:00
Renovate Bot
52eabe4daf Update ASF-ui digest to 46fe13d 2022-05-20 05:16:48 +00:00
ArchiBot
86fc8a765c Automatic translations update 2022-05-20 02:34:05 +00:00
JustArchi
9162752b99 Misc 2022-05-19 21:38:40 +02:00
JustArchi
4436e8dc43 Add STD command trigger for STD plugin 2022-05-19 21:34:57 +02:00
JustArchi
1f0b996cf5 Improve login procedure
- Allow user to recover from SteamGuard/2FA failures when inputting manually
- Unify login failures in a single mechanism
- Add fallback for Steam informing us about lack of 2FA code when actually having mobile authenticator and supplying it (ultra rare screwup)
2022-05-19 15:33:53 +02:00
ArchiBot
4dc7acb914 Automatic translations update 2022-05-19 02:39:10 +00:00
Renovate Bot
d570a17532 Update wiki digest to 6958aab 2022-05-18 14:59:10 +00:00
Renovate Bot
e5184adede Update ASF-ui digest to 790ff09 2022-05-18 14:58:40 +00:00
JustArchi
13a5fa7c02 Misc 2022-05-18 11:45:31 +02:00
JustArchi
c698fe7b07 Bump 2022-05-18 11:31:49 +02:00
JustArchi
a08c85e40b Increase interactive console responsiveness, misc
Previously we were sleeping always, also after execution of the command and after wrong keys being pressed, which resulted in excessive delay if user mashed his keyboard like a madman before hitting c to enter the console, entirely unnecessarily.
2022-05-18 11:30:46 +02:00
JustArchi
055af32219 Address missed NLog breaking change 2022-05-18 11:20:35 +02:00
ArchiBot
080b500ebf Automatic translations update 2022-05-18 02:33:30 +00:00
JustArchi
4a329b0b15 Bump 2022-05-17 23:03:42 +02:00
JustArchi
e2494960ae Bump 2022-05-17 23:02:14 +02:00
JustArchi
2e987ccee6 Address NLog changes 2022-05-17 20:12:57 +02:00
renovate[bot]
99284e22c9 Update dependency NLog.Web.AspNetCore to v5 (#2579)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-05-17 20:09:34 +02:00
renovate[bot]
37eac5844e Update ASF-ui digest to 664643e (#2578)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-05-17 11:41:52 +02:00
Renovate Bot
35f295a860 Update ASF-ui digest to 8d0abfb 2022-05-16 19:07:27 +00:00
Renovate Bot
29d047271e Update actions/setup-node action to v3.2.0 2022-05-16 14:03:52 +00:00
Renovate Bot
948a86bfc9 Update ASF-ui digest to 2fe4a70 2022-05-16 04:50:08 +00:00
ArchiBot
f853c61821 Automatic translations update 2022-05-16 02:25:24 +00:00
JustArchi
5b1cb16c98 Stop wasting CPU cycles! 2022-05-13 19:33:08 +02:00
JustArchi
bdac1b2782 Bump 2022-05-13 18:22:24 +02:00
JustArchi
7532b89fd0 Closes #2572
At least the code is now shorter, lol
2022-05-13 18:15:15 +02:00
JustArchi
7cd351d1cd Misc
This applies only to Debug builds, as Release ones don't use checked arithmetic anyway
2022-05-13 18:12:31 +02:00
Renovate Bot
9c8d63318e Update ASF-ui digest to bb59242 2022-05-13 05:08:45 +00:00
ArchiBot
f0c0e07489 Automatic translations update 2022-05-13 02:42:57 +00:00
ArchiBot
d2e79ff3a4 Automatic translations update 2022-05-12 02:33:43 +00:00
Renovate Bot
ab7b998e3b Update dependency Microsoft.NET.Test.Sdk to v17.2.0 2022-05-11 21:19:10 +00:00
Łukasz Domeradzki
9c88d14c8e Resolve NU1507 (#2575)
* Attempt to resolve NU1507

* Let's try this then

* Revert "Let's try this then"

This reverts commit 86ef6f9abf.

* How about this

* And this?

* So why not this?

* And this?

* Revert "And this?"

This reverts commit e43fc83dcc.

* Revert "So why not this?"

This reverts commit e630dd8365.
2022-05-11 20:11:58 +02:00
ArchiBot
9a3c3bdbaf Automatic translations update 2022-05-11 02:35:21 +00:00
Renovate Bot
7b3598af20 Update dotnet monorepo to v3.1.25 2022-05-10 19:23:40 +00:00
Renovate Bot
35d2156855 Update ASF-ui digest to b22bef8 2022-05-10 16:40:51 +00:00
JustArchi
d589da7a39 Bump 2022-05-10 11:14:11 +02:00
JustArchi
c10de94bd0 Closes #2571 2022-05-10 11:11:45 +02:00
Renovate Bot
d164296d7e Update actions/setup-dotnet action to v2.1.0 2022-05-09 10:21:58 +00:00
Renovate Bot
b378a76072 Update ASF-ui digest to 0ed48ea 2022-05-08 22:02:09 +00:00
Archi
263a2db476 CI: Remove excessive continue on errors 2022-05-08 23:01:45 +02:00
Renovate Bot
61549fc983 Update ASF-ui digest to 1f02912 2022-05-07 03:46:37 +00:00
ArchiBot
19aad04143 Automatic translations update 2022-05-07 02:24:04 +00:00
Renovate Bot
5a5f3c6786 Update ASF-ui digest to a9ddf4a 2022-05-06 22:30:53 +00:00
Renovate Bot
bd68df2fd6 Update crowdin/github-action action to v1.4.9 2022-05-06 16:18:34 +00:00
renovate[bot]
2a97644468 Update docker/build-push-action action to v3 (#2565)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-05-06 10:20:55 +02:00
renovate[bot]
5304ca3e07 Update docker/login-action action to v2 (#2566)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-05-06 10:20:50 +02:00
renovate[bot]
f0471ac0eb Update docker/setup-buildx-action action to v2 (#2567)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-05-06 10:20:42 +02:00
ArchiBot
b0e7f1963c Automatic translations update 2022-05-06 02:27:24 +00:00
Renovate Bot
78990e8aff Update wiki digest to b43bf17 2022-05-05 13:03:59 +00:00
Renovate Bot
b871970d85 Update ASF-ui digest to 722f6b2 2022-05-05 00:18:06 +00:00
ArchiBot
d563a20288 Automatic translations update 2022-05-04 02:36:40 +00:00
Renovate Bot
b2fefa4476 Update wiki digest to 37d8dcc 2022-05-03 11:51:02 +00:00
ArchiBot
840cf25ea4 Automatic translations update 2022-04-30 02:36:10 +00:00
Renovate Bot
79587f68d7 Update ASF-ui digest to a351077 2022-04-30 00:09:30 +00:00
Archi
45adf9c1a1 Bump 2022-04-29 20:25:59 +02:00
Archi
1dcf98c849 Fix SteamPassword input
Asking for password with encryption enabled always resulted in an error, as the password wasn't properly set to the plaintext and we were back to square one.

The previous logic was overly complex, I don't know why, this should achieve the same and be much easier to understand while at it.
2022-04-29 17:55:33 +02:00
ArchiBot
a826b7f9b7 Automatic translations update 2022-04-29 02:37:09 +00:00
Renovate Bot
85c8397cf7 Update docker/setup-buildx-action action to v1.7.0 2022-04-28 11:01:51 +00:00
ArchiBot
dbf1c1ba51 Automatic translations update 2022-04-28 02:50:54 +00:00
Renovate Bot
359439e306 Update ASF-ui digest to 892742c 2022-04-27 13:48:33 +00:00
ArchiBot
763766e092 Automatic translations update 2022-04-27 02:42:35 +00:00
Renovate Bot
a4a347e957 Update mstest monorepo to v2.2.10 2022-04-26 22:17:23 +00:00
Renovate Bot
844ca93647 Update wiki digest to 0c633a3 2022-04-26 18:49:18 +00:00
Renovate Bot
16fe445ea9 Update ASF-ui digest to 83c8a83 2022-04-26 18:48:48 +00:00
Renovate Bot
34bf8fb84f Update crazy-max/ghaction-import-gpg action to v4.4.0 2022-04-25 14:36:52 +00:00
Renovate Bot
4873cd337a Update ASF-ui digest to 5554b10 2022-04-25 03:07:32 +00:00
ArchiBot
8c0249a62d Automatic translations update 2022-04-25 02:33:43 +00:00
ArchiBot
220ecf0c38 Automatic translations update 2022-04-24 02:24:03 +00:00
Renovate Bot
ebd79425f4 Update wiki digest to 370d1b3 2022-04-23 18:48:11 +00:00
Renovate Bot
2eaf934dde Update dependency Markdig.Signed to v0.30.2 2022-04-23 14:22:52 +00:00
Archi
599cd9bff8 Bump 2022-04-23 15:02:37 +02:00
Archi
339e83a818 Use saner custom schema IDs in swagger 2022-04-23 14:58:22 +02:00
Archi
f083bb2d3b Closes #2558 2022-04-23 13:11:10 +02:00
Archi
9f68d17a28 Add initialization for Madness
This is probably not needed, but might come useful in the future.
2022-04-23 13:09:51 +02:00
Renovate Bot
d8413f9633 Update ASF-ui digest to 5a6f341 2022-04-23 04:05:53 +00:00
ArchiBot
27d9d61309 Automatic translations update 2022-04-23 02:24:12 +00:00
Renovate Bot
2fc92ce427 Update dependency JustArchiNET.Madness to v3.5.2 2022-04-22 16:54:52 +00:00
Renovate Bot
86d94a7bbe Update swashbuckle-aspnetcore monorepo to v6.3.1 2022-04-22 14:44:55 +00:00
Renovate Bot
9647db8bf7 Update dependency Markdig.Signed to v0.30.1 2022-04-22 11:13:08 +00:00
Renovate Bot
2ce5018d62 Update ASF-ui digest to f23f502 2022-04-22 05:37:01 +00:00
ArchiBot
61768dbeb9 Automatic translations update 2022-04-22 02:41:44 +00:00
Renovate Bot
37ced5d4e3 Update dependency Markdig.Signed to v0.30.0 2022-04-21 19:21:48 +00:00
Renovate Bot
c1a695de7b Update actions/checkout action to v3.0.2 2022-04-21 16:13:25 +00:00
Renovate Bot
7040bdabf6 Update ASF-ui digest to e1d754d 2022-04-21 12:28:06 +00:00
ArchiBot
14512aec71 Automatic translations update 2022-04-21 02:36:31 +00:00
Renovate Bot
40d67ac185 Update dependency Markdig.Signed to v0.29.0 2022-04-20 18:53:26 +00:00
Renovate Bot
7de67e84f9 Update ASF-ui digest to 992113b 2022-04-20 04:04:09 +00:00
Renovate Bot
6c9df75a1a Update ASF-ui digest to 18c96be 2022-04-19 14:39:57 +00:00
ArchiBot
b98d8405e1 Automatic translations update 2022-04-19 02:37:37 +00:00
Archi
062b241232 Misc 2022-04-18 21:08:30 +02:00
Archi
b5af510eb9 Improve weak passwords reasons 2022-04-18 20:35:47 +02:00
renovate[bot]
2edcb7a0ce Update dependency JetBrains.Annotations to v2022 (#2557)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-04-18 19:56:26 +02:00
ArchiBot
58cf93ff48 Automatic translations update 2022-04-18 02:35:31 +00:00
Renovate Bot
9f0f3339c5 Update wiki digest to d965790 2022-04-17 12:56:19 +00:00
ArchiBot
6b88d49067 Automatic translations update 2022-04-17 02:23:50 +00:00
ArchiBot
b4e102682f Automatic translations update 2022-04-16 02:23:14 +00:00
Renovate Bot
a580c85234 Update wiki digest to 55b7332 2022-04-15 21:17:18 +00:00
Renovate Bot
a3cce515ec Update ASF-ui digest to e0d5a20 2022-04-15 18:48:42 +00:00
Archi
1f21c1f9f6 Bump 2022-04-15 19:41:18 +02:00
Archi
9c7014d5c1 Misc 2022-04-15 19:38:56 +02:00
Archi
0a01dfa22b Misc 2022-04-15 19:37:18 +02:00
qhy040404
a8bb107e23 Update README.md (#2555) 2022-04-15 19:24:07 +02:00
Renovate Bot
151f6cfe4a Update actions/checkout action to v3.0.1 2022-04-14 20:13:59 +00:00
Renovate Bot
d21e398ac0 Update crowdin/github-action action to v1.4.8 2022-04-14 03:10:04 +00:00
ArchiBot
16f7f82dc0 Automatic translations update 2022-04-14 02:28:46 +00:00
Renovate Bot
8a6a02e034 Update ASF-ui digest to ffa260b 2022-04-13 22:08:23 +00:00
Sebastian Göls
b8bfcd5df3 Simplify LogNullError calls (#2554)
* Remove necessity of nameof(...) in calls to ArchiLogger.LogNullError(...)

* Upgrade Madness

* Upgrade Madness

* Split up compound null log statements
2022-04-13 23:16:36 +02:00
Sebastian Göls
2326196e01 Slightly deduplicate utility code (#2553) 2022-04-13 21:44:57 +02:00
ArchiBot
380d785388 Automatic translations update 2022-04-13 02:29:12 +00:00
Renovate Bot
edd82b365c Update wiki digest to ced81fb 2022-04-12 19:39:24 +00:00
Renovate Bot
d49d106d64 Update dotnet monorepo to v3.1.24 2022-04-12 00:05:28 +00:00
Archi
78407fbd9c Remove obsolete statistics mapping
This already skipped one monthly cycle, everybody who remotely cares to use up-to-date ASF is already migrated to proper config, everybody else is not making use of it either way
2022-04-12 00:52:19 +02:00
Archi
1a87149765 Correct doc 2022-04-12 00:48:24 +02:00
Renovate Bot
f260015098 Update ASF-ui digest to 9bc1e73 2022-04-11 19:13:41 +00:00
Renovate Bot
5016abe45e Update actions/setup-node action to v3.1.1 2022-04-11 14:10:40 +00:00
Renovate Bot
2238897f37 Update ASF-ui digest to 02f5fc8 2022-04-11 03:47:19 +00:00
ArchiBot
493f40a97c Automatic translations update 2022-04-11 02:29:56 +00:00
Renovate Bot
5ec7ca050b Update ASF-ui digest to b16b9fa 2022-04-09 01:36:03 +00:00
ArchiBot
f727403295 Automatic translations update 2022-04-08 02:26:07 +00:00
Renovate Bot
027d23d894 Update ASF-ui digest to 8df1cd1 2022-04-07 20:37:03 +00:00
Renovate Bot
f36798b2c3 Update ASF-ui digest to 082828a 2022-04-07 11:00:19 +00:00
ArchiBot
202a92f66f Automatic translations update 2022-04-07 02:23:49 +00:00
Renovate Bot
48b2a4c859 Update wiki digest to 309c165 2022-04-06 22:51:35 +00:00
Renovate Bot
cfbc3d749f Update ASF-ui digest to 49413ea 2022-04-06 21:06:44 +00:00
Archi
a76af71227 Update RELEASE_TEMPLATE.md 2022-04-06 20:49:17 +02:00
Archi
cc5e5dfcc9 Bump 2022-04-06 20:18:01 +02:00
Archi
a185f2f03d Fix steam parental mess 2022-04-06 20:12:01 +02:00
Archi
3772b303c5 Bump 2022-04-06 17:09:02 +02:00
Archi
d6ed6e81a4 Let's try declaring base Chinese instead 2022-04-06 15:43:13 +02:00
Archi
0bbc85527a Allow cmdline arg for forbidding Steam parental generation 2022-04-06 14:16:26 +02:00
Archi
1eabe3a5ed Fix netf brain damage 2022-04-06 14:05:10 +02:00
Archi
f95b6bf089 Refuse to accept SteamParentalCode other than 4 0-9 digits 2022-04-06 13:58:35 +02:00
Renovate Bot
ff7b4582c7 Update ASF-ui digest to 498c8de 2022-04-06 00:07:06 +00:00
Renovate Bot
7e7fb9cd16 Update ASF-ui digest to 8f5ce0b 2022-04-05 19:39:26 +00:00
Archi
65bbaf628e Bump 2022-04-05 20:28:49 +02:00
Archi
db2cbde708 Bump 2022-04-05 20:28:30 +02:00
ArchiBot
b701acf72f Automatic translations update 2022-04-05 02:23:19 +00:00
Łukasz Domeradzki
ceb021dbdf Use windows-latest runner again (#2547)
* Attempt at resolving https://github.com/actions/virtual-environments/issues/5189

* Clean up dockerfiles from no longer required workarounds
2022-04-04 22:06:03 +02:00
ArchiBot
ce1c77780d Automatic translations update 2022-04-04 02:29:49 +00:00
Renovate Bot
5b7858c2a0 Update wiki digest to a05a01e 2022-04-03 19:13:33 +00:00
Archi
635afa7165 Update Madness 2022-04-02 16:41:48 +02:00
Archi
c79c314b20 Fix generic-netf
Again and again!
2022-04-02 16:08:41 +02:00
Archi
ec78ad1ac2 Add overflow-related fixes and improvements 2022-04-02 16:00:07 +02:00
Archi
9273d73640 Check for overflow and underflow in debug builds 2022-04-02 13:58:55 +02:00
Renovate Bot
d598b99a1e Update ASF-ui digest to ba9a208 2022-04-02 05:05:07 +00:00
Renovate Bot
f12b00bb4c Update ASF-ui digest to c7e3eee 2022-04-01 20:10:36 +00:00
Renovate Bot
ff7116d2ac Update actions/setup-node action to v3.1.0 2022-04-01 11:47:41 +00:00
Renovate Bot
ee435fc628 Update ASF-ui digest to d282988 2022-03-31 06:34:58 +00:00
Renovate Bot
c8f02779b6 Update ASF-ui digest to a78474f 2022-03-30 03:27:46 +00:00
ArchiBot
83667ed7c3 Automatic translations update 2022-03-30 02:50:37 +00:00
Archi
387ef3e0dd Update revolut handle 2022-03-30 00:41:27 +02:00
ArchiBot
7e8c6b7fb3 Automatic translations update 2022-03-29 02:24:49 +00:00
Renovate Bot
e97b440225 Update wiki digest to a9d72e4 2022-03-28 22:59:44 +00:00
ArchiBot
b1db99f328 Automatic translations update 2022-03-28 02:25:26 +00:00
Renovate Bot
ace0ca4555 Update dependency Markdig.Signed to v0.28.1 2022-03-27 09:46:26 +00:00
ArchiBot
33767ace78 Automatic translations update 2022-03-27 02:21:44 +00:00
Renovate Bot
5d5a76de40 Update ASF-ui digest to 711c0f2 2022-03-26 03:19:31 +00:00
ArchiBot
582680f69d Automatic translations update 2022-03-26 02:21:11 +00:00
ArchiBot
e65729ee1a Automatic translations update 2022-03-25 02:21:29 +00:00
Archi
83a353dfe0 Bump 2022-03-24 22:28:40 +01:00
ArchiBot
aea7c7640c Automatic translations update 2022-03-24 02:22:40 +00:00
Łukasz Domeradzki
7118185ac5 Remove support for IPC localization (#2545) 2022-03-24 01:47:12 +01:00
Renovate Bot
b681b74ee6 Update ASF-ui digest to 871721c 2022-03-22 20:45:48 +00:00
Archi
cc83222c3e Use shorter syntax for json properties 2022-03-22 16:33:47 +01:00
Archi
6c570738ee Fix handling inventory loading errors
"success" doesn't have to exist as a property in error json
2022-03-22 14:57:48 +01:00
Archi
393ddf6f10 Misc optimization 2022-03-22 12:27:07 +01:00
renovate[bot]
75a7df2751 Update peter-evans/dockerhub-description action to v3 (#2542)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-03-22 11:37:59 +01:00
Archi
0c3dfaa4ae Use generic delay for all unhandled disconnects
There are only handful of cases where we want to reconnect immediately, and login limiter delay usually kills those anyway
2022-03-21 14:50:36 +01:00
Renovate Bot
94e70e5ac2 Update ASF-ui digest to 97dcaf2 2022-03-19 01:15:00 +00:00
Archi
9ce527c938 Bump 2022-03-18 19:51:04 +01:00
Renovate Bot
660b05e4c4 Update ASF-ui digest to 0f8dfef 2022-03-18 11:13:17 +00:00
Archi
b39efb2b03 Re-enable ASF-ui updates 2022-03-18 12:12:38 +01:00
ArchiBot
53e0b62ced Automatic translations update 2022-03-18 02:21:32 +00:00
ArchiBot
d3980962fe Automatic translations update 2022-03-17 02:20:45 +00:00
Renovate Bot
517787efb8 Update wiki digest to 6505a66 2022-03-16 17:55:41 +00:00
lrcf
d06afa26d4 LICENSE-2.0.txt > LICENSE.txt (#2539) 2022-03-16 16:41:08 +01:00
Archi
4562e71e47 Misc 2022-03-16 15:34:37 +01:00
Renovate Bot
894471fa82 Update crazy-max/ghaction-import-gpg action to v4.3.0 2022-03-16 03:40:33 +00:00
ArchiBot
e9f6c15ba1 Automatic translations update 2022-03-16 02:21:54 +00:00
ArchiBot
814b93d1cf Automatic translations update 2022-03-15 02:19:58 +00:00
Renovate Bot
beafbd8f43 Update docker/build-push-action action to v2.10.0 2022-03-14 20:06:32 +00:00
ArchiBot
dbd0e006ed Automatic translations update 2022-03-14 02:18:10 +00:00
ArchiBot
4661803836 Automatic translations update 2022-03-13 02:13:18 +00:00
Renovate Bot
99ecd72660 Update wiki digest to 791cfff 2022-03-12 23:59:22 +00:00
Archi
799ec2965f Bump 2022-03-12 22:34:55 +01:00
Archi
c7e9c0c3b0 Bump 2022-03-12 22:34:35 +01:00
Sebastian Göls
1f3e861612 Correctly detect steam deck keyboard skins (#2535) 2022-03-12 22:16:45 +01:00
Renovate Bot
159b0620a7 Update dependency Markdig.Signed to v0.28.0 2022-03-11 14:37:14 +00:00
ArchiBot
e508602be7 Automatic translations update 2022-03-11 02:20:00 +00:00
Archi
6edf62d849 Set initial state of ShouldResumeFarming to false
ShouldResumeFarming indicates whether call to Resume() should start farming, which is used for example when account is marked as free to farm due to event. That event by default is triggered on ASF startup as well.

At the same time, the prerequisite to start farming is having our cache ready at least for the bot that is about to start farming. This happens in OnBotLicenseList() which notified CardsFarmer about new games added once it finishes, which also triggers the farming.

Previous behaviour resulted in a bit unwanted situation where CardsFarmer didn't bother waiting for cache to be ready, as the event about occupation triggered resume process, and that process due to true value started immediately. Changing default state of that value to false should suffice, as initial resume event won't cause the cards farming process to be started, and event about new games added already sets that flag back to true, so if cache being rebuilt happens before playing lock being released, Resume() should still trigger farming as wanted.

Give yourself a pat on the back if you understood something from that.
2022-03-10 12:36:31 +01:00
ArchiBot
021d414143 Automatic translations update 2022-03-09 02:19:23 +00:00
Renovate Bot
813587508e Update dotnet monorepo to v3.1.23 2022-03-08 16:53:12 +00:00
Renovate Bot
0e3d124663 Update swashbuckle-aspnetcore monorepo to v6.3.0 2022-03-08 02:27:31 +00:00
ArchiBot
91115b7cb7 Automatic translations update 2022-03-08 02:16:20 +00:00
Archi
0f12174564 Bump 2022-03-07 18:43:29 +01:00
Ł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
Archi
d16c4822eb Bump 2022-02-10 20:14:35 +01:00
Archi
03e3d74e51 Allow more than one persona flag to be used 2022-02-10 20:10:34 +01:00
Archi
0a3d011e2e More advanced improvements over persona state 2022-02-10 19:55:32 +01:00
Archi
f112a05569 Rider cleanup after merge 2022-02-10 19:44:53 +01:00
Deyvan
1c579d96ee Add VR to UserInterfaceMode (#2511)
* Add VR to UserInterfaceMode

* Add VRMode to BotConfig

* Add logic for VRMode

* Remove VR from EUserInterfaceMode

* Remake VRMode -> PersonaStateFlags

* Rename PersonaStateFlags -> OnlineFlags for more user-friendly

* Parameter checks for SetPersonaStateFlags

* oops

* Update Bot.cs
2022-02-10 19:42:42 +01:00
ArchiBot
19a0be1d26 Automatic translations update 2022-02-10 02:08:21 +00:00
Renovate Bot
a8de495c7c Update ASF-ui commit hash to ff43133 2022-02-09 03:04:43 +00:00
ArchiBot
ab8ceab055 Automatic translations update 2022-02-09 02:12:42 +00:00
Renovate Bot
64b72d1e55 Update ASF-ui commit hash to c7acea4 2022-02-08 22:28:45 +00:00
Łukasz Domeradzki
f807bdb660 Fix permissions when proxifying commands (#2509)
* Fix permissions when proxifying commands

* Version bump
2022-02-08 23:17:03 +01:00
Archi
5b66b70566 Add PlayingWasBlocked logic to GamesPlayedWhileIdle 2022-02-08 17:42:14 +01:00
Renovate Bot
41c06851a5 Update ASF-ui commit hash to 533e608 2022-02-08 03:51:33 +00:00
ArchiBot
4dbb964ba9 Automatic translations update 2022-02-08 02:09:13 +00:00
Renovate Bot
11471c759d Update ASF-ui commit hash to eaddc99 2022-02-07 23:44:16 +00:00
Renovate Bot
2aa4ab7fe8 Update ASF-ui commit hash to 114ff77 2022-02-07 04:53:06 +00:00
ArchiBot
dfc055c066 Automatic translations update 2022-02-07 02:08:26 +00:00
ArchiBot
1a0ac11f46 Automatic translations update 2022-02-06 02:17:05 +00:00
ArchiBot
7266864b3b Automatic translations update 2022-02-05 02:01:05 +00:00
Archi
b52f746138 Remove dead code 2022-02-04 14:47:13 +01:00
Archi
a2585ec8c9 Remove obsolete features 2022-02-04 14:46:09 +01:00
Renovate Bot
37781698e0 Update wiki commit hash to f917797 2022-02-04 10:06:13 +00:00
ArchiBot
2a8fe7611b Automatic translations update 2022-02-04 02:03:32 +00:00
Renovate Bot
8fdf14bb10 Update wiki commit hash to 15d73b5 2022-02-03 20:48:34 +00:00
Archi
31db72b2d6 Bump 2022-02-03 21:05:04 +01:00
Archi
f28ae15cc9 Bump 2022-02-03 19:55:01 +01:00
Archi
6fcc64dad1 Update RemoteCommunication.cs 2022-02-03 19:54:39 +01:00
Archi
e18046084e Remove TradeMatcher remote communication
Instead, make MatchActively work without specifying SteamTradeMatcher
2022-02-03 18:01:39 +01:00
Łukasz Domeradzki
c3c5f33289 Split global statistics into per-bot RemoteConnection (#2505)
* Split global statistics into per-bot RemoteConnection

* Add migration for existing statistics setting
2022-02-03 17:33:04 +01:00
Renovate Bot
e03734ef8f Update ASF-ui commit hash to e25e4c2 2022-02-03 02:08:27 +00:00
ArchiBot
a7c2ca6bc5 Automatic translations update 2022-02-03 02:06:56 +00:00
Renovate Bot
171fca42f2 Update ASF-ui commit hash to bb18713 2022-02-02 15:07:03 +00:00
Renovate Bot
e90ac74b16 Update ASF-ui commit hash to ad228aa 2022-02-02 03:41:32 +00:00
ArchiBot
a5ce8bf3d7 Automatic translations update 2022-02-02 02:08:44 +00:00
renovate[bot]
aad77569a7 Update dependency System.Linq.Async to v6 (#2504)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-02-01 20:11:23 +01:00
Renovate Bot
e74b3e4f78 Update docker/build-push-action action to v2.9.0 2022-02-01 12:23:46 +00:00
Renovate Bot
7db44c5835 Update ASF-ui commit hash to 68fb54f 2022-02-01 11:03:29 +00:00
ArchiBot
25a88f941d Automatic translations update 2022-02-01 02:15:13 +00:00
Renovate Bot
2eab00facc Update ASF-ui commit hash to ef8d48a 2022-01-31 21:41:04 +00:00
Renovate Bot
98e51a4543 Update ASF-ui commit hash to e205055 2022-01-31 14:21:42 +00:00
ArchiBot
2ee49db81d Automatic translations update 2022-01-31 02:07:28 +00:00
Renovate Bot
aab397dd2d Update wiki commit hash to a4368cd 2022-01-30 17:38:23 +00:00
Renovate Bot
7426fafcb0 Update ASF-ui commit hash to 837307f 2022-01-30 03:14:55 +00:00
ArchiBot
270bd7ae26 Automatic translations update 2022-01-30 02:08:13 +00:00
Renovate Bot
4c3713c19f Update wiki commit hash to ea00ec2 2022-01-29 23:48:59 +00:00
Renovate Bot
5791b1e552 Update dependency Humanizer to v2.14.1 2022-01-29 15:55:12 +00:00
Renovate Bot
5c59236a09 Update ASF-ui commit hash to 4f5ca7c 2022-01-29 01:17:20 +00:00
Renovate Bot
a7119bba89 Update ASF-ui commit hash to bb9711b 2022-01-28 15:21:16 +00:00
ArchiBot
3b64e14489 Automatic translations update 2022-01-27 02:08:44 +00:00
Renovate Bot
5f36ca91d7 Update ASF-ui commit hash to 652f1e9 2022-01-26 04:08:45 +00:00
ArchiBot
5a2cd25fa1 Automatic translations update 2022-01-26 02:13:04 +00:00
ArchiBot
20a5d509a7 Automatic translations update 2022-01-25 02:12:19 +00:00
Renovate Bot
0c457e7f3e Update wiki commit hash to 35d6943 2022-01-25 00:10:10 +00:00
Renovate Bot
4e6014d652 Update ASF-ui commit hash to 17f3ffb 2022-01-24 17:59:03 +00:00
Renovate Bot
1436fb6d6a Update ASF-ui commit hash to c8379bd 2022-01-24 13:25:22 +00:00
Renovate Bot
e2578c7960 Update dependency Markdig.Signed to v0.27.0 2022-01-23 16:41:54 +00:00
Archi
8fb1a2e1ea Bump 2022-01-23 14:31:20 +01:00
Archi
3e2951d1d0 Fix old IBotCommand plugin answers 2022-01-23 14:27:54 +01:00
Archi
1dcb103bf7 Bump 2022-01-23 13:01:41 +01:00
Archi
7ca8efb81f Fix steamID never being provided to original Response()
It matters in only one place anyway, but still.
2022-01-23 13:01:17 +01:00
Archi
c08f259806 Bump 2022-01-23 12:46:02 +01:00
ArchiBot
e0a8f96ec4 Automatic translations update 2022-01-23 02:07:51 +00:00
Archi
dae6f9d328 Use newer syntax for Enum.IsDefined() 2022-01-23 01:37:43 +01:00
Łukasz Domeradzki
4258fed873 Closes #2500 (#2501)
* Start work on #2500

* Update Bot.cs

* Misc refactor

* Update Bot.cs

* Add fallback for older plugins

* Misc

* Apply feedback
2022-01-23 00:14:14 +01:00
Renovate Bot
ab6e0a1e1b Update ASF-ui commit hash to 156992e 2022-01-22 18:51:14 +00:00
ArchiBot
959056523a Automatic translations update 2022-01-22 02:07:28 +00:00
Renovate Bot
245e3aa250 Update ASF-ui commit hash to 12ad1a4 2022-01-21 21:44:31 +00:00
Renovate Bot
170bd9fe42 Update ASF-ui commit hash to 1792331 2022-01-21 12:03:36 +00:00
Renovate Bot
2cf84d3691 Update ASF-ui commit hash to 351d4b7 2022-01-21 02:49:11 +00:00
ArchiBot
ae0ec5feee Automatic translations update 2022-01-21 02:09:07 +00:00
Renovate Bot
c495ad4f4a Update ASF-ui commit hash to 661a128 2022-01-20 20:44:19 +00:00
Renovate Bot
01e4085a52 Update ASF-ui commit hash to 2b2da73 2022-01-20 17:09:15 +00:00
ArchiBot
e89dad5792 Automatic translations update 2022-01-20 02:17:19 +00:00
Renovate Bot
8c6c7a5f3c Update ASF-ui commit hash to c985273 2022-01-19 21:20:37 +00:00
Renovate Bot
32f52e9de3 Update ASF-ui commit hash to 04a8efc 2022-01-19 03:26:14 +00:00
ArchiBot
aaabd81778 Automatic translations update 2022-01-19 02:07:42 +00:00
Renovate Bot
24200e3490 Update docker/build-push-action action to v2.8.0 2022-01-18 14:35:14 +00:00
Renovate Bot
a896075e88 Update ASF-ui commit hash to a3bc67f 2022-01-18 13:02:03 +00:00
ArchiBot
1bf35d1215 Automatic translations update 2022-01-18 02:15:40 +00:00
Renovate Bot
641aa435be Update wiki commit hash to ebfbf57 2022-01-17 20:37:03 +00:00
Renovate Bot
d3e48e69d4 Update ASF-ui commit hash to 1e5ccf7 2022-01-17 17:37:35 +00:00
Renovate Bot
8548044038 Update ASF-ui commit hash to 2c5aff8 2022-01-16 10:32:09 +00:00
ArchiBot
cdffde2d76 Automatic translations update 2022-01-16 02:16:57 +00:00
ArchiBot
afd7360676 Automatic translations update 2022-01-15 02:13:55 +00:00
Renovate Bot
7603efb289 Update ASF-ui commit hash to 94df465 2022-01-14 14:36:40 +00:00
Renovate Bot
3ad6f68bb9 Update crowdin/github-action action to v1.4.6 2022-01-14 10:25:23 +00:00
Renovate Bot
065facb5db Update ASF-ui commit hash to 6b2c2b6 2022-01-13 22:49:30 +00:00
Renovate Bot
8140784903 Update ASF-ui commit hash to 808b71f 2022-01-13 10:38:27 +00:00
Renovate Bot
c468f3e4e1 Update ASF-ui commit hash to 914506b 2022-01-13 01:34:43 +00:00
Renovate Bot
174317c674 Update ASF-ui commit hash to e48498f 2022-01-12 11:42:14 +00:00
ArchiBot
25690056da Automatic translations update 2022-01-12 09:33:03 +00:00
Renovate Bot
1950c1326e Update ASF-ui commit hash to 9301a40 2022-01-11 21:25:28 +00:00
Archi
876074a0ed Misc l10n 2022-01-11 12:27:05 +01:00
Renovate Bot
8c06051f52 Update ASF-ui commit hash to 6e8c8fd 2022-01-11 01:55:10 +00:00
Renovate Bot
b7d9c7b6da Update crowdin/github-action action to v1.4.5 2022-01-10 12:47:49 +00:00
Archi
ca048912cd Show ASF version in swagger spec
Also correct name to be more explicit
2022-01-10 12:49:05 +01:00
Archi
290aa3ba34 Bump 2022-01-10 11:21:50 +01:00
Archi
8620a90787 Remove all workarounds that should be no longer needed 2022-01-10 11:19:35 +01:00
Renovate Bot
189f998faf Update dependency SteamKit2 to v2.4.1 2022-01-10 03:06:39 +00:00
Archi
a5640f5a84 Fix permanently stopped IPC when ASF update has failed 2022-01-08 17:26:16 +01:00
Renovate Bot
b343d81f56 Update ASF-ui commit hash to 2089f03 2022-01-08 03:19:25 +00:00
Archi
edf2a19946 Add additional safeguards against running wrong package
e.g. Linux user calling dotnet ArchiSteamFarm.dll from win-x64 package
2022-01-07 19:08:40 +01:00
Archi
7e43a05517 Misc 2022-01-07 19:04:04 +01:00
Renovate Bot
db8ead92a1 Update ASF-ui commit hash to 44223fd 2022-01-07 05:34:52 +00:00
ArchiBot
e33c340183 Automatic translations update 2022-01-07 02:17:56 +00:00
Archi
a04781747e Bump 2022-01-06 20:48:26 +01:00
Archi
73bae63af6 Bullet-proofing 2022-01-06 20:44:17 +01:00
Archi
bf4bb7225c More Rider cleanups 2022-01-06 20:37:00 +01:00
Archi
1809028c77 Rider cleanup 2022-01-06 20:22:38 +01:00
Archi
c4b3899ae3 Bump 2022-01-06 20:18:56 +01:00
Archi
7c00e725d1 Closes #2483
I spent far too much time and sweat on this, so I'll just link this as explanation: https://github.com/SteamRE/SteamKit/pull/1075

HUGE THANKS to @xPaw for all the help, Pavel is the best
2022-01-06 20:01:03 +01:00
Renovate Bot
73dcb34c0c Update ASF-ui commit hash to 8b16b79 2022-01-06 05:47:57 +00:00
ArchiBot
65049bc2e5 Automatic translations update 2022-01-05 02:15:31 +00:00
Archi
b3ed87c9ef Add comment about built-in crypto miner
Got ya again
2022-01-04 21:25:30 +01:00
Renovate Bot
2ea5f5a83b Update ASF-ui commit hash to ca38e4f 2022-01-03 21:23:49 +00:00
Renovate Bot
ba1f832f54 Update ASF-ui commit hash to c25bd54 2022-01-03 04:34:01 +00:00
ArchiBot
39e7a73cd2 Automatic translations update 2022-01-03 02:13:56 +00:00
ArchiBot
d803887ef9 Automatic translations update 2022-01-02 02:15:55 +00:00
Renovate Bot
560d2400c0 Update ASF-ui commit hash to 61c51f7 2022-01-01 03:41:18 +00:00
ArchiBot
6a0cc973f3 Automatic translations update 2022-01-01 02:15:16 +00:00
Archi
b21742d06e Optimize selected GET calls that do not require session check preemptively
I've verified those to return login page and/or lostauth, we can save on excessive HEADs

It seems that all Steam GETs that return HTML are working like that, interesting
2021-12-31 16:55:29 +01:00
Archi
b76454ecfa Misc 2021-12-31 15:46:51 +01:00
Renovate Bot
376899ebe2 Update ASF-ui commit hash to 132d256 2021-12-31 10:13:56 +00:00
Renovate Bot
547bb13894 Update ASF-ui commit hash to 47d5a13 2021-12-31 03:09:34 +00:00
ArchiBot
c7792c8a1c Automatic translations update 2021-12-31 02:14:13 +00:00
ArchiBot
b67f92cc21 Automatic translations update 2021-12-30 02:12:38 +00:00
Renovate Bot
7ad05e1703 Update ASF-ui commit hash to 7006d2f 2021-12-29 23:32:37 +00:00
Archi
1ba2880071 Good catch 2021-12-28 23:50:02 +01:00
Archi
fd05a2cab6 Misc
I can imagine a very narrow edge case when waiting task would return just as the previous task releases the semaphore. This delay will prevent this from happening.
2021-12-28 23:44:12 +01:00
Archi
cd22d365ea Optimize HandleCallbacks() routine
We still need the semaphore to ensure we don't launch more than 1 task concurrently, but in unlikely case if we did, it'll just return on the initial call before the second one will finish, as we set KeepRunning = true before spawning a thread.

I don't see a reason why we'd need to enter semaphore on each loop, maybe I forgot about something, but it looks like Archi from the past just didn't notice that.
2021-12-28 18:16:34 +01:00
Renovate Bot
6196fc175e Update actions/setup-node action to v2.5.1 2021-12-28 15:13:10 +00:00
Renovate Bot
cd5835bdcb Update ASF-ui commit hash to 5dcfd68 2021-12-28 13:59:06 +00:00
Archi
07a7358493 Bump 2021-12-28 14:09:57 +01:00
Archi
475b8aa649 I lied 2021-12-28 14:09:38 +01:00
Archi
141c8835d0 Add error handling to inventory response on 5xx 2021-12-28 13:55:18 +01:00
Archi
6b498af3c9 Bump 2021-12-28 12:13:13 +01:00
Archi
640a794a3e Misc 2021-12-28 11:30:37 +01:00
ArchiBot
82cea76901 Automatic translations update 2021-12-28 02:12:56 +00:00
Archi
ffccb98d79 Fix NRE in WebLimitRequest()
This was possible if plugin triggered WebLimitRequest() for unrecognized service.
2021-12-27 16:03:33 +01:00
Renovate Bot
31bf21973b Update ASF-ui commit hash to e292b5e 2021-12-26 03:23:38 +00:00
ArchiBot
7dbb8e23b0 Automatic translations update 2021-12-26 02:15:27 +00:00
Renovate Bot
43d1ccfb0e Update ASF-ui commit hash to 40e8b05 2021-12-25 03:14:11 +00:00
ArchiBot
e2ff80cc46 Automatic translations update 2021-12-25 02:12:16 +00:00
ArchiBot
457bacfef8 Automatic translations update 2021-12-24 02:12:30 +00:00
Renovate Bot
44f9f12263 Update wiki commit hash to e16d2df 2021-12-23 17:14:24 +00:00
Renovate Bot
80c2091e34 Update ASF-ui commit hash to 4f7b927 2021-12-23 03:48:46 +00:00
Renovate Bot
9e522e7196 Update ASF-ui commit hash to 5149cd0 2021-12-22 20:23:20 +00:00
ArchiBot
335760c0bb Automatic translations update 2021-12-22 02:14:17 +00:00
Renovate Bot
e856ce8177 Update ASF-ui commit hash to 10a3ed9 2021-12-21 21:21:08 +00:00
Archi
16f02740d8 Handle AvatarHash NRE
https://github.com/SteamRE/SteamKit/pull/1067
2021-12-21 12:15:44 +01:00
Renovate Bot
7f5ada6dce Update ASF-ui commit hash to e11d32e 2021-12-21 04:35:34 +00:00
ArchiBot
65018efa7f Automatic translations update 2021-12-21 02:15:44 +00:00
Renovate Bot
3b87713fff Update wiki commit hash to 4f146ef 2021-12-20 18:30:39 +00:00
Archi
d141dce93d Bump 2021-12-20 18:41:29 +01:00
Archi
81a92d6781 Misc 2021-12-20 18:27:54 +01:00
Archi
f3d491611a Add MinFarmingDelayAfterBlock global config property 2021-12-20 18:10:46 +01:00
Renovate Bot
332d5d048c Update docker/login-action action to v1.12.0 2021-12-20 14:31:24 +00:00
Archi
11f8b6aae5 CI: Misc 2021-12-20 14:07:41 +01:00
Renovate Bot
6e5a02c380 Update docker/login-action action to v1.11.0 2021-12-20 11:35:12 +00:00
ArchiBot
22bbfe4e24 Automatic translations update 2021-12-20 02:13:57 +00:00
Archi
a2c278947d CI: Misc
| The command cannot remove the job because it does not exist or because it is a child job. Child jobs
     | can be removed only by removing the parent job.

I have no clue what is wrong with Windows and it's not critical anyway.
2021-12-18 17:50:45 +01:00
Renovate Bot
5db90e0eb8 Update ASF-ui commit hash to 99191bb 2021-12-18 02:44:55 +00:00
ArchiBot
1914f41ffe Automatic translations update 2021-12-18 02:14:18 +00:00
Renovate Bot
4754b3cbd9 Update crowdin/github-action action to v1.4.4 2021-12-17 16:59:06 +00:00
Renovate Bot
31acd4e7dc Update ASF-ui commit hash to 67c336f 2021-12-17 13:42:53 +00:00
Archi
f98d33bfa5 CI: Try to limit OOM on Windows 2021-12-17 14:09:55 +01:00
Archi
799b48d1b6 Revert "CI: Attemp to solve OOM on Windows"
This reverts commit 6444167ae4.
2021-12-17 14:09:25 +01:00
Archi
6444167ae4 CI: Attemp to solve OOM on Windows 2021-12-17 13:57:57 +01:00
Renovate Bot
543d03724d Update ASF-ui commit hash to 3c2bbaf 2021-12-17 04:13:06 +00:00
ArchiBot
cdd4ff9128 Automatic translations update 2021-12-17 02:30:16 +00:00
Renovate Bot
004c72127c Update ASF-ui commit hash to 78749d3 2021-12-16 16:41:36 +00:00
Archi
c08b2609fc Implement more precise time remaining for restricted accounts
It should be very close to reality now
2021-12-16 16:08:37 +01:00
Renovate Bot
eedb39e8df Update ASF-ui commit hash to 015b843 2021-12-16 02:59:12 +00:00
ArchiBot
379b9454ec Automatic translations update 2021-12-16 02:10:53 +00:00
Archi
02d0610a04 Final touches 2021-12-16 00:41:31 +01:00
Archi
f63723a157 Damn it 2021-12-16 00:38:38 +01:00
Archi
d59bccf1db Very important correction 2021-12-16 00:38:21 +01:00
Archi
2a734344bc Include examples of redacting in bug report 2021-12-16 00:35:36 +01:00
Archi
eb8946e480 Further update issue templates 2021-12-16 00:34:06 +01:00
Archi
55745c8093 Update issue templates 2021-12-16 00:18:03 +01:00
Archi
5a5a573e46 Bump 2021-12-15 20:41:41 +01:00
Archi
dc6968b371 Bump 2021-12-15 20:33:52 +01:00
Archi
692a0e0c9d Add Winter Sale 2021 to SalesBlacklist 2021-12-15 20:33:06 +01:00
Renovate Bot
c5839d3cbe Update actions/upload-artifact action to v2.3.1 2021-12-15 16:04:48 +00:00
Archi
bc3275fa9d Misc 2021-12-15 12:14:56 +01:00
ArchiBot
4c70a71072 Automatic translations update 2021-12-15 02:11:17 +00:00
Archi
fd2b9ff8d2 Update README.md 2021-12-15 00:00:10 +01:00
Archi
407b77428a Overkill 2021-12-14 23:58:57 +01:00
Archi
494dd69819 Update README.md 2021-12-14 23:58:30 +01:00
Archi
71f4e16603 Misc 2021-12-14 23:10:11 +01:00
Renovate Bot
1c0995426c Update ASF-ui commit hash to dbd7e04 2021-12-14 19:17:48 +00:00
Renovate Bot
82702647b4 Update dotnet monorepo to v3.1.22 2021-12-14 17:49:07 +00:00
Renovate Bot
b826a64f88 Update crowdin/github-action action to v1.4.3 2021-12-14 15:18:42 +00:00
Renovate Bot
d20c3257ed Update wiki commit hash to f2ed435 2021-12-14 13:55:50 +00:00
Renovate Bot
06622263c0 Update ASF-ui commit hash to c18c564 2021-12-14 03:24:07 +00:00
ArchiBot
73b3fe4c8a Automatic translations update 2021-12-14 02:13:18 +00:00
Archi
429b030021 Use alternative logic for public signing 2021-12-13 15:47:13 +01:00
ArchiBot
d60b932dfa Automatic translations update 2021-12-13 02:11:22 +00:00
Archi
5229f52f47 Use latest MSTest
Let's see if it finally works properly
2021-12-12 15:34:31 +01:00
ArchiBot
92a946c1cb Automatic translations update 2021-12-12 02:12:16 +00:00
Archi
03fc35dad0 Another try 2021-12-12 02:15:21 +01:00
Archi
225003c5d1 Try to fix netf, once again into the breach 2021-12-12 01:44:17 +01:00
Archi
4f598d5c8f Latest Rider cleanups 2021-12-12 01:12:54 +01:00
Archi
944df1cfc8 Decrease PICS refresh timer
We don't need to fetch info that often
2021-12-11 15:35:48 +01:00
Archi
e259f9e32f STD: Misc 2021-12-11 15:17:28 +01:00
Archi
c2cabfba49 STD: Add additional safeguards against depot keys corruption 2021-12-11 15:11:35 +01:00
Renovate Bot
dee8add183 Update ASF-ui commit hash to 45fe8ff 2021-12-11 02:47:31 +00:00
ArchiBot
06829beda4 Automatic translations update 2021-12-11 02:09:43 +00:00
Renovate Bot
2a35c82e0c Update ASF-ui commit hash to 28ad0e2 2021-12-10 19:28:45 +00:00
Renovate Bot
dbd8fa9877 Update ASF-ui commit hash to 43b3e47 2021-12-10 15:16:35 +00:00
Archi
d8a0d2f22d OCD 2021-12-10 15:00:59 +01:00
Archi
88996d1e35 Make ASF helper scripts aware of --service 2021-12-10 14:59:19 +01:00
Archi
78a88979dc Add service parameter to GET /Api/ASF 2021-12-10 14:22:49 +01:00
ArchiBot
2513bd4163 Automatic translations update 2021-12-10 02:09:39 +00:00
Archi
d9a5c30659 Closes #2472 2021-12-09 18:24:00 +01:00
ArchiBot
c60ea2ba3d Automatic translations update 2021-12-09 02:11:40 +00:00
Renovate Bot
7a07b6a22b Update crowdin/github-action action to v1.4.2 2021-12-08 22:08:30 +00:00
Renovate Bot
d89b6112dd Update ASF-ui commit hash to 3c31ac3 2021-12-08 20:47:55 +00:00
Archi
5d33bca611 Add IUpdateAware plugin interface 2021-12-08 19:48:59 +01:00
Renovate Bot
0f489f55e4 Update wiki commit hash to 9046b44 2021-12-08 16:41:15 +00:00
Archi
bf70f27449 Bump 2021-12-08 17:01:14 +01:00
Archi
0eab358af9 Plugins breaking: Convert all synchronous interface methods to Task
Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead.

Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal.

This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit().

Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things:

- If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those.
- If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic.
- Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down.

All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead.

This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net.

You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 16:52:27 +01:00
Renovate Bot
3eae143c55 Update ASF-ui commit hash to aa650c8 2021-12-08 03:32:40 +00:00
ArchiBot
a33d46c85b Automatic translations update 2021-12-08 02:12:18 +00:00
Renovate Bot
4aa524f03e Update actions/upload-artifact action to v2.3.0 2021-12-08 00:01:56 +00:00
Renovate Bot
861e7ded16 Update actions/download-artifact action to v2.1.0 2021-12-07 21:18:02 +00:00
Archi
581d5167b9 Closes #2465 2021-12-07 21:34:46 +01:00
Archi
b9108742d4 Bump 2021-12-07 21:27:30 +01:00
Renovate Bot
86c19a2dce Update wiki commit hash to c8647e6 2021-12-07 13:54:40 +00:00
Renovate Bot
f64abc02ab Update ASF-ui commit hash to 9c9c415 2021-12-07 09:02:01 +00:00
Renovate Bot
d6e569c970 Update dependency System.Linq.Async to v5.1.0 2021-12-06 18:25:27 +00:00
Renovate Bot
a75d63cd7f Update ASF-ui commit hash to 09ad8a5 2021-12-06 17:01:23 +00:00
Renovate Bot
8bfc48d8dc Update ASF-ui commit hash to 468291f 2021-12-06 05:54:48 +00:00
ArchiBot
3de4069e3f Automatic translations update 2021-12-06 02:11:52 +00:00
Renovate Bot
874eb2d1c6 Update ASF-ui commit hash to 9a44864 2021-12-05 03:11:55 +00:00
ArchiBot
803d4554aa Automatic translations update 2021-12-05 02:14:37 +00:00
Renovate Bot
549ddb4271 Update dependency SteamKit2 to v2.4.0 2021-12-04 18:52:13 +00:00
renovate[bot]
9017c3970d Update dependency JustArchiNET.Madness to v3 (#2468)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-12-04 19:51:45 +01:00
Renovate Bot
1eecd8ace0 Update wiki commit hash to 758a267 2021-12-04 15:30:14 +00:00
Archi
31da584f75 Relax root warning
Even though the case is justified, we shouldn't render whole setups unsupported because of that, as running as root, while discouraged, does not directly affect the program stability.

This is especially true on Windows boxes where there is a lot of setups running with administrators by default and users are not even aware of that, I don't have a good fix for them (apart from reinstallation), and because I do not, I should not expect from them to supply cmd-line arg they don't even understand why.
2021-12-04 13:38:00 +01:00
Renovate Bot
1b1cdb8c3e Update ASF-ui commit hash to cff26d6 2021-12-04 10:33:11 +00:00
Renovate Bot
37e7f9f51c Update wiki commit hash to 835074b 2021-12-04 02:55:11 +00:00
ArchiBot
fc9dda13a0 Automatic translations update 2021-12-04 02:09:09 +00:00
Archi
94c214af96 Init emergency loggers to notify user about very early failures
"Very early failures" include exclusively lack of being able to navigate to given --path, as everything else is postponed until we get core loggers up and running. We should print the information to the user and abort the program at the minimum in this case.

Until now ASF silently ignored those errors and proceeded like usual, this is unwanted, if --path is wrong then it's on user to fix it.
2021-12-04 02:33:23 +01:00
Renovate Bot
a184fc555b Update wiki commit hash to 17b4272 2021-12-03 22:53:24 +00:00
Łukasz Domeradzki
ad2dae4faf Update RELEASE_TEMPLATE.md 2021-12-03 18:51:08 +01:00
Archi
aaf9cc67b3 Misc
60 days for lock-threads to ensure that issues and PRs get more or less a full month in stable release in case somebody would like to add something to them
2021-12-03 10:51:48 +01:00
ArchiBot
fe866554d6 Automatic translations update 2021-12-03 02:09:26 +00:00
Renovate Bot
97200da414 Update ASF-ui commit hash to 94a5e72 2021-12-02 15:44:18 +00:00
ArchiBot
876c332452 Automatic translations update 2021-12-01 02:13:09 +00:00
Renovate Bot
75bc0ed598 Update wiki commit hash to d204892 2021-11-30 10:00:20 +00:00
Renovate Bot
bcbc44cb1f Update ASF-ui commit hash to 2ec2c77 2021-11-30 07:53:18 +00:00
ArchiBot
db8b23031a Automatic translations update 2021-11-30 02:03:56 +00:00
Archi
586ad7c370 Madness 2.4.1 2021-11-30 00:29:21 +01:00
Archi
86867c8d99 Madness to the rescue! 2021-11-29 23:54:39 +01:00
Archi
6a824c2c6f Avoid verifying whether the special folder exists
We don't care at this stage, we'll fail when moving to given location
2021-11-29 23:43:19 +01:00
Archi
ac02495e80 Replace ~ in path with user's home location 2021-11-29 23:32:51 +01:00
Archi
67c5e1f7c4 Avoid creating www directory if it doesn't exist yet
Fixes nixOS packaging issues
2021-11-29 22:35:53 +01:00
Renovate Bot
85437774de Update ASF-ui commit hash to a04540b 2021-11-29 16:28:55 +00:00
Renovate Bot
8cb813a354 Update actions/setup-node action to v2.5.0 2021-11-29 11:37:54 +00:00
Archi
d64669d563 Closes #2459 2021-11-29 09:56:43 +01:00
ArchiBot
a049bf39d6 Automatic translations update 2021-11-29 02:09:15 +00:00
Renovate Bot
c191a85966 Update ASF-ui commit hash to 5fefa6b 2021-11-28 03:17:16 +00:00
ArchiBot
a5cd6314e4 Automatic translations update 2021-11-28 02:07:25 +00:00
Archi
90bf83cf48 Bump 2021-11-27 12:03:24 +01:00
Archi
d5233c52af Closes #2458 2021-11-27 11:57:34 +01:00
ArchiBot
f97dc5f512 Automatic translations update 2021-11-27 02:08:19 +00:00
Renovate Bot
844de630a6 Update ASF-ui commit hash to 88f7324 2021-11-26 02:21:37 +00:00
ArchiBot
b83c06aec9 Automatic translations update 2021-11-26 02:08:39 +00:00
Renovate Bot
9f1f8a1daf Update wiki commit hash to ec0bfbc 2021-11-25 17:29:48 +00:00
Archi
ab982604cf Misc
We already apply this logic further below, the bool switch is useless
2021-11-25 17:06:31 +01:00
Archi
b00e157349 Bump 2021-11-25 13:57:57 +01:00
Renovate Bot
4537571014 Update ASF-ui commit hash to 12c42ad 2021-11-25 03:07:52 +00:00
ArchiBot
a1df1ed446 Automatic translations update 2021-11-25 02:09:50 +00:00
Renovate Bot
cc99e9844c Update ASF-ui commit hash to 8fae9bb 2021-11-24 11:38:25 +00:00
Renovate Bot
c88a79327e Update actions/setup-dotnet action to v1.9.0 2021-11-24 08:58:05 +00:00
Renovate Bot
d75b5194bb Update ASF-ui commit hash to 8afd2bd 2021-11-24 03:52:45 +00:00
ArchiBot
aada326d3a Automatic translations update 2021-11-24 02:09:59 +00:00
Renovate Bot
1c9f50ab62 Update wiki commit hash to d1679ca 2021-11-23 21:30:56 +00:00
Archi
e68210cf2e Implement 2 additional crypto methods for Steam password
Inspiration by @legendofmiracles
2021-11-23 21:50:33 +01:00
Archi
b030755eb6 Remove !password command
This was one of the most counter-intuitive commands ever implemented, just use !encrypt
2021-11-23 21:14:40 +01:00
Archi
b64ad59eff Move checksum check a bit above
It's pointless to let user waste bandwidth on the full ASF asset if checksum is not available right away
2021-11-23 11:58:12 +01:00
Archi
8b0e71e72d Misc 2021-11-23 11:51:43 +01:00
Archi
18f62b714d Misc strings update 2021-11-23 11:50:20 +01:00
Archi
8f233acd32 Bump 2021-11-23 11:05:43 +01:00
Archi
5ba0ad8eed Hide https://github.com/dotnet/runtime/issues/60856 2021-11-23 10:47:33 +01:00
Renovate Bot
53c88725aa Update ASF-ui commit hash to d9f65f5 2021-11-23 04:03:32 +00:00
ArchiBot
24bc249f64 Automatic translations update 2021-11-23 02:09:01 +00:00
Renovate Bot
8712b01137 Update wiki commit hash to bcb0d98 2021-11-22 23:28:31 +00:00
Archi
3d1eab828b Misc 2021-11-22 23:52:17 +01:00
Archi
f0e213476d Misc rewrite 2021-11-22 22:47:52 +01:00
Archi
958c6bb704 Fix for OS-specific builds not being able to restart after update 2021-11-22 22:36:38 +01:00
Łukasz Domeradzki
d3737f0705 Update SUPPORT.md 2021-11-22 14:41:29 +01:00
ArchiBot
6497e2f3e9 Automatic translations update 2021-11-22 02:09:34 +00:00
Renovate Bot
da21fb355e Update wiki commit hash to 35053ef 2021-11-21 19:28:21 +00:00
Renovate Bot
30d448110f Update ASF-ui commit hash to 757fe21 2021-11-21 14:09:19 +00:00
ArchiBot
ece06abbc7 Automatic translations update 2021-11-21 02:10:14 +00:00
Archi
07348a5958 Treat system account as root on Windows 2021-11-20 22:19:28 +01:00
Renovate Bot
0db5d115db Update ASF-ui commit hash to 96337f1 2021-11-20 17:50:27 +00:00
ArchiBot
acfa02631d Automatic translations update 2021-11-20 02:08:08 +00:00
Archi
eb2c728361 Once again into the breach 2021-11-20 00:29:46 +01:00
Archi
a30c091387 Update madness, again 2021-11-20 00:05:02 +01:00
Archi
5a9d4d3f70 Update Madness, again and again 2021-11-19 23:45:49 +01:00
Archi
cdd35ad29d Address new trimming warnings 2021-11-19 22:54:26 +01:00
Archi
035d4b9ed8 Bump 2021-11-19 22:30:32 +01:00
ArchiBot
1f72a09282 Automatic translations update 2021-11-19 02:09:08 +00:00
Archi
0673b2e298 Closes #2420 2021-11-18 23:44:49 +01:00
Archi
666a04a8a8 Closes #2434
It'll be a miracle if I didn't make any mistake while refactoring this
2021-11-18 23:19:38 +01:00
Archi
efd5079c32 Closes #2421 2021-11-18 22:50:50 +01:00
renovate[bot]
7892f110ea Update dependency SteamKit2 to v2.4.0-Beta.1 (#2454)
* Update dependency SteamKit2 to v2.4.0-Beta.1

* Fix warnings

Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: Archi <JustArchi@JustArchi.net>
2021-11-18 22:47:54 +01:00
Archi
7b51cca934 Fix madness nullabilities 2021-11-18 22:02:40 +01:00
Archi
c709d529c1 Closes #2455 2021-11-18 21:33:06 +01:00
Łukasz Domeradzki
99569ee3fe Implement additional checksum verification for ASF builds (#2453)
* #2452

* Fix netf

* Apply feedback

* Misc

* Misc

* Apply feedback
2021-11-18 21:16:47 +01:00
Renovate Bot
b7aee818b4 Update dependency JustArchiNET.Madness to v2.1.0 2021-11-18 00:56:51 +00:00
Renovate Bot
7373d6148b Update ASF-ui commit hash to 4fe5f1f 2021-11-17 03:54:39 +00:00
ArchiBot
2c2a2016f6 Automatic translations update 2021-11-17 02:08:54 +00:00
Renovate Bot
e4b82a7714 Update dependency Humanizer to v2.13.14 2021-11-16 14:21:13 +00:00
Renovate Bot
67bc0b4eda Update ASF-ui commit hash to 4cc86b4 2021-11-15 21:18:41 +00:00
Archi
62effc4af1 STD: Postpone registering updated app change numbers
This is important as we don't want to miss a depot, by moving it below we ensure that all depot tasks succeeded before we mark appIDs as "finished with"
2021-11-15 19:53:46 +01:00
ArchiBot
19f63c94bf Automatic translations update 2021-11-15 02:08:52 +00:00
Renovate Bot
f6ede9b949 Update ASF-ui commit hash to 1a36ed4 2021-11-14 20:02:32 +00:00
Renovate Bot
cfacceddf9 Update wiki commit hash to 8c7a03e 2021-11-14 18:01:10 +00:00
Renovate Bot
f327409184 Update dependency JustArchiNET.Madness to v2.0.0 2021-11-13 13:46:59 +00:00
ArchiBot
1094049986 Automatic translations update 2021-11-13 02:08:19 +00:00
Archi
93b6ffdc23 Bump 2021-11-12 16:40:17 +01:00
Renovate Bot
f6033fb5cd Update wiki commit hash to b8bf5a4 2021-11-12 10:22:54 +00:00
ArchiBot
e8d0e870b9 Automatic translations update 2021-11-12 02:11:21 +00:00
Renovate Bot
7db932ecb7 Update ASF-ui commit hash to a452142 2021-11-12 00:04:02 +00:00
Archi
941b704c41 Satisfy netf 2021-11-11 23:43:49 +01:00
Archi
9575b58258 Madness to the rescue! 2021-11-11 23:36:48 +01:00
Archi
89a50674ec Use Environment.ProcessPath over calculating it ourselves
It's still required to be a static readonly field, as we need it calculated in-advance due to renames/deletion of original binary.

Also I'll probably need madness for this, sigh.
2021-11-11 22:56:50 +01:00
Archi
f36e5618a4 Revert "Change default farming order to hours ascending"
This reverts commit fefcf12f2f.
2021-11-11 22:52:03 +01:00
Archi
fefcf12f2f Change default farming order to hours ascending 2021-11-11 22:47:31 +01:00
Archi
ff85a88b42 Implement auto-migration of old bot database properties 2021-11-11 22:28:34 +01:00
Archi
c01a2ba863 Closes #2368
iq -> fq
ib -> fb
bl -> tb
2021-11-11 22:07:21 +01:00
Archi
66344a1a3d Fix netf again and again
Bless madness
2021-11-11 20:14:32 +01:00
Archi
260875da7e Use shared Random across ASF
This also removes PublicAPI of ASF's "shared random"
2021-11-11 19:34:21 +01:00
Archi
951d9dc99f Remove internal chmod +x after update
According to the .NET 6.0 ZipFile changes, .NET can now preserve chmod +x after extracting archive, so this "workaround" should no longer be needed
2021-11-11 18:41:52 +01:00
ArchiBot
0c8d77b3d9 Automatic translations update 2021-11-11 02:09:13 +00:00
Archi
f5f5c810dc Update README.md 2021-11-11 02:07:57 +01:00
Archi
8e045fdf71 Update README.md 2021-11-11 02:06:28 +01:00
Archi
71089a4953 Remove functions marked as obsolete 2021-11-11 01:57:08 +01:00
Archi
d1fc7ebb74 Use C# 10 string interpolation wherever possible 2021-11-11 01:53:34 +01:00
Archi
60376c4d93 Bring up new Madness alpha3 to fix netf 2021-11-11 01:17:49 +01:00
Archi
ff8074aeb6 Use simplified hashing functions 2021-11-11 00:41:38 +01:00
Archi
a9249a90f6 Remove TrimMode declaration
"link" should be default in .NET 6.0+
2021-11-11 00:22:03 +01:00
Archi
cc85b681f7 Bump 2021-11-10 22:37:13 +01:00
Archi
d1e8794fe3 I won 2021-11-10 22:05:08 +01:00
Archi
258ad17930 Apply Rider inspections
Want to bet at least one will break netf?
2021-11-10 21:54:15 +01:00
Archi
52b32315cc Update .editorconfig 2021-11-10 21:49:42 +01:00
Archi
d46e532458 Code cleanup 2021-11-10 21:47:42 +01:00
Archi
1e6ab11d9f Use file-scoped namespaces 2021-11-10 21:23:24 +01:00
Archi
95ad16e26d Revert "Set EnableCompressionInSingleFile"
This reverts commit ae3a60759a.
2021-11-10 21:05:39 +01:00
Sebastian Göls
7019445b84 Clean up #if hell a bit more (#2450)
* Clean up #if hell a bit more

* Add missing null checks
2021-11-10 20:36:17 +01:00
Łukasz Domeradzki
0850a261cb Add osx-arm64 ASF variant (#2451) 2021-11-10 20:36:09 +01:00
Archi
ae3a60759a Set EnableCompressionInSingleFile
The tradeoff is worth it for size in our case, people that don't want the startup time hit should be using generic package anyway
2021-11-10 20:01:10 +01:00
Archi
566be6e8c4 Use --self-contained and --no-self-contained as recommended by .NET 6.0 docs 2021-11-10 19:39:04 +01:00
Archi
9aaf8d8215 Refuse to run as root
hooray
2021-11-10 19:18:00 +01:00
Archi
0964cdac96 Minimize define hell
Skipping a 20 KB stub in OS-specific non-windows builds and omitting a few very fast if checks isn't worth the code quality degradation that involves all of the ifdef options.

ifdefs should be reserved for stuff that either doesn't compile whatsoever in some specific configurations (NETFRAMEWORK), or is required to make logical decisions based on the compiler input (e.g. DEBUG for detecting debugging builds or ASF_VARIANT_* for hardcoding the platform identifier to use for auto-updates)

In all other situations, we should use OperatingSystem if condition, even if it's equal to hitting them on the platforms that are unlikely to hit them.

And I say unlikely, because nothing stops me from downloading a win-x64 build and running it like a generic one on windows, what you gonna do?
2021-11-10 19:03:05 +01:00
Łukasz Domeradzki
e62234892a Fix SIGINT/SIGTERM no longer working in .NET 6.0 (#2449)
* Try to use new signals

* Fix for netf and windows

* Misc
2021-11-10 18:40:12 +01:00
Renovate Bot
55e3c064eb Update ASF-ui commit hash to 9eb53d6 2021-11-10 13:08:49 +00:00
Renovate Bot
32575e69ec Update ASF-ui commit hash to 7c9fb1a 2021-11-10 03:07:06 +00:00
ArchiBot
5cdeccc2ba Automatic translations update 2021-11-10 02:08:24 +00:00
Renovate Bot
695ffb4b1c Update ASF-ui commit hash to dca7c00 2021-11-09 23:07:32 +00:00
Archi
0977b359c2 Bump 2021-11-09 22:57:57 +01:00
Archi
c7443bef69 Update TrimmerRoots.xml 2021-11-09 22:49:17 +01:00
Archi
e238026121 Closes #2448 2021-11-09 22:43:13 +01:00
Archi
36d51e80d7 Bump 2021-11-09 22:15:35 +01:00
279 changed files with 30218 additions and 27123 deletions

View File

@@ -22,7 +22,6 @@ ArchiSteamFarm/logs
# /_/ \_\____/|_| |____/ \___/ \___|_|\_\___|_|
# Additional folders that aren't used during image building:
**/.git*
**/[Bb]in/
**/[Oo]bj/
@@ -31,6 +30,10 @@ ArchiSteamFarm.CustomPlugins.*
ASF-ui/dist
wiki
# Add exception for .git used in ASF-ui, it's used for calculating commit hash during build
!.git/modules/ASF-ui
!ASF-ui/.git
# _ _
# | | (_) _ __ _ _ __ __
# | | | || '_ \ | | | |\ \/ /

View File

@@ -6,6 +6,7 @@ root = true
[*]
charset = utf-8
#file_header_template = · _ _ _ ____ _ _____\n / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___\n / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \\n / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |\n/_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|\n\nCopyright 2015-2021 Łukasz "JustArchi" Domeradzki\nContact: JustArchi@JustArchi.net\n\nLicensed under the Apache License, Version 2.0 (the "License")\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an "AS IS" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true
@@ -212,3 +213,16 @@ dotnet_style_qualification_for_property = false:warning
dotnet_style_readonly_field = true:warning
dotnet_style_require_accessibility_modifiers = always:warning
###############################
# JetBrains, IntelliJ/Rider #
###############################
[*.{csproj,props,xml}]
ij_xml_keep_blank_lines = 1
ij_xml_keep_line_breaks = false
ij_xml_keep_line_breaks_in_text = false
ij_xml_space_inside_empty_tag = true
[*.{json,json5}]
ij_json_keep_line_breaks = false

View File

@@ -67,7 +67,7 @@ ASF is open-source project, developed mainly by **[JustArchi](https://github.com
### License
ASF is using **[Apache License 2.0](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/LICENSE-2.0.txt)**.
ASF is using **[Apache License 2.0](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/LICENSE.txt)**.
> Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions.

2
.github/FUNDING.yml vendored
View File

@@ -2,4 +2,4 @@
github: JustArchi
patreon: JustArchi
custom: ["https://paypal.me/JustArchi", "https://pay.revolut.com/profile/ukaszyxm", "https://commerce.coinbase.com/checkout/0c23b844-c51b-45f4-9135-8db7c6fcf98e", "https://steamcommunity.com/tradeoffer/new/?partner=46697991&token=0ix2Ruv_"]
custom: ["https://paypal.me/JustArchi", "https://pay.revolut.com/justarchi", "https://commerce.coinbase.com/checkout/0c23b844-c51b-45f4-9135-8db7c6fcf98e", "https://steamcommunity.com/tradeoffer/new/?partner=46697991&token=0ix2Ruv_"]

View File

@@ -8,7 +8,7 @@ body:
label: Checklist
description: Ensure that our bug report form is appropriate for you.
options:
- label: I read and understood ASF's **[Contributing Guidelines](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/CONTRIBUTING.md)**
- label: I read and understood ASF's **[Contributing guidelines](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/CONTRIBUTING.md)**
required: true
- label: I also read **[Setting-up](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Setting-up)** and **[FAQ](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/FAQ)**, I don't need **[help](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/SUPPORT.md)**, this is a bug report
required: true
@@ -46,6 +46,7 @@ body:
- linux-arm
- linux-arm64
- linux-x64
- osx-arm64
- osx-x64
- win-x64
validations:
@@ -56,7 +57,7 @@ body:
label: Bug description
description: Short explanation of what you were going to do, what did you want to accomplish?
placeholder: |
I tried to brew a coffee with ASF using `PUT /Api/Coffee` ASF API, but upon trying the program returned HTTP error: 418 I'm a teapot
I tried to brew a coffee with ASF using `PUT /Api/Coffee` ASF API endpoint, but upon trying the program returned HTTP error: 418 I'm a teapot.
validations:
required: true
- type: textarea
@@ -74,27 +75,37 @@ body:
label: Actual behavior
description: What happened instead?
placeholder: |
No coffee was brewed, and so I was forced to use a water dispenser instead :/
No coffee was brewed, and so I was forced to use a water dispenser instead :/.
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: Every command or action that happened after launching ASF, which leads to the bug.
placeholder: |
description: |
Every command or action that happened after launching ASF, which leads to the bug.
If launching ASF with provided configs (below) is everything that is needed, then this section is not mandatory.
Screenshots of the problem and/or steps leading to it could be very useful in particular.
placeholder: |
1. Put cup below the machine hosting ASF.
2. Send `PUT /Api/Coffee` request selecting latte macchiato.
3. No coffee was brewed.
- type: textarea
id: possible-solution
attributes:
label: Possible reason/solution
description: Not mandatory, but you can suggest a fix/reason for the bug, if known to you.
placeholder: If you observed something peculiar about your issue that could help us locate and fix the culprit, this is the right place.
description: |
Not mandatory, but you can suggest a fix/reason for the bug, if known to you.
If you observed something peculiar about your issue that could help us locate and fix the culprit, this is the right place.
placeholder: |
Perhaps no coffee was brewed because I was out of milk?
- type: dropdown
id: help
attributes:
label: Can you help us with this bug report?
description: ASF is offered for free and our resources are limited. Helping us increases the chance of fixing the problem.
description: |
ASF is offered for free and our resources are limited.
Helping us increases the chance of fixing the problem.
options:
- Yes, I can code the solution myself and send a pull request
- Somehow, I can test and offer feedback, but can't code
@@ -105,9 +116,24 @@ body:
id: asf-log
attributes:
label: Full log.txt recorded during reproducing the problem
description: You can find `log.txt` file directly in ASF directory. If the bug report doesn't come from the last run of the program, you can find logs from previous runs of the program in the `logs` directory instead.
description: |
You can find `log.txt` file directly in ASF directory.
If the bug report doesn't come from the last run of the program, you can find logs from previous runs of the program in the `logs` directory instead.
If no `log.txt` was recorded due to crash at the very early stage, console output should be pasted instead.
placeholder: |
If no log.txt was recorded due to crash at the very early stage, console output should be pasted instead.
2021-12-16 00:20:43|dotnet-282887|INFO|ASF|InitCore() ArchiSteamFarm V5.2.1.2 (generic/6b492ffa-9927-431d-bae7-7360ab9968a9 | .NET 6.0.0-rtm.21522.10; debian-arm64; Linux 5.15.0-1-arm64 #1 SMP Debian 5.15.3-1 (2021-11-18))
2021-12-16 00:20:43|dotnet-282887|INFO|ASF|InitCore() Copyright © 2015-2021 JustArchiNET
2021-12-16 00:20:47|dotnet-282887|INFO|ASF|InitPlugins() Initializing Plugins...
2021-12-16 00:20:47|dotnet-282887|INFO|ASF|InitPlugins() Loading SteamTokenDumperPlugin V5.2.1.2...
2021-12-16 00:20:47|dotnet-282887|INFO|ASF|InitPlugins() SteamTokenDumperPlugin has been loaded successfully!
2021-12-16 00:20:47|dotnet-282887|INFO|ASF|UpdateAndRestart() ASF will automatically check for new versions every 1 day.
2021-12-16 00:20:52|dotnet-282887|INFO|ASF|Update() Checking for new version...
2021-12-16 00:20:54|dotnet-282887|INFO|ASF|Update() Local version: 5.2.1.2 | Remote version: 5.2.1.2
2021-12-16 00:20:54|dotnet-282887|INFO|ASF|Load() Loading STD global cache...
2021-12-16 00:20:56|dotnet-282887|INFO|ASF|Load() Validating STD global cache integrity...
2021-12-16 00:20:56|dotnet-282887|INFO|ASF|OnASFInit() SteamTokenDumperPlugin has been initialized successfully, thank you in advance for your help. The first submission will happen in approximately 47 minutes from now.
2021-12-16 00:20:57|dotnet-282887|INFO|ASF|Start() Starting IPC server...
2021-12-16 00:20:59|dotnet-282887|INFO|ASF|Start() IPC server ready!
render: text
validations:
required: true
@@ -115,9 +141,9 @@ body:
id: global-config
attributes:
label: Global ASF.json config file
description: The config can be found in `config` directory under `ASF.json` name. You can leave this field empty if not using one.
placeholder: |
Paste the file content here, no need for triple backtick tags
description: |
The config can be found in `config` directory under `ASF.json` name.
You can leave this field empty if not using one.
Ensure that your config has redacted (but NOT removed) potentially-sensitive properties, such as:
- IPCPassword (recommended)
@@ -127,14 +153,22 @@ body:
- WebProxyUsername (optionally, if exposing private details)
Redacting involves replacing sensitive details, for example with stars (***). You should refrain from removing config lines entirely, as their pure existence may be relevant and should be preserved.
placeholder: |
{
"AutoRestart": false,
"Headless": true,
"IPCPassword": "********",
"UpdateChannel": 2,
"SteamTokenDumperPluginEnabled": true
}
render: json
- type: textarea
id: bot-config
attributes:
label: BotName.json config of all affected bot instances
description: Bot config files can be found in `config` directory, ending with `json` extension. You can leave this field empty if you don't have any defined.
placeholder: |
Paste the file content here, no need for triple backtick tags
description: |
Bot config files can be found in `config` directory, ending with `json` extension.
You can leave this field empty if you don't have any defined.
Ensure that your config has redacted (but NOT removed) potentially-sensitive properties, such as:
- SteamLogin (mandatory)
@@ -145,6 +179,12 @@ body:
- SteamUserPermissions (optionally, only SteamIDs)
Redacting involves replacing sensitive details, for example with stars (***). You should refrain from removing config lines entirely, as their pure existence may be relevant and should be preserved.
placeholder: |
{
"Enabled": true,
"SteamLogin": "********",
"SteamPassword": "********"
}
render: json
- type: textarea
id: additional-info

View File

@@ -8,7 +8,7 @@ body:
label: Checklist
description: Ensure that our enhancement idea form is appropriate for you.
options:
- label: I read and understood ASF's **[Contributing Guidelines](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/CONTRIBUTING.md)**
- label: I read and understood ASF's **[Contributing guidelines](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/CONTRIBUTING.md)**
required: true
- label: I also read **[Setting-up](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Setting-up)** and **[FAQ](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/FAQ)**, I don't need **[help](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/SUPPORT.md)**, this is an enhancement idea
required: true
@@ -26,8 +26,12 @@ body:
id: enhancement-purpose
attributes:
label: Enhancement purpose
description: Purpose of the enhancement - if it solves some problem, precise in particular which. If it benefits the program in some other way, precise in particular why.
placeholder: Present the underlying reason why this enhancement makes sense, and what is the context of it.
description: |
Purpose of the enhancement - if it solves some problem, precise in particular which. If it benefits the program in some other way, precise in particular why.
Present the underlying reason why this enhancement makes sense, and what is the context of it.
placeholder: |
As of today ASF offers variety of beverages, such as latte macchiato or cappuccino. I'd appreciate if ASF offered some no-milk options as well, for example espresso or ristretto.
I believe it'd further improve the program offering the users wider selection, which is very convenient.
validations:
required: true
- type: textarea
@@ -35,25 +39,30 @@ body:
attributes:
label: Solution
description: What would you like to see as a solution to the purpose specified by you above?
placeholder: What would work for you?
placeholder: |
Simply add an option to brew some no-milk types of coffee. The existing logic is fine, we just need wider choice!
validations:
required: true
- type: textarea
id: why-existing-not-sufficient
attributes:
label: Why currently available solutions are not sufficient?
description: Evaluate the existing solutions in regards to your requirements.
placeholder: |
description: |
Evaluate the existing solutions in regards to your requirements.
If something you're suggesting is already possible, then explain to us why the currently available solutions are not sufficient.
If it's not possible yet, then explain to us why it should be.
placeholder: |
I'm allergic to milk, there is currently no option to pick a beverage that doesn't include it.
Temporarily I'm switching cup mid-brewing as a workaround, but that is suboptimal considering the milk wasted.
validations:
required: true
- type: dropdown
id: help
attributes:
label: Can you help us with this enhancement idea?
description: ASF is offered for free and our resources are limited. Helping us increases the chance of making it happen.
description: |
ASF is offered for free and our resources are limited.
Helping us increases the chance of making it happen.
options:
- Yes, I can code the solution myself and send a pull request
- Somehow, I can test and offer feedback, but can't code

View File

@@ -8,7 +8,7 @@ body:
label: Checklist
description: Ensure that our wiki suggestion form is appropriate for you.
options:
- label: I read and understood ASF's **[Contributing Guidelines](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/CONTRIBUTING.md)**
- label: I read and understood ASF's **[Contributing guidelines](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/CONTRIBUTING.md)**
required: true
- label: I also read **[Setting-up](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Setting-up)** and **[FAQ](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/FAQ)**, I don't need **[help](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/SUPPORT.md)**, this is a wiki suggestion
required: true
@@ -20,7 +20,9 @@ body:
id: wiki-page
attributes:
label: Wiki page
description: If this is a suggestion regarding an existing wiki page, please link it for reference. If the wiki page doesn't exist, suggest its title.
description: |
If this is a suggestion regarding an existing wiki page, please link it for reference.
If the wiki page doesn't exist, suggest its title.
placeholder: https://github.com/JustArchiNET/ArchiSteamFarm/wiki/???
validations:
required: true
@@ -28,26 +30,32 @@ body:
id: issue
attributes:
label: The issue
description: Please specify your issue in regards to our wiki documentation.
placeholder: |
description: |
Please specify your issue in regards to our wiki documentation.
If you're reporting a mistake/correction, state what is wrong.
If you're suggesting an idea, explain the details.
placeholder: |
As of today the wiki doesn't explain how to sing famous song composed by Rick Astley - Never Gonna Give You Up.
I'm sick of googling the lyrics every time I'm opening a complaint on your GitHub, so please consider just adding it along the other stuff.
validations:
required: true
- type: textarea
id: wrong-text
attributes:
label: Wrong text
description: The existing text on the wiki which you classify as wrong.
placeholder: |
description: |
The existing text on the wiki which you classify as wrong.
If you're suggesting a new page, paragraph or other addition to the wiki, then this section is not mandatory.
placeholder: |
Lack of song lyrics is what's wrong!
render: markdown
- type: textarea
id: suggested-improvement
attributes:
label: Suggested improvement
description: The new or corrected text that would satisfy your issue stated above. You may use **[markdown](https://guides.github.com/features/mastering-markdown)** for formatting.
description: |
The new or corrected text that would satisfy your issue stated above.
You may use **[markdown](https://guides.github.com/features/mastering-markdown)** for formatting.
placeholder: |
# Never Gonna Give You Up by Rick Astley

View File

@@ -1,6 +1,6 @@
### Notice
**Pre-releases are experimental versions that often contain unpatched bugs, work-in-progress features or rewritten implementations. If you don't consider yourself advanced user, please download **[latest stable release](https://github.com/JustArchiNET/ArchiSteamFarm/releases/latest)** instead. Pre-release versions are dedicated to users who know how to report bugs, deal with issues and give feedback - no technical support will be given. Check out ASF **[release cycle](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Release-cycle)** if you'd like to learn more.**
**Pre-releases are experimental versions that often contain unpatched bugs, work-in-progress features and rewritten implementations. If you don't consider yourself advanced user, please download **[latest stable release](https://github.com/JustArchiNET/ArchiSteamFarm/releases/latest)** instead. Pre-release versions are dedicated to users who know how to report bugs, deal with issues and give feedback - no technical support will be given. Check out ASF **[release cycle](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Release-cycle)** if you'd like to learn more.**
---
@@ -12,6 +12,6 @@ This is automated GitHub deployment, human-readable changelog should be availabl
### Support
ASF is available for free, this release was made possible thanks to the people that decided to support the project. If you're grateful for what we're doing, please consider donating. Developing ASF requires massive amount of time and knowledge, especially when it comes to Steam (and its problems). Even $1 is highly appreciated and shows that you care. Thank you!
ASF is available for free, this release was made possible thanks to the people that decided to support the project. If you're grateful for what we're doing, please consider a donation. Developing ASF requires massive amount of time and knowledge, especially when it comes to Steam (and its problems). Even $1 is highly appreciated and shows that you care. Thank you!
[![GitHub sponsor](https://img.shields.io/badge/GitHub-sponsor-ea4aaa.svg?logo=github-sponsors)](https://github.com/sponsors/JustArchi) [![Patreon support](https://img.shields.io/badge/Patreon-support-f96854.svg?logo=patreon)](https://www.patreon.com/JustArchi) [![Crypto donate](https://img.shields.io/badge/Crypto-donate-f7931a.svg?logo=bitcoin)](https://commerce.coinbase.com/checkout/0c23b844-c51b-45f4-9135-8db7c6fcf98e) [![PayPal.me donate](https://img.shields.io/badge/PayPal.me-donate-00457c.svg?logo=paypal)](https://paypal.me/JustArchi) [![PayPal donate](https://img.shields.io/badge/PayPal-donate-00457c.svg?logo=paypal)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HD2P2P3WGS5Y4) [![Revolut donate](https://img.shields.io/badge/Revolut-donate-0075eb.svg?logo=revolut)](https://pay.revolut.com/profile/ukaszyxm) [![Steam donate](https://img.shields.io/badge/Steam-donate-000000.svg?logo=steam)](https://steamcommunity.com/tradeoffer/new/?partner=46697991&token=0ix2Ruv_)
[![GitHub sponsor](https://img.shields.io/badge/GitHub-sponsor-ea4aaa.svg?logo=github-sponsors)](https://github.com/sponsors/JustArchi) [![Patreon support](https://img.shields.io/badge/Patreon-support-f96854.svg?logo=patreon)](https://www.patreon.com/JustArchi) [![Crypto donate](https://img.shields.io/badge/Crypto-donate-f7931a.svg?logo=bitcoin)](https://commerce.coinbase.com/checkout/0c23b844-c51b-45f4-9135-8db7c6fcf98e) [![PayPal.me donate](https://img.shields.io/badge/PayPal.me-donate-00457c.svg?logo=paypal)](https://paypal.me/JustArchi) [![PayPal donate](https://img.shields.io/badge/PayPal-donate-00457c.svg?logo=paypal)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HD2P2P3WGS5Y4) [![Revolut donate](https://img.shields.io/badge/Revolut-donate-0075eb.svg?logo=revolut)](https://pay.revolut.com/justarchi) [![Steam donate](https://img.shields.io/badge/Steam-donate-000000.svg?logo=steam)](https://steamcommunity.com/tradeoffer/new/?partner=46697991&token=0ix2Ruv_)

2
.github/SUPPORT.md vendored
View File

@@ -4,4 +4,4 @@ Our **[wiki](https://github.com/JustArchiNET/ArchiSteamFarm/wiki)** is the offic
We also have three independent support channels dedicated to our ASF users, in case you couldn't manage to solve the issue yourself. We answer all support and technical matters in our **[GitHub discussions](https://github.com/JustArchiNET/ArchiSteamFarm/discussions/categories/support)**, **[Steam group](https://steamcommunity.com/groups/archiasf/discussions/1)**, and on our **[Discord server](https://discord.gg/hSQgt8j)**. You're free to use the support channel that matches your preferences, although keep in mind that you have a higher chance solving your issue on the GitHub or Steam, where we're doing our best to answer all questions that couldn't be answered by our community itself (as opposed to Discord server where we're not active 24/7 and therefore not always able to answer).
GitHub issues are being used solely for ASF development, especially in regards to bugs and enhancements. We have a very strict policy regarding that, as GitHub issues is **not** a general support channel, it's dedicated exclusively to ASF development and we're not answering common ASF matters there, as we have appropriate support channels (mentioned above) for that. Common matters include not only general questions or issues that are obviously related to program usage, but also users reporting "bugs" that are clearly considered intended behaviour coming for example (and mainly) from misconfiguration or lack of understanding how the program works. If you're not sure whether your matter relates to ASF development or not, especially if you're not sure if it's a bug or intended behaviour, we recommend to use a support channel instead, where we'll answer you in calm atmosphere and forward your matter to GitHub if deemed appropriate. Invalid GitHub issues will be closed immediately and won't be answered.
GitHub **issues** (unlike discussions), are being used solely for ASF development, especially in regards to bugs and enhancements. We have a very strict policy regarding that, as GitHub issues is **not** a general support channel, it's dedicated exclusively to ASF development and we're not answering common ASF matters there, as we have appropriate support channels (mentioned above) for that. Common matters include not only general questions or issues that are obviously related to program usage, but also users reporting "bugs" that are clearly considered intended behaviour coming for example (and mainly) from misconfiguration or lack of understanding how the program works. If you're not sure whether your matter relates to ASF development or not, especially if you're not sure if it's a bug or intended behaviour, we recommend to use a support channel instead, where we'll answer you in calm atmosphere and forward your matter as GitHub issue if deemed appropriate. Invalid GitHub issues will be closed immediately and won't be answered.

11
.github/crowdin.yml vendored
View File

@@ -1,3 +1,4 @@
"base_path": ".."
"preserve_hierarchy": true
"files": [
{
@@ -5,7 +6,10 @@
"translation": "/ArchiSteamFarm/Localization/Strings.%locale%.resx",
"translation_replace": {
".lol-US.resx": ".qps-Ploc.resx",
".sr-CS.resx": ".sr-Latn.resx"
".sr-CS.resx": ".sr-Latn.resx",
".zh-CN.resx": ".zh-Hans.resx",
".zh-HK.resx": ".zh-Hant-HK.resx",
".zh-TW.resx": ".zh-Hant.resx"
}
},
{
@@ -13,7 +17,10 @@
"translation": "/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Localization/Strings.%locale%.resx",
"translation_replace": {
".lol-US.resx": ".qps-Ploc.resx",
".sr-CS.resx": ".sr-Latn.resx"
".sr-CS.resx": ".sr-Latn.resx",
".zh-CN.resx": ".zh-Hans.resx",
".zh-HK.resx": ".zh-Hant-HK.resx",
".zh-TW.resx": ".zh-Hant.resx"
}
},
{

View File

@@ -17,13 +17,8 @@
{
// TODO: <= 3.1 for Mono support, last failed version 6.12, https://steamcommunity.com/groups/archiasf/discussions/1/2997673517556002529
"allowedVersions": "<= 3.1",
"matchManagers": [ "nuget" ],
"matchPackageNames": [ "Microsoft.Extensions.Configuration.Json", "Microsoft.Extensions.Logging.Configuration" ]
},
{
// TODO: <= 2.2.4 due to https://github.com/microsoft/testfx/issues/906, should be resolved with v2.2.8+ (?)
"allowedVersions": "<= 2.2.4",
"groupName": "MSTest packages",
"matchPackagePatterns": ["^MSTest\\..+"]
}
]
}

View File

@@ -19,12 +19,12 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.2
with:
submodules: recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v1.8.2
uses: actions/setup-dotnet@v2.1.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.1
uses: crowdin/github-action@1.4.11
with:
crowdin_branch_name: main
config: '.github/crowdin.yml'

View File

@@ -17,15 +17,15 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.2
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.6.0
uses: docker/setup-buildx-action@v2.0.0
- name: Build ${{ matrix.configuration }} Docker image from ${{ matrix.file }}
uses: docker/build-push-action@v2.7.0
uses: docker/build-push-action@v3.1.1
with:
context: .
file: ${{ matrix.file }}

View File

@@ -15,22 +15,22 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.2
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.6.0
uses: docker/setup-buildx-action@v2.0.0
- name: Login to ghcr.io
uses: docker/login-action@v1.10.0
uses: docker/login-action@v2.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v1.10.0
uses: docker/login-action@v2.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -55,7 +55,7 @@ jobs:
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
- name: Build and publish Docker image from Dockerfile.Service
uses: docker/build-push-action@v2.7.0
uses: docker/build-push-action@v3.1.1
with:
context: .
file: Dockerfile.Service

View File

@@ -16,22 +16,22 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.2
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.6.0
uses: docker/setup-buildx-action@v2.0.0
- name: Login to ghcr.io
uses: docker/login-action@v1.10.0
uses: docker/login-action@v2.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v1.10.0
uses: docker/login-action@v2.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -55,7 +55,7 @@ jobs:
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
- name: Build and publish Docker image from Dockerfile
uses: docker/build-push-action@v2.7.0
uses: docker/build-push-action@v3.1.1
with:
context: .
platforms: ${{ env.PLATFORMS }}
@@ -70,8 +70,7 @@ jobs:
push: true
- name: Update DockerHub repository description
continue-on-error: true
uses: peter-evans/dockerhub-description@v2.4.3
uses: peter-evans/dockerhub-description@v3.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

View File

@@ -16,22 +16,22 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.2
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.6.0
uses: docker/setup-buildx-action@v2.0.0
- name: Login to ghcr.io
uses: docker/login-action@v1.10.0
uses: docker/login-action@v2.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v1.10.0
uses: docker/login-action@v2.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -56,7 +56,7 @@ jobs:
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
- name: Build and publish Docker image from Dockerfile
uses: docker/build-push-action@v2.7.0
uses: docker/build-push-action@v3.1.1
with:
context: .
platforms: ${{ env.PLATFORMS }}

View File

@@ -11,5 +11,5 @@ jobs:
- name: Lock inactive threads
uses: dessant/lock-threads@v3.0.0
with:
issue-inactive-days: 30
pr-inactive-days: 30
issue-inactive-days: 60
pr-inactive-days: 60

View File

@@ -25,12 +25,12 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.2
with:
submodules: recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v1.8.2
uses: actions/setup-dotnet@v2.1.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.4.1
uses: actions/setup-node@v3.4.1
with:
check-latest: true
node-version: ${{ env.NODE_JS_VERSION }}
@@ -119,7 +119,7 @@ jobs:
- name: Publish ArchiSteamFarm on Unix
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
env:
VARIANTS: generic linux-arm linux-arm64 linux-x64 osx-x64 win-x64 # NOTE: When modifying variants, don't forget to update ASF_VARIANT definitions in SharedInfo.cs!
VARIANTS: generic linux-arm linux-arm64 linux-x64 osx-arm64 osx-x64 win-x64 # NOTE: When modifying variants, don't forget to update ASF_VARIANT definitions in SharedInfo.cs!
shell: sh
run: |
set -eu
@@ -128,22 +128,11 @@ jobs:
if [ "$1" = 'generic' ]; then
variantArgs="-p:TargetLatestRuntimePatch=false -p:UseAppHost=false"
else
variantArgs="-p:PublishSingleFile=true -p:PublishTrimmed=true -r $1"
variantArgs="-p:PublishSingleFile=true -p:PublishTrimmed=true -r $1 --self-contained"
fi
dotnet publish ArchiSteamFarm -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/${1}" "-p:ASFVariant=$1" -p:ContinuousIntegrationBuild=true --no-restore --nologo $variantArgs
# If we're including any overlay for this variant, copy it to output directory
variant_os="$(echo "$1" | cut -d '-' -f 1)"
if [ -d "ArchiSteamFarm/overlay/${variant_os}" ]; then
cp -pR "ArchiSteamFarm/overlay/${variant_os}/"* "out/${1}"
fi
if [ "$1" != "$variant_os" ] && [ -d "ArchiSteamFarm/overlay/${1}" ]; then
cp -pR "ArchiSteamFarm/overlay/${1}/"* "out/${1}"
fi
# If we're including SteamTokenDumper plugin for this framework, copy it to output directory
if [ -d "out/${STEAM_TOKEN_DUMPER_NAME}/${NET_CORE_VERSION}" ]; then
mkdir -p "out/${1}/plugins/${STEAM_TOKEN_DUMPER_NAME}"
@@ -177,7 +166,7 @@ jobs:
# Create the final zip file
case "$(uname -s)" in
"Darwin")
# We prefer to use zip on OS X as 7z implementation on that OS doesn't handle file permissions (chmod +x)
# We prefer to use zip on macOS as 7z implementation on that OS doesn't handle file permissions (chmod +x)
if command -v zip >/dev/null; then
(
cd "${GITHUB_WORKSPACE}/out/${1}"
@@ -220,7 +209,7 @@ jobs:
- name: Publish ArchiSteamFarm on Windows
if: startsWith(matrix.os, 'windows-')
env:
VARIANTS: generic generic-netf linux-arm linux-arm64 linux-x64 osx-x64 win-x64 # NOTE: When modifying variants, don't forget to update ASF_VARIANT definitions in SharedInfo.cs!
VARIANTS: generic generic-netf linux-arm linux-arm64 linux-x64 osx-arm64 osx-x64 win-x64 # NOTE: When modifying variants, don't forget to update ASF_VARIANT definitions in SharedInfo.cs!
shell: pwsh
run: |
Set-StrictMode -Version Latest
@@ -245,7 +234,7 @@ jobs:
if ($variant -like 'generic*') {
$variantArgs = '-p:TargetLatestRuntimePatch=false', '-p:UseAppHost=false'
} else {
$variantArgs = '-p:PublishSingleFile=true', '-p:PublishTrimmed=true', '-r', "$variant"
$variantArgs = '-p:PublishSingleFile=true', '-p:PublishTrimmed=true', '-r', "$variant", '--self-contained'
}
dotnet publish ArchiSteamFarm -c "$env:CONFIGURATION" -f "$targetFramework" -o "out\$variant" "-p:ASFVariant=$variant" -p:ContinuousIntegrationBuild=true --no-restore --nologo $variantArgs
@@ -254,17 +243,6 @@ jobs:
throw "Last command failed."
}
# If we're including any overlay for this variant, copy it to output directory
$variant_os = $variant.Split('-', 2)[0];
if (Test-Path "ArchiSteamFarm\overlay\$variant_os" -PathType Container) {
Copy-Item "ArchiSteamFarm\overlay\$variant_os\*" "out\$variant" -Recurse
}
if (($variant -ne $variant_os) -and (Test-Path "ArchiSteamFarm\overlay\$variant" -PathType Container)) {
Copy-Item "ArchiSteamFarm\overlay\$variant\*" "out\$variant" -Recurse
}
# If we're including SteamTokenDumper plugin for this framework, copy it to output directory
if (Test-Path "out\$env:STEAM_TOKEN_DUMPER_NAME\$targetFramework" -PathType Container) {
if (!(Test-Path "out\$variant\plugins\$env:STEAM_TOKEN_DUMPER_NAME" -PathType Container)) {
@@ -325,56 +303,64 @@ jobs:
foreach ($variant in $env:VARIANTS.Split([char[]] $null, [System.StringSplitOptions]::RemoveEmptyEntries)) {
Start-Job -Name "$variant" $PublishBlock -ArgumentList "$variant"
# Limit active jobs in parallel to help with memory usage
$jobs = $(Get-Job -State Running)
while (@($jobs).Count -ge 5) {
Wait-Job -Job $jobs -Any | Out-Null
$jobs = $(Get-Job -State Running)
}
}
Get-Job | Receive-Job -Wait
- name: Upload ASF-generic
continue-on-error: true
uses: actions/upload-artifact@v2.2.4
uses: actions/upload-artifact@v3.1.0
with:
name: ${{ matrix.os }}_ASF-generic
path: out/ASF-generic.zip
- name: Upload ASF-generic-netf
continue-on-error: true
if: startsWith(matrix.os, 'windows-')
uses: actions/upload-artifact@v2.2.4
uses: actions/upload-artifact@v3.1.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.2.4
uses: actions/upload-artifact@v3.1.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.2.4
uses: actions/upload-artifact@v3.1.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.2.4
uses: actions/upload-artifact@v3.1.0
with:
name: ${{ matrix.os }}_ASF-linux-x64
path: out/ASF-linux-x64.zip
- name: Upload ASF-osx-arm64
uses: actions/upload-artifact@v3.1.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.2.4
uses: actions/upload-artifact@v3.1.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.2.4
uses: actions/upload-artifact@v3.1.0
with:
name: ${{ matrix.os }}_ASF-win-x64
path: out/ASF-win-x64.zip
@@ -386,55 +372,61 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.2
# 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.0.10
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-generic
path: out
- name: Download ASF-generic-netf artifact from windows-latest
uses: actions/download-artifact@v2.0.10
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-generic-netf
path: out
- name: Download ASF-linux-arm artifact from windows-latest
uses: actions/download-artifact@v2.0.10
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-linux-arm
path: out
- name: Download ASF-linux-arm64 artifact from windows-latest
uses: actions/download-artifact@v2.0.10
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-linux-arm64
path: out
- name: Download ASF-linux-x64 artifact from windows-latest
uses: actions/download-artifact@v2.0.10
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-linux-x64
path: out
- name: Download ASF-osx-arm64 artifact from windows-latest
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-osx-arm64
path: out
- name: Download ASF-osx-x64 artifact from windows-latest
uses: actions/download-artifact@v2.0.10
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_ASF-osx-x64
path: out
- name: Download ASF-win-x64 artifact from windows-latest
uses: actions/download-artifact@v2.0.10
uses: actions/download-artifact@v3.0.0
with:
name: windows-latest_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@v5.1.0
with:
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
@@ -451,15 +443,13 @@ jobs:
)
- name: Upload SHA512SUMS
continue-on-error: true
uses: actions/upload-artifact@v2.2.4
uses: actions/upload-artifact@v3.1.0
with:
name: SHA512SUMS
path: out/SHA512SUMS
- name: Upload SHA512SUMS.sign
continue-on-error: true
uses: actions/upload-artifact@v2.2.4
uses: actions/upload-artifact@v3.1.0
with:
name: SHA512SUMS.sign
path: out/SHA512SUMS.sign
@@ -525,6 +515,16 @@ jobs:
asset_name: ASF-linux-x64.zip
asset_content_type: application/zip
- name: Upload ASF-osx-arm64 to GitHub release
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.github_release.outputs.upload_url }}
asset_path: out/ASF-osx-arm64.zip
asset_name: ASF-osx-arm64.zip
asset_content_type: application/zip
- name: Upload ASF-osx-x64 to GitHub release
uses: actions/upload-release-asset@v1.0.2
env:

View File

@@ -10,7 +10,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3.0.2
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.1
uses: crowdin/github-action@1.4.11
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@v5.1.0
with:
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
git_config_global: true

35
.gitignore vendored
View File

@@ -18,13 +18,16 @@ ArchiSteamFarm/logs
# Ignore standard out folders for publishing
**/out
# JetBrains Rider
.idea/
# _ _
# | | (_) _ __ _ _ __ __
# | | | || '_ \ | | | |\ \/ /
# | |___ | || | | || |_| | > <
# |_____||_||_| |_| \__,_|/_/\_\
#
# https://github.com/github/gitignore/blob/master/Global/Linux.gitignore
# https://github.com/github/gitignore/blob/main/Global/Linux.gitignore
# 4f7062e132d7f88e68ab737e64fef872bd3a491f
*~
@@ -47,7 +50,7 @@ ArchiSteamFarm/logs
# | | | | | || (_| || (__ | |_| | ___) |
# |_| |_| |_| \__,_| \___| \___/ |____/
#
# https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
# 2bb963b16a1957c865335e53537036c2e97399b5
# General
@@ -84,7 +87,7 @@ Temporary Items
# |_| |_| \___/ |_| |_| \___/ |____/ \___| \_/ \___||_| \___/ | .__/
# |_|
#
# https://github.com/github/gitignore/blob/master/Global/MonoDevelop.gitignore
# https://github.com/github/gitignore/blob/main/Global/MonoDevelop.gitignore
# e8b2e1a9cc7c9ca49bb05c20a4c4491b85feba6d
#User Specific
@@ -102,13 +105,13 @@ test-results/
# \ V / | |\__ \| |_| || (_| || | ___) || |_ | |_| || (_| || || (_) |
# \_/ |_||___/ \__,_| \__,_||_||____/ \__| \__,_| \__,_||_| \___/
#
# https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# 888439ee893d0097862f1d510585bd0e3cfd500f
# https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# 491040e88a572d300a59484cb78c86c5e944b70a
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
@@ -313,9 +316,6 @@ PublishScripts/
*.nuget.props
*.nuget.targets
# Nuget personal access tokens and Credentials
nuget.config
# Microsoft Azure Build Output
csx/
*.build.csdef
@@ -404,6 +404,17 @@ node_modules/
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
@@ -460,6 +471,9 @@ ASALocalRun/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
@@ -491,7 +505,6 @@ FodyWeavers.xsd
*.msp
# JetBrains Rider
.idea/
*.sln.iml
# __ __ _ _
@@ -500,7 +513,7 @@ FodyWeavers.xsd
# \ V V / | || | | || (_| || (_) |\ V V / \__ \
# \_/\_/ |_||_| |_| \__,_| \___/ \_/\_/ |___/
#
# https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
# 5808b77453dec299d4daf8557b05a80be832a5b8
# Windows thumbnail cache files

2
ASF-ui

Submodule ASF-ui updated: 88fbcc1ac4...f486cd15ab

View File

@@ -5,6 +5,7 @@
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" IncludeAssets="compile" />
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,48 +20,25 @@
// limitations under the License.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.Web.Responses;
using Newtonsoft.Json;
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
// This is example class that shows how you can call third-party services within your plugin
// You've always wanted from your ASF to post cats, right? Now is your chance!
// P.S. The code is almost 1:1 copy from the one I use in ArchiBot, you can thank me later
internal static class CatAPI {
private const string URL = "https://aws.random.cat";
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
internal static async Task<string?> GetRandomCatURL(WebBrowser webBrowser) {
if (webBrowser == null) {
throw new ArgumentNullException(nameof(webBrowser));
}
// This is example class that shows how you can call third-party services within your plugin
// You've always wanted from your ASF to post cats, right? Now is your chance!
// P.S. The code is almost 1:1 copy from the one I use in ArchiBot, you can thank me later
internal static class CatAPI {
private const string URL = "https://aws.random.cat";
Uri request = new($"{URL}/meow");
internal static async Task<Uri?> GetRandomCatURL(WebBrowser webBrowser) {
ArgumentNullException.ThrowIfNull(webBrowser);
ObjectResponse<MeowResponse>? response = await webBrowser.UrlGetToJsonObject<MeowResponse>(request).ConfigureAwait(false);
Uri request = new($"{URL}/meow");
if (response == null) {
return null;
}
ObjectResponse<MeowResponse>? response = await webBrowser.UrlGetToJsonObject<MeowResponse>(request).ConfigureAwait(false);
if (string.IsNullOrEmpty(response.Content.Link)) {
throw new InvalidOperationException(nameof(response.Content.Link));
}
return Uri.EscapeDataString(response.Content.Link);
}
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
private sealed class MeowResponse {
[JsonProperty(PropertyName = "file", Required = Required.Always)]
internal readonly string Link = "";
[JsonConstructor]
private MeowResponse() { }
}
#pragma warning restore CA1812 // False positive, the class is used during json deserialization
return response?.Content?.URL;
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -27,26 +27,26 @@ using ArchiSteamFarm.IPC.Controllers.Api;
using ArchiSteamFarm.IPC.Responses;
using Microsoft.AspNetCore.Mvc;
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
// This is an example class which shows you how you can extend ASF's API with your own custom API routes and controllers
// You're free to decide whether you want to integrate with existing ASF concepts (such as ArchiController/GenericResponse), or roll out your own
// All API controllers will be discovered during our Kestrel initialization using attributes mapping, you're also getting usual ASF goodies such as swagger documentation out of the box
[Route("/Api/Cat")]
public sealed class CatController : ArchiController {
/// <summary>
/// Fetches URL of a random cat picture.
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(GenericResponse<string>), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.ServiceUnavailable)]
public async Task<ActionResult<GenericResponse>> CatGet() {
if (ASF.WebBrowser == null) {
throw new InvalidOperationException(nameof(ASF.WebBrowser));
}
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
string? link = await CatAPI.GetRandomCatURL(ASF.WebBrowser).ConfigureAwait(false);
return !string.IsNullOrEmpty(link) ? Ok(new GenericResponse<string>(link)) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false));
// This is an example class which shows you how you can extend ASF's API with your own custom API routes and controllers
// You're free to decide whether you want to integrate with existing ASF concepts (such as ArchiController/GenericResponse), or roll out your own
// All API controllers will be discovered during our Kestrel initialization using attributes mapping, you're also getting usual ASF goodies such as swagger documentation out of the box
[Route("/Api/Cat")]
public sealed class CatController : ArchiController {
/// <summary>
/// Fetches URL of a random cat picture.
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(GenericResponse<Uri>), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.ServiceUnavailable)]
public async Task<ActionResult<GenericResponse>> CatGet() {
if (ASF.WebBrowser == null) {
throw new InvalidOperationException(nameof(ASF.WebBrowser));
}
Uri? url = await CatAPI.GetRandomCatURL(ASF.WebBrowser).ConfigureAwait(false);
return url != null ? Ok(new GenericResponse<Uri>(url)) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false));
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -28,154 +28,160 @@ using ArchiSteamFarm.Core;
using ArchiSteamFarm.Plugins.Interfaces;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Storage;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SteamKit2;
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
// In order for your plugin to work, it must export generic ASF's IPlugin interface
[Export(typeof(IPlugin))]
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
// Your plugin class should inherit the plugin interfaces it wants to handle
// If you do not want to handle a particular action (e.g. OnBotMessage that is offered in IBotMessage), it's the best idea to not inherit it at all
// This will keep your code compact, efficient and less dependent. You can always add additional interfaces when you'll need them, this example project will inherit quite a bit of them to show you potential usage
internal sealed class ExamplePlugin : IASF, IBot, IBotCommand, IBotConnection, IBotFriendRequest, IBotMessage, IBotModules, IBotTradeOffer {
// This is used for identification purposes, typically you want to use a friendly name of your plugin here, such as the name of your main class
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
public string Name => nameof(ExamplePlugin);
// In order for your plugin to work, it must export generic ASF's IPlugin interface
[Export(typeof(IPlugin))]
// This will be displayed to the user and written in the log file, typically you should point it to the version of your library, but alternatively you can do some more advanced logic if you'd like to
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
public Version Version => typeof(ExamplePlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
// Your plugin class should inherit the plugin interfaces it wants to handle
// If you do not want to handle a particular action (e.g. OnBotMessage that is offered in IBotMessage), it's the best idea to not inherit it at all
// This will keep your code compact, efficient and less dependent. You can always add additional interfaces when you'll need them, this example project will inherit quite a bit of them to show you potential usage
internal sealed class ExamplePlugin : IASF, IBot, IBotCommand2, IBotConnection, IBotFriendRequest, IBotMessage, IBotModules, IBotTradeOffer {
// This is used for identification purposes, typically you want to use a friendly name of your plugin here, such as the name of your main class
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
public string Name => nameof(ExamplePlugin);
// Plugins can expose custom properties for our GET /Api/Plugins API call, simply annotate them with [JsonProperty] (or keep public)
[JsonProperty]
public bool CustomIsEnabledField { get; private set; } = true;
// This will be displayed to the user and written in the log file, typically you should point it to the version of your library, but alternatively you can do some more advanced logic if you'd like to
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
public Version Version => typeof(ExamplePlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
// This method, apart from being called before any bot initialization takes place, allows you to read custom global config properties that are not recognized by ASF
// Thanks to that, you can extend default ASF config with your own stuff, then parse it here in order to customize your plugin during runtime
// Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately
// In addition to that, this method also guarantees that all plugins were already OnLoaded(), which allows cross-plugins-communication to be possible
public void OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
if (additionalConfigProperties == null) {
return;
}
// Plugins can expose custom properties for our GET /Api/Plugins API call, simply annotate them with [JsonProperty] (or keep public)
[JsonProperty]
public bool CustomIsEnabledField { get; private set; } = true;
foreach ((string configProperty, JToken configValue) in additionalConfigProperties) {
// It's a good idea to prefix your custom properties with the name of your plugin, so there will be no possible conflict of ASF or other plugins using the same name, neither now or in the future
switch (configProperty) {
case nameof(ExamplePlugin) + "TestProperty" when configValue.Type == JTokenType.Boolean:
bool exampleBooleanValue = configValue.Value<bool>();
ASF.ArchiLogger.LogGenericInfo($"{nameof(ExamplePlugin)}TestProperty boolean property has been found with a value of: {exampleBooleanValue}");
// This method, apart from being called before any bot initialization takes place, allows you to read custom global config properties that are not recognized by ASF
// Thanks to that, you can extend default ASF config with your own stuff, then parse it here in order to customize your plugin during runtime
// Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately
// In addition to that, this method also guarantees that all plugins were already OnLoaded(), which allows cross-plugins-communication to be possible
public Task OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
if (additionalConfigProperties == null) {
return Task.CompletedTask;
}
break;
}
foreach ((string configProperty, JToken configValue) in additionalConfigProperties) {
// It's a good idea to prefix your custom properties with the name of your plugin, so there will be no possible conflict of ASF or other plugins using the same name, neither now or in the future
switch (configProperty) {
case $"{nameof(ExamplePlugin)}TestProperty" when configValue.Type == JTokenType.Boolean:
bool exampleBooleanValue = configValue.Value<bool>();
ASF.ArchiLogger.LogGenericInfo($"{nameof(ExamplePlugin)}TestProperty boolean property has been found with a value of: {exampleBooleanValue}");
break;
}
}
// This method is called when unknown command is received (starting with CommandPrefix)
// This allows you to recognize the command yourself and implement custom commands
// Keep in mind that there is no guarantee what is the actual access of steamID, so you should do the appropriate access checking yourself
// You can use either ASF's default functions for that, or implement your own logic as you please
// Since ASF already had to do initial parsing in order to determine that the command is unknown, args[] are splitted using standard ASF delimiters
// If by any chance you want to handle message in its raw format, you also have it available, although for usual ASF pattern you can most likely stick with args[] exclusively. The message has CommandPrefix already stripped for your convenience
// If you do not recognize the command, just return null/empty and allow ASF to gracefully return "unknown command" to user on usual basis
public async Task<string?> OnBotCommand(Bot bot, ulong steamID, string message, string[] args) {
// In comparison with OnBotMessage(), we're using asynchronous CatAPI call here, so we declare our method as async and return the message as usual
// Notice how we handle access here as well, it'll work only for FamilySharing+
switch (args[0].ToUpperInvariant()) {
case "CAT" when bot.HasAccess(steamID, BotConfig.EAccess.FamilySharing):
// Notice how we can decide whether to use bot's AWH WebBrowser or ASF's one. For Steam-related requests, AWH's one should always be used, for third-party requests like those it doesn't really matter
// Still, it makes sense to pass AWH's one, so in case you get some errors or alike, you know from which bot instance they come from. It's similar to using Bot's ArchiLogger compared to ASF's one
string? randomCatURL = await CatAPI.GetRandomCatURL(bot.ArchiWebHandler.WebBrowser).ConfigureAwait(false);
// ASF interface methods usually expect a Task as a return value, this allows you to optionally implement async operations in your functions (with async Task function signature)
// If your method does not implement any async operations (is fully synchronous), you could in theory still mark it as async, but a better idea is to just return Task.CompletedTask from it, like here
return Task.CompletedTask;
}
return !string.IsNullOrEmpty(randomCatURL) ? randomCatURL : "God damn it, we're out of cats, care to notify my master? Thanks!";
default:
return null;
}
}
// This method is called when unknown command is received (starting with CommandPrefix)
// This allows you to recognize the command yourself and implement custom commands
// Keep in mind that there is no guarantee what is the actual access of steamID, so you should do the appropriate access checking yourself
// You can use either ASF's default functions for that, or implement your own logic as you please
// Since ASF already had to do initial parsing in order to determine that the command is unknown, args[] are splitted using standard ASF delimiters
// If by any chance you want to handle message in its raw format, you also have it available, although for usual ASF pattern you can most likely stick with args[] exclusively. The message has CommandPrefix already stripped for your convenience
// If you do not recognize the command, just return null/empty and allow ASF to gracefully return "unknown command" to user on usual basis
public async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
// In comparison with OnBotMessage(), we're using asynchronous CatAPI call here, so we declare our method as async and return the message as usual
// Notice how we handle access here as well, it'll work only for FamilySharing+
switch (args[0].ToUpperInvariant()) {
case "CAT" when access >= EAccess.FamilySharing:
// Notice how we can decide whether to use bot's AWH WebBrowser or ASF's one. For Steam-related requests, AWH's one should always be used, for third-party requests like those it doesn't really matter
// Still, it makes sense to pass AWH's one, so in case you get some errors or alike, you know from which bot instance they come from. It's similar to using Bot's ArchiLogger compared to ASF's one
Uri? randomCatURL = await CatAPI.GetRandomCatURL(bot.ArchiWebHandler.WebBrowser).ConfigureAwait(false);
// This method is called when bot is destroyed, e.g. on config removal
// You should ensure that all of your references to this bot instance are cleared - most of the time this is anything you created in OnBotInit(), including deep roots in your custom modules
// This doesn't have to be done immediately (e.g. no need to cancel existing work), but it should be done in timely manner when everything is finished
// Doing so will allow the garbage collector to dispose the bot afterwards, refraining from doing so will create a "memory leak" by keeping the reference alive
public void OnBotDestroy(Bot bot) { }
// This method is called when bot is disconnected from Steam network, you may want to use this info in some kind of way, or not
// ASF tries its best to provide logical reason why the disconnection has happened, and will use EResult.OK if the disconnection was initiated by us (e.g. as part of a command)
// Still, you should take anything other than EResult.OK with a grain of salt, unless you want to assume that Steam knows why it disconnected us (hehe, you bet)
public void OnBotDisconnected(Bot bot, EResult reason) { }
// This method is called when bot receives a friend request or group invite that ASF isn't willing to accept
// It allows you to generate a response whether ASF should accept it (true) or proceed like usual (false)
// If you wanted to do extra filtering (e.g. friend requests only), you can interpret the steamID as SteamID (SteamKit2 type) and then operate on AccountType
// As an example, we'll run a trade bot that is open to all friend/group invites, therefore we'll accept all of them here
public Task<bool> OnBotFriendRequest(Bot bot, ulong steamID) => Task.FromResult(true);
// This method is called at the end of Bot's constructor
// You can initialize all your per-bot structures here
// In general you should do that only when you have a particular need of custom modules or alike, since ASF's plugin system will always provide bot to you as a function argument
public void OnBotInit(Bot bot) {
// Apart of those two that are already provided by ASF, you can also initialize your own logger with your plugin's name, if needed
bot.ArchiLogger.LogGenericInfo($"Our bot named {bot.BotName} has been initialized, and we're letting you know about it from our {nameof(ExamplePlugin)}!");
ASF.ArchiLogger.LogGenericWarning("In case we won't have a bot reference or have something process-wide to log, we can also use ASF's logger!");
}
// This method, apart from being called during bot modules initialization, allows you to read custom bot config properties that are not recognized by ASF
// Thanks to that, you can extend default bot config with your own stuff, then parse it here in order to customize your plugin during runtime
// Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately
// Also keep in mind that this function can be called multiple times, e.g. when user edits his bot configs during runtime
// Take a look at OnASFInit() for example parsing code
public async void OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
// ASF marked this message as synchronous, in case we have async code to execute, we can just use async void return
// For example, we'll ensure that every bot starts paused regardless of Paused property, in order to do this, we'll just call Pause here in InitModules()
// Thanks to the fact that this method is called with each bot config reload, we'll ensure that our bot stays paused even if it'd get unpaused otherwise
bot.ArchiLogger.LogGenericInfo("Pausing this bot as asked from the plugin");
await bot.Actions.Pause(true).ConfigureAwait(false);
}
// This method is called when the bot is successfully connected to Steam network and it's a good place to schedule any on-connected tasks, as AWH is also expected to be available shortly
public void OnBotLoggedOn(Bot bot) { }
// This method is called when bot receives a message that is NOT a command (in other words, a message that doesn't start with CommandPrefix)
// Normally ASF entirely ignores such messages as the program should not respond to something that isn't recognized
// Therefore this function allows you to catch all such messages and handle them yourself
// Keep in mind that there is no guarantee what is the actual access of steamID, so you should do the appropriate access checking yourself
// You can use either ASF's default functions for that, or implement your own logic as you please
// If you do not intend to return any response to user, just return null/empty and ASF will proceed with the silence as usual
public Task<string?> OnBotMessage(Bot bot, ulong steamID, string message) {
// Normally ASF will expect from you async-capable responses, such as Task<string>. This allows you to make your code fully asynchronous which is a core foundation on which ASF is built upon
// Since in this method we're not doing any async stuff, instead of defining this method as async (pointless), we just need to wrap our responses in Task.FromResult<>()
if (Bot.BotsReadOnly == null) {
throw new InvalidOperationException(nameof(Bot.BotsReadOnly));
}
// As a starter, we can for example ignore messages sent from our own bots, since otherwise they can run into a possible infinite loop of answering themselves
if (Bot.BotsReadOnly.Values.Any(existingBot => existingBot.SteamID == steamID)) {
return Task.FromResult<string?>(null);
}
// If this message doesn't come from one of our bots, we can reply to the user in some pre-defined way
bot.ArchiLogger.LogGenericTrace("Hey boss, we got some unknown message here!");
return Task.FromResult((string?) "I didn't get that, did you mean to use a command?");
}
// This method is called when bot receives a trade offer that ASF isn't willing to accept (ignored and rejected trades)
// It allows you not only to analyze such trades, but generate a response whether ASF should accept it (true), or proceed like usual (false)
// Thanks to that, you can implement custom rules for all trades that aren't handled by ASF, for example cross-set trading on your own custom rules
// You'd implement your own logic here, as an example we'll allow all trades to be accepted if the bot's name starts from "TrashBot"
public Task<bool> OnBotTradeOffer(Bot bot, TradeOffer tradeOffer) => Task.FromResult(bot.BotName.StartsWith("TrashBot", StringComparison.OrdinalIgnoreCase));
// This is the earliest method that will be called, right after loading the plugin, long before any bot initialization takes place
// It's a good place to initialize all potential (non-bot-specific) structures that you will need across lifetime of your plugin, such as global timers, concurrent dictionaries and alike
// If you do not have any global structures to initialize, you can leave this function empty
// At this point you can access core ASF's functionality, such as logging, but more advanced structures (like ASF's WebBrowser) will be available in OnASFInit(), which itself takes place after every plugin gets OnLoaded()
// Typically you should use this function only for preparing core structures of your plugin, and optionally also sending a message to the user (e.g. support link, welcome message or similar), ASF-specific things should usually happen in OnASFInit()
public void OnLoaded() {
ASF.ArchiLogger.LogGenericInfo($"Hey! Thanks for checking if our example plugin works fine, this is a confirmation that indeed {nameof(OnLoaded)}() method was called!");
ASF.ArchiLogger.LogGenericInfo("Good luck in whatever you're doing!");
return randomCatURL != null ? randomCatURL.ToString() : "God damn it, we're out of cats, care to notify my master? Thanks!";
default:
return null;
}
}
// This method is called when bot is destroyed, e.g. on config removal
// You should ensure that all of your references to this bot instance are cleared - most of the time this is anything you created in OnBotInit(), including deep roots in your custom modules
// This doesn't have to be done immediately (e.g. no need to cancel existing work), but it should be done in timely manner when everything is finished
// Doing so will allow the garbage collector to dispose the bot afterwards, refraining from doing so will create a "memory leak" by keeping the reference alive
public Task OnBotDestroy(Bot bot) => Task.CompletedTask;
// This method is called when bot is disconnected from Steam network, you may want to use this info in some kind of way, or not
// ASF tries its best to provide logical reason why the disconnection has happened, and will use EResult.OK if the disconnection was initiated by us (e.g. as part of a command)
// Still, you should take anything other than EResult.OK with a grain of salt, unless you want to assume that Steam knows why it disconnected us (hehe, you bet)
public Task OnBotDisconnected(Bot bot, EResult reason) => Task.CompletedTask;
// This method is called when bot receives a friend request or group invite that ASF isn't willing to accept
// It allows you to generate a response whether ASF should accept it (true) or proceed like usual (false)
// If you wanted to do extra filtering (e.g. friend requests only), you can interpret the steamID as SteamID (SteamKit2 type) and then operate on AccountType
// As an example, we'll run a trade bot that is open to all friend/group invites, therefore we'll accept all of them here
public Task<bool> OnBotFriendRequest(Bot bot, ulong steamID) => Task.FromResult(true);
// This method is called at the end of Bot's constructor
// You can initialize all your per-bot structures here
// In general you should do that only when you have a particular need of custom modules or alike, since ASF's plugin system will always provide bot to you as a function argument
public Task OnBotInit(Bot bot) {
// Apart of those two that are already provided by ASF, you can also initialize your own logger with your plugin's name, if needed
bot.ArchiLogger.LogGenericInfo($"Our bot named {bot.BotName} has been initialized, and we're letting you know about it from our {nameof(ExamplePlugin)}!");
ASF.ArchiLogger.LogGenericWarning("In case we won't have a bot reference or have something process-wide to log, we can also use ASF's logger!");
return Task.CompletedTask;
}
// This method, apart from being called during bot modules initialization, allows you to read custom bot config properties that are not recognized by ASF
// Thanks to that, you can extend default bot config with your own stuff, then parse it here in order to customize your plugin during runtime
// Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately
// Also keep in mind that this function can be called multiple times, e.g. when user edits his bot configs during runtime
// Take a look at OnASFInit() for example parsing code
public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
// For example, we'll ensure that every bot starts paused regardless of Paused property, in order to do this, we'll just call Pause here in InitModules()
// Thanks to the fact that this method is called with each bot config reload, we'll ensure that our bot stays paused even if it'd get unpaused otherwise
bot.ArchiLogger.LogGenericInfo("Pausing this bot as asked from the plugin");
await bot.Actions.Pause(true).ConfigureAwait(false);
}
// This method is called when the bot is successfully connected to Steam network and it's a good place to schedule any on-connected tasks, as AWH is also expected to be available shortly
public Task OnBotLoggedOn(Bot bot) => Task.CompletedTask;
// This method is called when bot receives a message that is NOT a command (in other words, a message that doesn't start with CommandPrefix)
// Normally ASF entirely ignores such messages as the program should not respond to something that isn't recognized
// Therefore this function allows you to catch all such messages and handle them yourself
// Keep in mind that there is no guarantee what is the actual access of steamID, so you should do the appropriate access checking yourself
// You can use either ASF's default functions for that, or implement your own logic as you please
// If you do not intend to return any response to user, just return null/empty and ASF will proceed with the silence as usual
public Task<string?> OnBotMessage(Bot bot, ulong steamID, string message) {
// Normally ASF will expect from you async-capable responses, such as Task<string>. This allows you to make your code fully asynchronous which is a core foundation on which ASF is built upon
// Since in this method we're not doing any async stuff, instead of defining this method as async (pointless), we just need to wrap our responses in Task.FromResult<>()
if (Bot.BotsReadOnly == null) {
throw new InvalidOperationException(nameof(Bot.BotsReadOnly));
}
// As a starter, we can for example ignore messages sent from our own bots, since otherwise they can run into a possible infinite loop of answering themselves
if (Bot.BotsReadOnly.Values.Any(existingBot => existingBot.SteamID == steamID)) {
return Task.FromResult<string?>(null);
}
// If this message doesn't come from one of our bots, we can reply to the user in some pre-defined way
bot.ArchiLogger.LogGenericTrace("Hey boss, we got some unknown message here!");
return Task.FromResult((string?) "I didn't get that, did you mean to use a command?");
}
// This method is called when bot receives a trade offer that ASF isn't willing to accept (ignored and rejected trades)
// It allows you not only to analyze such trades, but generate a response whether ASF should accept it (true), or proceed like usual (false)
// Thanks to that, you can implement custom rules for all trades that aren't handled by ASF, for example cross-set trading on your own custom rules
// You'd implement your own logic here, as an example we'll allow all trades to be accepted if the bot's name starts from "TrashBot"
public Task<bool> OnBotTradeOffer(Bot bot, TradeOffer tradeOffer) => Task.FromResult(bot.BotName.StartsWith("TrashBot", StringComparison.OrdinalIgnoreCase));
// This is the earliest method that will be called, right after loading the plugin, long before any bot initialization takes place
// It's a good place to initialize all potential (non-bot-specific) structures that you will need across lifetime of your plugin, such as global timers, concurrent dictionaries and alike
// If you do not have any global structures to initialize, you can leave this function empty
// At this point you can access core ASF's functionality, such as logging, but more advanced structures (like ASF's WebBrowser) will be available in OnASFInit(), which itself takes place after every plugin gets OnLoaded()
// Typically you should use this function only for preparing core structures of your plugin, and optionally also sending a message to the user (e.g. support link, welcome message or similar), ASF-specific things should usually happen in OnASFInit()
public Task OnLoaded() {
ASF.ArchiLogger.LogGenericInfo($"Hey! Thanks for checking if our example plugin works fine, this is a confirmation that indeed {nameof(OnLoaded)}() method was called!");
ASF.ArchiLogger.LogGenericInfo("Good luck in whatever you're doing!");
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,37 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class MeowResponse {
[JsonProperty("file", Required = Required.Always)]
internal readonly Uri URL = null!;
[JsonConstructor]
private MeowResponse() { }
}
#pragma warning restore CA1812 // False positive, the class is used during json deserialization

View File

@@ -5,6 +5,7 @@
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
</ItemGroup>

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,41 +24,44 @@ using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Runtime;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Plugins.Interfaces;
namespace ArchiSteamFarm.CustomPlugins.PeriodicGC {
[Export(typeof(IPlugin))]
[SuppressMessage("ReSharper", "UnusedType.Global")]
internal sealed class PeriodicGCPlugin : IPlugin {
private const byte GCPeriod = 60; // In seconds
namespace ArchiSteamFarm.CustomPlugins.PeriodicGC;
private static readonly object LockObject = new();
private static readonly Timer PeriodicGCTimer = new(PerformGC);
[Export(typeof(IPlugin))]
[SuppressMessage("ReSharper", "UnusedType.Global")]
internal sealed class PeriodicGCPlugin : IPlugin {
private const byte GCPeriod = 60; // In seconds
public string Name => nameof(PeriodicGCPlugin);
private static readonly object LockObject = new();
private static readonly Timer PeriodicGCTimer = new(PerformGC);
public Version Version => typeof(PeriodicGCPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public string Name => nameof(PeriodicGCPlugin);
public void OnLoaded() {
TimeSpan timeSpan = TimeSpan.FromSeconds(GCPeriod);
public Version Version => typeof(PeriodicGCPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
ASF.ArchiLogger.LogGenericWarning($"Periodic GC will occur every {timeSpan.ToHumanReadable()}. Please keep in mind that this plugin should be used for debugging tests only.");
public Task OnLoaded() {
TimeSpan timeSpan = TimeSpan.FromSeconds(GCPeriod);
lock (LockObject) {
PeriodicGCTimer.Change(timeSpan, timeSpan);
}
ASF.ArchiLogger.LogGenericWarning($"Periodic GC will occur every {timeSpan.ToHumanReadable()}. Please keep in mind that this plugin should be used for debugging tests only.");
lock (LockObject) {
PeriodicGCTimer.Change(timeSpan, timeSpan);
}
private static void PerformGC(object? state = null) {
ASF.ArchiLogger.LogGenericWarning($"Performing GC, current memory: {GC.GetTotalMemory(false) / 1024} KB.");
return Task.CompletedTask;
}
lock (LockObject) {
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true);
}
private static void PerformGC(object? state = null) {
ASF.ArchiLogger.LogGenericWarning($"Performing GC, current memory: {GC.GetTotalMemory(false) / 1024} KB.");
ASF.ArchiLogger.LogGenericWarning($"GC finished, current memory: {GC.GetTotalMemory(false) / 1024} KB.");
lock (LockObject) {
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true);
}
ASF.ArchiLogger.LogGenericWarning($"GC finished, current memory: {GC.GetTotalMemory(false) / 1024} KB.");
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -28,280 +28,298 @@ using System.Linq;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization;
using JetBrains.Annotations;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
internal sealed class GlobalCache : SerializableFile {
private static string SharedFilePath => Path.Combine(ArchiSteamFarm.SharedInfo.ConfigDirectory, $"{nameof(SteamTokenDumper)}.cache");
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, uint> AppChangeNumbers = new();
internal sealed class GlobalCache : SerializableFile {
private static string SharedFilePath => Path.Combine(ArchiSteamFarm.SharedInfo.ConfigDirectory, $"{nameof(SteamTokenDumper)}.cache");
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, ulong> AppTokens = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, uint> AppChangeNumbers = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, string> DepotKeys = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, ulong> AppTokens = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, ulong> PackageTokens = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, string> DepotKeys = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, ulong> SubmittedApps = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, ulong> PackageTokens = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, string> SubmittedDepots = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, ulong> SubmittedApps = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, ulong> SubmittedPackages = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, string> SubmittedDepots = new();
[JsonProperty(Required = Required.DisallowNull)]
internal uint LastChangeNumber { get; private set; }
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, ulong> SubmittedPackages = new();
internal GlobalCache() => FilePath = SharedFilePath;
[JsonProperty(Required = Required.DisallowNull)]
internal uint LastChangeNumber { get; private set; }
[UsedImplicitly]
public bool ShouldSerializeAppChangeNumbers() => !AppChangeNumbers.IsEmpty;
internal GlobalCache() => FilePath = SharedFilePath;
[UsedImplicitly]
public bool ShouldSerializeAppTokens() => !AppTokens.IsEmpty;
[UsedImplicitly]
public bool ShouldSerializeAppChangeNumbers() => !AppChangeNumbers.IsEmpty;
[UsedImplicitly]
public bool ShouldSerializeDepotKeys() => !DepotKeys.IsEmpty;
[UsedImplicitly]
public bool ShouldSerializeAppTokens() => !AppTokens.IsEmpty;
[UsedImplicitly]
public bool ShouldSerializeLastChangeNumber() => LastChangeNumber > 0;
[UsedImplicitly]
public bool ShouldSerializeDepotKeys() => !DepotKeys.IsEmpty;
[UsedImplicitly]
public bool ShouldSerializePackageTokens() => !PackageTokens.IsEmpty;
[UsedImplicitly]
public bool ShouldSerializeLastChangeNumber() => LastChangeNumber > 0;
[UsedImplicitly]
public bool ShouldSerializeSubmittedApps() => !SubmittedApps.IsEmpty;
[UsedImplicitly]
public bool ShouldSerializePackageTokens() => !PackageTokens.IsEmpty;
[UsedImplicitly]
public bool ShouldSerializeSubmittedDepots() => !SubmittedDepots.IsEmpty;
[UsedImplicitly]
public bool ShouldSerializeSubmittedApps() => !SubmittedApps.IsEmpty;
[UsedImplicitly]
public bool ShouldSerializeSubmittedPackages() => !SubmittedPackages.IsEmpty;
[UsedImplicitly]
public bool ShouldSerializeSubmittedDepots() => !SubmittedDepots.IsEmpty;
internal ulong GetAppToken(uint appID) => AppTokens[appID];
[UsedImplicitly]
public bool ShouldSerializeSubmittedPackages() => !SubmittedPackages.IsEmpty;
internal Dictionary<uint, ulong> GetAppTokensForSubmission() => AppTokens.Where(appToken => (SteamTokenDumperPlugin.Config?.SecretAppIDs.Contains(appToken.Key) == false) && (appToken.Value > 0) && (!SubmittedApps.TryGetValue(appToken.Key, out ulong token) || (appToken.Value != token))).ToDictionary(static appToken => appToken.Key, static appToken => appToken.Value);
internal Dictionary<uint, string> GetDepotKeysForSubmission() => DepotKeys.Where(depotKey => (SteamTokenDumperPlugin.Config?.SecretDepotIDs.Contains(depotKey.Key) == false) && !string.IsNullOrEmpty(depotKey.Value) && (!SubmittedDepots.TryGetValue(depotKey.Key, out string? key) || (depotKey.Value != key))).ToDictionary(static depotKey => depotKey.Key, static depotKey => depotKey.Value);
internal Dictionary<uint, ulong> GetPackageTokensForSubmission() => PackageTokens.Where(packageToken => (SteamTokenDumperPlugin.Config?.SecretPackageIDs.Contains(packageToken.Key) == false) && (packageToken.Value > 0) && (!SubmittedPackages.TryGetValue(packageToken.Key, out ulong token) || (packageToken.Value != token))).ToDictionary(static packageToken => packageToken.Key, static packageToken => packageToken.Value);
internal ulong GetAppToken(uint appID) => AppTokens[appID];
internal static async Task<GlobalCache?> Load() {
if (!File.Exists(SharedFilePath)) {
GlobalCache result = new();
internal Dictionary<uint, ulong> GetAppTokensForSubmission() => AppTokens.Where(appToken => (SteamTokenDumperPlugin.Config?.SecretAppIDs.Contains(appToken.Key) == false) && (appToken.Value > 0) && (!SubmittedApps.TryGetValue(appToken.Key, out ulong token) || (appToken.Value != token))).ToDictionary(static appToken => appToken.Key, static appToken => appToken.Value);
internal Dictionary<uint, string> GetDepotKeysForSubmission() => DepotKeys.Where(depotKey => (SteamTokenDumperPlugin.Config?.SecretDepotIDs.Contains(depotKey.Key) == false) && !string.IsNullOrEmpty(depotKey.Value) && (!SubmittedDepots.TryGetValue(depotKey.Key, out string? key) || (depotKey.Value != key))).ToDictionary(static depotKey => depotKey.Key, static depotKey => depotKey.Value);
internal Dictionary<uint, ulong> GetPackageTokensForSubmission() => PackageTokens.Where(packageToken => (SteamTokenDumperPlugin.Config?.SecretPackageIDs.Contains(packageToken.Key) == false) && (packageToken.Value > 0) && (!SubmittedPackages.TryGetValue(packageToken.Key, out ulong token) || (packageToken.Value != token))).ToDictionary(static packageToken => packageToken.Key, static packageToken => packageToken.Value);
Utilities.InBackground(result.Save);
internal static async Task<GlobalCache?> Load() {
if (!File.Exists(SharedFilePath)) {
return new GlobalCache();
}
return result;
}
ASF.ArchiLogger.LogGenericInfo(Strings.LoadingGlobalCache);
GlobalCache? globalCache;
GlobalCache? globalCache;
try {
string json = await File.ReadAllTextAsync(SharedFilePath).ConfigureAwait(false);
try {
string json = await File.ReadAllTextAsync(SharedFilePath).ConfigureAwait(false);
if (string.IsNullOrEmpty(json)) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
return null;
}
globalCache = JsonConvert.DeserializeObject<GlobalCache>(json);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
if (string.IsNullOrEmpty(json)) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(json)));
return null;
}
if (globalCache == null) {
ASF.ArchiLogger.LogNullError(nameof(globalCache));
globalCache = JsonConvert.DeserializeObject<GlobalCache>(json);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return null;
}
return globalCache;
return null;
}
internal void OnPICSChanges(uint currentChangeNumber, IReadOnlyCollection<KeyValuePair<uint, SteamApps.PICSChangesCallback.PICSChangeData>> appChanges) {
if (currentChangeNumber == 0) {
throw new ArgumentOutOfRangeException(nameof(currentChangeNumber));
}
if (globalCache == null) {
ASF.ArchiLogger.LogNullError(globalCache);
if (appChanges == null) {
throw new ArgumentNullException(nameof(appChanges));
}
if (currentChangeNumber <= LastChangeNumber) {
return;
}
LastChangeNumber = currentChangeNumber;
foreach ((uint appID, SteamApps.PICSChangesCallback.PICSChangeData appData) in appChanges) {
if (!AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) || (appData.ChangeNumber <= previousChangeNumber)) {
continue;
}
AppChangeNumbers.TryRemove(appID, out _);
}
Utilities.InBackground(Save);
return null;
}
internal void OnPICSChangesRestart(uint currentChangeNumber) {
if (currentChangeNumber == 0) {
throw new ArgumentOutOfRangeException(nameof(currentChangeNumber));
}
ASF.ArchiLogger.LogGenericInfo(Strings.ValidatingGlobalCacheIntegrity);
if (currentChangeNumber <= LastChangeNumber) {
return;
}
if (globalCache.DepotKeys.Values.Any(static depotKey => !IsValidDepotKey(depotKey))) {
ASF.ArchiLogger.LogGenericError(Strings.GlobalCacheIntegrityValidationFailed);
LastChangeNumber = currentChangeNumber;
AppChangeNumbers.Clear();
Utilities.InBackground(Save);
return null;
}
internal bool ShouldRefreshAppInfo(uint appID) => !AppChangeNumbers.ContainsKey(appID);
internal bool ShouldRefreshDepotKey(uint depotID) => !DepotKeys.ContainsKey(depotID);
return globalCache;
}
internal void UpdateAppChangeNumbers(IReadOnlyCollection<KeyValuePair<uint, uint>> appChangeNumbers) {
if (appChangeNumbers == null) {
throw new ArgumentNullException(nameof(appChangeNumbers));
}
bool save = false;
foreach ((uint appID, uint changeNumber) in appChangeNumbers) {
if (AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) && (previousChangeNumber == changeNumber)) {
continue;
}
AppChangeNumbers[appID] = changeNumber;
save = true;
}
if (save) {
Utilities.InBackground(Save);
}
internal void OnPICSChanges(uint currentChangeNumber, IReadOnlyCollection<KeyValuePair<uint, SteamApps.PICSChangesCallback.PICSChangeData>> appChanges) {
if (currentChangeNumber == 0) {
throw new ArgumentOutOfRangeException(nameof(currentChangeNumber));
}
internal void UpdateAppTokens(IReadOnlyCollection<KeyValuePair<uint, ulong>> appTokens, IReadOnlyCollection<uint> publicAppIDs) {
if (appTokens == null) {
throw new ArgumentNullException(nameof(appTokens));
}
ArgumentNullException.ThrowIfNull(appChanges);
if (publicAppIDs == null) {
throw new ArgumentNullException(nameof(publicAppIDs));
}
bool save = false;
foreach ((uint appID, ulong appToken) in appTokens) {
if (AppTokens.TryGetValue(appID, out ulong previousAppToken) && (previousAppToken == appToken)) {
continue;
}
AppTokens[appID] = appToken;
save = true;
}
foreach (uint appID in publicAppIDs) {
if (AppTokens.TryGetValue(appID, out ulong previousAppToken) && (previousAppToken == 0)) {
continue;
}
AppTokens[appID] = 0;
save = true;
}
if (save) {
Utilities.InBackground(Save);
}
if (currentChangeNumber <= LastChangeNumber) {
return;
}
internal void UpdateDepotKeys(ICollection<SteamApps.DepotKeyCallback> depotKeyResults) {
if (depotKeyResults == null) {
throw new ArgumentNullException(nameof(depotKeyResults));
LastChangeNumber = currentChangeNumber;
foreach ((uint appID, SteamApps.PICSChangesCallback.PICSChangeData appData) in appChanges) {
if (!AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) || (previousChangeNumber >= appData.ChangeNumber)) {
continue;
}
bool save = false;
foreach (SteamApps.DepotKeyCallback depotKeyResult in depotKeyResults) {
if (depotKeyResult.Result != EResult.OK) {
continue;
}
string depotKey = BitConverter.ToString(depotKeyResult.DepotKey).Replace("-", "", StringComparison.Ordinal);
if (DepotKeys.TryGetValue(depotKeyResult.DepotID, out string? previousDepotKey) && (previousDepotKey == depotKey)) {
continue;
}
DepotKeys[depotKeyResult.DepotID] = depotKey;
save = true;
}
if (save) {
Utilities.InBackground(Save);
}
AppChangeNumbers.TryRemove(appID, out _);
}
internal void UpdatePackageTokens(IReadOnlyCollection<KeyValuePair<uint, ulong>> packageTokens) {
if (packageTokens == null) {
throw new ArgumentNullException(nameof(packageTokens));
}
Utilities.InBackground(Save);
}
bool save = false;
foreach ((uint packageID, ulong packageToken) in packageTokens) {
if (PackageTokens.TryGetValue(packageID, out ulong previousPackageToken) && (previousPackageToken == packageToken)) {
continue;
}
PackageTokens[packageID] = packageToken;
save = true;
}
if (save) {
Utilities.InBackground(Save);
}
internal void OnPICSChangesRestart(uint currentChangeNumber) {
if (currentChangeNumber == 0) {
throw new ArgumentOutOfRangeException(nameof(currentChangeNumber));
}
internal void UpdateSubmittedData(IReadOnlyDictionary<uint, ulong> apps, IReadOnlyDictionary<uint, ulong> packages, IReadOnlyDictionary<uint, string> depots) {
if (apps == null) {
throw new ArgumentNullException(nameof(apps));
if (currentChangeNumber <= LastChangeNumber) {
return;
}
LastChangeNumber = currentChangeNumber;
AppChangeNumbers.Clear();
Utilities.InBackground(Save);
}
internal bool ShouldRefreshAppInfo(uint appID) => !AppChangeNumbers.ContainsKey(appID);
internal bool ShouldRefreshDepotKey(uint depotID) => !DepotKeys.ContainsKey(depotID);
internal void UpdateAppChangeNumbers(IReadOnlyCollection<KeyValuePair<uint, uint>> appChangeNumbers) {
ArgumentNullException.ThrowIfNull(appChangeNumbers);
bool save = false;
foreach ((uint appID, uint changeNumber) in appChangeNumbers) {
if (AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) && (previousChangeNumber >= changeNumber)) {
continue;
}
if (packages == null) {
throw new ArgumentNullException(nameof(packages));
}
if (depots == null) {
throw new ArgumentNullException(nameof(depots));
}
foreach ((uint appID, ulong token) in apps) {
SubmittedApps[appID] = token;
}
foreach ((uint packageID, ulong token) in packages) {
SubmittedPackages[packageID] = token;
}
foreach ((uint depotID, string key) in depots) {
SubmittedDepots[depotID] = key;
}
AppChangeNumbers[appID] = changeNumber;
save = true;
}
if (save) {
Utilities.InBackground(Save);
}
}
internal void UpdateAppTokens(IReadOnlyCollection<KeyValuePair<uint, ulong>> appTokens, IReadOnlyCollection<uint> publicAppIDs) {
ArgumentNullException.ThrowIfNull(appTokens);
ArgumentNullException.ThrowIfNull(publicAppIDs);
bool save = false;
foreach ((uint appID, ulong appToken) in appTokens) {
if (AppTokens.TryGetValue(appID, out ulong previousAppToken) && (previousAppToken == appToken)) {
continue;
}
AppTokens[appID] = appToken;
save = true;
}
foreach (uint appID in publicAppIDs) {
if (AppTokens.TryGetValue(appID, out ulong previousAppToken) && (previousAppToken == 0)) {
continue;
}
AppTokens[appID] = 0;
save = true;
}
if (save) {
Utilities.InBackground(Save);
}
}
internal void UpdateDepotKeys(ICollection<SteamApps.DepotKeyCallback> depotKeyResults) {
ArgumentNullException.ThrowIfNull(depotKeyResults);
bool save = false;
foreach (SteamApps.DepotKeyCallback depotKeyResult in depotKeyResults) {
if (depotKeyResult.Result != EResult.OK) {
continue;
}
string depotKey = Convert.ToHexString(depotKeyResult.DepotKey);
if (!IsValidDepotKey(depotKey)) {
ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid, nameof(depotKey)));
continue;
}
if (DepotKeys.TryGetValue(depotKeyResult.DepotID, out string? previousDepotKey) && (previousDepotKey == depotKey)) {
continue;
}
DepotKeys[depotKeyResult.DepotID] = depotKey;
save = true;
}
if (save) {
Utilities.InBackground(Save);
}
}
internal void UpdatePackageTokens(IReadOnlyCollection<KeyValuePair<uint, ulong>> packageTokens) {
ArgumentNullException.ThrowIfNull(packageTokens);
bool save = false;
foreach ((uint packageID, ulong packageToken) in packageTokens) {
if (PackageTokens.TryGetValue(packageID, out ulong previousPackageToken) && (previousPackageToken == packageToken)) {
continue;
}
PackageTokens[packageID] = packageToken;
save = true;
}
if (save) {
Utilities.InBackground(Save);
}
}
internal void UpdateSubmittedData(IReadOnlyDictionary<uint, ulong> apps, IReadOnlyDictionary<uint, ulong> packages, IReadOnlyDictionary<uint, string> depots) {
ArgumentNullException.ThrowIfNull(apps);
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;
}
if (save) {
Utilities.InBackground(Save);
}
}
private static bool IsValidDepotKey(string depotKey) {
if (string.IsNullOrEmpty(depotKey)) {
throw new ArgumentNullException(nameof(depotKey));
}
return (depotKey.Length == 64) && Utilities.IsValidHexadecimalText(depotKey);
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,15 +21,15 @@
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
public sealed class GlobalConfigExtension {
[JsonProperty(Required = Required.DisallowNull)]
public SteamTokenDumperConfig? SteamTokenDumperPlugin { get; private set; }
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
[JsonProperty(Required = Required.DisallowNull)]
public bool SteamTokenDumperPluginEnabled { get; private set; }
public sealed class GlobalConfigExtension {
[JsonProperty(Required = Required.DisallowNull)]
public SteamTokenDumperConfig? SteamTokenDumperPlugin { get; private set; }
[JsonConstructor]
internal GlobalConfigExtension() { }
}
[JsonProperty(Required = Required.DisallowNull)]
public bool SteamTokenDumperPluginEnabled { get; private set; }
[JsonConstructor]
internal GlobalConfigExtension() { }
}

View File

@@ -1,7 +1,6 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -12,46 +11,32 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Strings {
private static global::System.Resources.ResourceManager resourceMan;
private static System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
private static System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Strings() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization.Strings", typeof(Strings).Assembly);
if (object.Equals(null, resourceMan)) {
System.Resources.ResourceManager temp = new System.Resources.ResourceManager("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization.Strings", typeof(Strings).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
@@ -60,246 +45,183 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization {
}
}
/// <summary>
/// Looks up a localized string similar to Finished retrieving {0} app access tokens..
/// </summary>
internal static string BotFinishedRetrievingAppAccessTokens {
get {
return ResourceManager.GetString("BotFinishedRetrievingAppAccessTokens", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Finished retrieving {0} app infos..
/// </summary>
internal static string BotFinishedRetrievingAppInfos {
get {
return ResourceManager.GetString("BotFinishedRetrievingAppInfos", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Finished retrieving {0} depot keys..
/// </summary>
internal static string BotFinishedRetrievingDepotKeys {
get {
return ResourceManager.GetString("BotFinishedRetrievingDepotKeys", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Finished retrieving a total of {0} app access tokens..
/// </summary>
internal static string BotFinishedRetrievingTotalAppAccessTokens {
get {
return ResourceManager.GetString("BotFinishedRetrievingTotalAppAccessTokens", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Finished retrieving all depot keys for a total of {0} apps..
/// </summary>
internal static string BotFinishedRetrievingTotalDepots {
get {
return ResourceManager.GetString("BotFinishedRetrievingTotalDepots", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to There are no apps that require a refresh on this bot instance..
/// </summary>
internal static string BotNoAppsToRefresh {
get {
return ResourceManager.GetString("BotNoAppsToRefresh", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Retrieving {0} app access tokens....
/// </summary>
internal static string BotRetrievingAppAccessTokens {
get {
return ResourceManager.GetString("BotRetrievingAppAccessTokens", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Retrieving {0} app infos....
/// </summary>
internal static string BotRetrievingAppInfos {
get {
return ResourceManager.GetString("BotRetrievingAppInfos", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Retrieving {0} depot keys....
/// </summary>
internal static string BotRetrievingDepotKeys {
get {
return ResourceManager.GetString("BotRetrievingDepotKeys", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Retrieving a total of {0} app access tokens....
/// </summary>
internal static string BotRetrievingTotalAppAccessTokens {
get {
return ResourceManager.GetString("BotRetrievingTotalAppAccessTokens", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Retrieving all depots for a total of {0} apps....
/// </summary>
internal static string BotRetrievingTotalDepots {
get {
return ResourceManager.GetString("BotRetrievingTotalDepots", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} could not be loaded, a fresh instance will be initialized....
/// </summary>
internal static string FileCouldNotBeLoadedFreshInit {
get {
return ResourceManager.GetString("FileCouldNotBeLoadedFreshInit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} is currently disabled according to your configuration. If you&apos;d like to help SteamDB in data submission, please check out our wiki..
/// </summary>
internal static string PluginDisabledInConfig {
get {
return ResourceManager.GetString("PluginDisabledInConfig", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} has been disabled due to a missing build token.
/// </summary>
internal static string PluginDisabledMissingBuildToken {
get {
return ResourceManager.GetString("PluginDisabledMissingBuildToken", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} has been initialized successfully, thank you in advance for your help. The first submission will happen in approximately {1} from now..
/// </summary>
internal static string PluginDisabledInConfig {
get {
return ResourceManager.GetString("PluginDisabledInConfig", resourceCulture);
}
}
internal static string PluginInitializedAndEnabled {
get {
return ResourceManager.GetString("PluginInitializedAndEnabled", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} initialized, the plugin will not resolve any of those: {1}..
/// </summary>
internal static string PluginSecretListInitialized {
internal static string FileCouldNotBeLoadedFreshInit {
get {
return ResourceManager.GetString("PluginSecretListInitialized", resourceCulture);
return ResourceManager.GetString("FileCouldNotBeLoadedFreshInit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The submission has failed due to too many requests sent, we&apos;ll try again in approximately {0} from now..
/// </summary>
internal static string SubmissionFailedTooManyRequests {
internal static string BotNoAppsToRefresh {
get {
return ResourceManager.GetString("SubmissionFailedTooManyRequests", resourceCulture);
return ResourceManager.GetString("BotNoAppsToRefresh", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Submitting a total of registered apps/packages/depots: {0}/{1}/{2}....
/// </summary>
internal static string SubmissionInProgress {
internal static string BotRetrievingTotalAppAccessTokens {
get {
return ResourceManager.GetString("SubmissionInProgress", resourceCulture);
return ResourceManager.GetString("BotRetrievingTotalAppAccessTokens", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Could not submit the data because there is no valid SteamID set that we could classify as a contributor. Consider setting up {0} property..
/// </summary>
internal static string SubmissionNoContributorSet {
internal static string BotRetrievingAppAccessTokens {
get {
return ResourceManager.GetString("SubmissionNoContributorSet", resourceCulture);
return ResourceManager.GetString("BotRetrievingAppAccessTokens", resourceCulture);
}
}
internal static string BotFinishedRetrievingAppAccessTokens {
get {
return ResourceManager.GetString("BotFinishedRetrievingAppAccessTokens", resourceCulture);
}
}
internal static string BotFinishedRetrievingTotalAppAccessTokens {
get {
return ResourceManager.GetString("BotFinishedRetrievingTotalAppAccessTokens", resourceCulture);
}
}
internal static string BotRetrievingTotalDepots {
get {
return ResourceManager.GetString("BotRetrievingTotalDepots", resourceCulture);
}
}
internal static string BotRetrievingAppInfos {
get {
return ResourceManager.GetString("BotRetrievingAppInfos", resourceCulture);
}
}
internal static string BotFinishedRetrievingAppInfos {
get {
return ResourceManager.GetString("BotFinishedRetrievingAppInfos", resourceCulture);
}
}
internal static string BotRetrievingDepotKeys {
get {
return ResourceManager.GetString("BotRetrievingDepotKeys", resourceCulture);
}
}
internal static string BotFinishedRetrievingDepotKeys {
get {
return ResourceManager.GetString("BotFinishedRetrievingDepotKeys", resourceCulture);
}
}
internal static string BotFinishedRetrievingTotalDepots {
get {
return ResourceManager.GetString("BotFinishedRetrievingTotalDepots", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to There is no new data to submit, everything is up-to-date..
/// </summary>
internal static string SubmissionNoNewData {
get {
return ResourceManager.GetString("SubmissionNoNewData", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The data has been successfully submitted. The server has registered a total of new apps/packages/depots: {0} ({1} verified)/{2} ({3} verified)/{4} ({5} verified)..
/// </summary>
internal static string SubmissionNoContributorSet {
get {
return ResourceManager.GetString("SubmissionNoContributorSet", resourceCulture);
}
}
internal static string SubmissionInProgress {
get {
return ResourceManager.GetString("SubmissionInProgress", resourceCulture);
}
}
internal static string SubmissionFailedTooManyRequests {
get {
return ResourceManager.GetString("SubmissionFailedTooManyRequests", resourceCulture);
}
}
internal static string SubmissionSuccessful {
get {
return ResourceManager.GetString("SubmissionSuccessful", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to New apps: {0}.
/// </summary>
internal static string SubmissionSuccessfulNewApps {
get {
return ResourceManager.GetString("SubmissionSuccessfulNewApps", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to New depots: {0}.
/// </summary>
internal static string SubmissionSuccessfulNewDepots {
get {
return ResourceManager.GetString("SubmissionSuccessfulNewDepots", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to New packages: {0}.
/// </summary>
internal static string SubmissionSuccessfulNewPackages {
get {
return ResourceManager.GetString("SubmissionSuccessfulNewPackages", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Verified apps: {0}.
/// </summary>
internal static string SubmissionSuccessfulVerifiedApps {
get {
return ResourceManager.GetString("SubmissionSuccessfulVerifiedApps", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Verified depots: {0}.
/// </summary>
internal static string SubmissionSuccessfulNewPackages {
get {
return ResourceManager.GetString("SubmissionSuccessfulNewPackages", resourceCulture);
}
}
internal static string SubmissionSuccessfulVerifiedPackages {
get {
return ResourceManager.GetString("SubmissionSuccessfulVerifiedPackages", resourceCulture);
}
}
internal static string SubmissionSuccessfulNewDepots {
get {
return ResourceManager.GetString("SubmissionSuccessfulNewDepots", resourceCulture);
}
}
internal static string SubmissionSuccessfulVerifiedDepots {
get {
return ResourceManager.GetString("SubmissionSuccessfulVerifiedDepots", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Verified packages: {0}.
/// </summary>
internal static string SubmissionSuccessfulVerifiedPackages {
internal static string PluginSecretListInitialized {
get {
return ResourceManager.GetString("SubmissionSuccessfulVerifiedPackages", resourceCulture);
return ResourceManager.GetString("PluginSecretListInitialized", resourceCulture);
}
}
internal static string LoadingGlobalCache {
get {
return ResourceManager.GetString("LoadingGlobalCache", resourceCulture);
}
}
internal static string ValidatingGlobalCacheIntegrity {
get {
return ResourceManager.GetString("ValidatingGlobalCacheIntegrity", resourceCulture);
}
}
internal static string GlobalCacheIntegrityValidationFailed {
get {
return ResourceManager.GetString("GlobalCacheIntegrityValidationFailed", resourceCulture);
}
}
}

View File

@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns="" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
<value>{0} быў адключаны з-за адсутнасці токена зборкі</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginDisabledInConfig" xml:space="preserve">
<value>{0} зараз адключаны ў адпаведнасці з вашай канфігурацыяй. Калі вы хочаце дапамагчы SteamDB у адпраўцы даных, калі ласка, зазірніце ў нашу вікі.</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginInitializedAndEnabled" xml:space="preserve">
<value>{0} быў паспяхова ініцыялізаваны, загадзя дзякуй за дапамогу. Першая адпраўка адбудзецца прыкладна праз {1}.</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
<value>Не атрымалася загрузіць {0}, будзе ініцыялізаваны новы асобнік...</value>
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
</data>
<data name="BotNoAppsToRefresh" xml:space="preserve">
<value>На гэтым асобніку бота няма праграм, якія патрабуюць абнаўлення.</value>
</data>
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>Атрыманне ў агульнай складанасці {0} токенаў доступу праграмы...</value>
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
</data>
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
<value>Атрыманне {0} токенаў доступу праграмы...</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
<value>Скончана атрыманне {0} токенаў доступу праграмы.</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>Завершана атрыманне {0} токенаў доступу праграмы.</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>Атрыманне ўсіх сховішч для агульнай колькасці праграм {0}...</value>
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
</data>
<data name="BotRetrievingAppInfos" xml:space="preserve">
<value>Атрыманне інфармацыі пра праграму {0}...</value>
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
<value>Завершана атрыманне інфармацыі пра праграму {0}.</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>Атрыманне {0} ключоў сховішча...</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>Завершана атрыманне {0} ключоў сховішча.</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>Завершана атрыманне ўсіх ключоў ключоў сховішча для {0} праграм.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
</data>
<data name="SubmissionNoNewData" xml:space="preserve">
<value>Новых дадзеных для адпраўкі няма, усё актуальна.</value>
</data>
<data name="SubmissionNoContributorSet" xml:space="preserve">
<value>Памылка адпраўкі дадзеных: карэктны SteamID не быў прадстаўлены. Праверце правільнасць налады {0}.</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>Адпраўка агульнай колькасці зарэгістраваных праграм/пакетаў/сховішч: {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>Адпраўка не ўдалася з-за занадта вялікай колькасці адпраўленых запытаў. Мы паўтарым спробу прыкладна праз {0}.</value>
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="SubmissionSuccessful" xml:space="preserve">
<value>Дадзеныя былі паспяхова адпраўлены. Сервер зарэгістраваў агульную колькасць новых праграм/пакетаў/сховішч: {0} ({1} праверана)/{2} ({3} праверана)/{4} ({5} праверана).</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>Новыя праграмы: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
<value>Правераныя праграмы: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
<value>Новыя пакеты: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
<value>Правераныя пакеты: {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>Новыя сховішчы: {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>Правераныя сховішчы: {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} ініцыялізаваны, убудова не працуе з ніводным з наступных: {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>Загрузка глабальнага кэша STD...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Праверка цэласнасці глабальнага кэша STD...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Не ўдалося праверыць цэласнасць глабальнага кэша STD. Гэта сведчыць аб магчымым пашкоджанні файла/памяці, замест гэтага будзе ініцыялізаваны новы асобнік.</value>
</data>
</root>

View File

@@ -62,31 +62,119 @@
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
<value>{0} е била деактивирана поради липсващ токън за изграждане</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginDisabledInConfig" xml:space="preserve">
<value>{0} в момента е деактивирана според вашата настройка. Ако искате да помогнете на SteamDB при подаването на данни, моля разгледайте нашата уикипедия.</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginInitializedAndEnabled" xml:space="preserve">
<value>{0} е инициализиранa успешно, благодаря Ви предварително за вашата помощ. Първото подаване на данни ще се случи приблизително след {1}.</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
<value>{0} не беше успешно заредена, вместо това ще бъде стартирана нова инстанция...</value>
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
</data>
<data name="BotNoAppsToRefresh" xml:space="preserve">
<value>Няма нови игри или приложения, които да изискват презареждане на бота.</value>
</data>
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>Събиране на общо {0} входящи токени за игри или приложения...</value>
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
</data>
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
<value>Събиране на {0} входящи токени за игри или приложения...</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
<value>Приключи събирането на {0} входящи токени за игри или приложения.</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>Приключи събиране на общо {0} входящи токени за игри или приложения.</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>Събиране на всички депа за общо {0} игри или проложения...</value>
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
</data>
<data name="BotRetrievingAppInfos" xml:space="preserve">
<value>Събиране на {0} информация за играта или приложението...</value>
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
<value>Приключи събирането на {0} информация за играта или приложението.</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>Събиране {0} ключове за депо...</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>Приключи събирането {0} ключове за депо.</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>Приключи събирането на всички ключове за депа за общо {0} игри или проложения.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
</data>
<data name="SubmissionNoNewData" xml:space="preserve">
<value>Няма нова информация за подаване, цялата информация е актуална.</value>
</data>
<data name="SubmissionNoContributorSet" xml:space="preserve">
<value>Подаването на данните не може да се изпълни, защото няма валиден SteamID зададен като автор или сътрудник. Помислете да настроите {0} правилно.</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>Подаване на общо регистрирани игри/приложения/пакети/депа: {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>Подаването е неуспешно поради твърде много изпратени заявки, ще опитаме отново приблизително след {0}.</value>
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="SubmissionSuccessful" xml:space="preserve">
<value>Данните са изпратени успешно. Сървърът регистрира общо нови игри/приложения/пакети/депа: {0} ({1} потвърдени)/{2} ({3} потвърдени)/{4} ({5} потвърдени).</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>Нови игри / приложения: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
<value>Потвърдени игри / приложения: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
<value>Нови пакети: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
<value>Потвърдени пакети: {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>Нови депа: {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>Потвърдени депа: {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} стартирана, плъгинът няма да разреши нито една от тези: {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>Зареждане на STD общ кеш...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Потвърждаване на цялостта на STD общия кеш...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Не успя да провери целостта на глобалния STD кеш. Това предполага потенциална грешка, вместо това ще бъде стартирана нова инстанция.</value>
</data>
</root>

View File

@@ -62,31 +62,119 @@
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
<value>{0} byl zakázán z důvodu chybějícího tokenu</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginDisabledInConfig" xml:space="preserve">
<value>{0} je v současné době v souladu s vaší konfigurací zakázán. Pokud byste chtěli pomoci SteamDB při odesílání dat, podívejte se na naši wiki.</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginInitializedAndEnabled" xml:space="preserve">
<value>{0} byl úspěšně inicializován, předem vám děkujeme za vaši pomoc. První příspěvek se od teď stane přibližně za {1}.</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
<value>{0} nelze načíst, nová instance bude inicializována...</value>
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
</data>
<data name="BotNoAppsToRefresh" xml:space="preserve">
<value>Neexistují žádné aplikace, které by vyžadovaly aktualizaci této instance bota.</value>
</data>
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>Načítám celkem {0} přístupových tokenů...</value>
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
</data>
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
<value>Načítání {0} přístupových tokenů...</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppAccessTokens" 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 app access tokens retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
<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>
</data>
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
<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>
</data>
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
<value>Ověřené aplikace: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
<value>Nové balíčky: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
<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>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Ověřování globální integrity STD keše...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Ověření globální integrity STD keše se nezdařilo. To naznačuje, že může dojít k poškození souboru/paměti, místo toho bude inicializována nová instance.</value>
</data>
</root>

View File

@@ -168,4 +168,13 @@
<value>{0} wurde initialisiert, das Plugin wird keinen der folgenden Werte verarbeiten: {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>Globaler STD-Cache wird geladen...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Überprüfe STD globale Cache-Integrität...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Fehler beim Überprüfen der globalen STD-Cache-Integrität. Dies deutet auf eine mögliche Datei-/Speicher-Beschädigung hin; stattdessen wird eine neue Instanz initialisiert.</value>
</data>
</root>

View File

@@ -168,4 +168,13 @@
<value>{0} αρχικοποιήθηκε, το plugin δεν θα επιλύσει κανένα από αυτά: {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>Φόρτωση καθολικής μνήμης cache...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Επικύρωση ακεραιότητας καθολικής λανθάνουσας μνήμης STD...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Αποτυχία επαλήθευσης ακεραιότητας καθολικής λανθάνουσας μνήμης STD. Αυτό υποδηλώνει πιθανή διαφθορά αρχείου/μνήμης, αντ' αυτού θα ξεκινήσει μια νέα διεργασία.</value>
</data>
</root>

View File

@@ -168,4 +168,13 @@
<value>{0} iniciado, el plugin no analizará ninguno de los siguientes: {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>Cargando caché global de STD... </value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Validando integridad de la caché global de STD... </value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>No se pudo verificar la integridad de la caché global de STD. Esto puede significar una potencial corrupción de archivo/memoria, se iniciará una nueva instancia. </value>
</data>
</root>

View File

@@ -62,31 +62,119 @@
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
<value>{0} on poistettu käytöstä puuttuvan koontitunnuksen vuoksi</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginDisabledInConfig" xml:space="preserve">
<value>{0} on tällä hetkellä poistettu käytöstä asetuksistasi. Jos haluat auttaa SteamDB:tä tietojen lähettämisessä, ole hyvä ja tutustu wikimme.</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginInitializedAndEnabled" xml:space="preserve">
<value>{0} on alustettu onnistuneesti, kiitos etukäteen avustasi. Ensimmäinen lähetys tapahtuu noin {1} jälkeen.</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
<value>{0} ei voitu ladata. Uusi instanssi alustetaan...</value>
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
</data>
<data name="BotNoAppsToRefresh" xml:space="preserve">
<value>Ei ole sovelluksia, jotka vaatisivat päivitystä tässä botin instanssissa.</value>
</data>
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>Haetaan yhteensä {0} sovelluksen käyttötunnisteita...</value>
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
</data>
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
<value>Haetaan {0} sovelluksen käyttötunnisteita...</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
<value>Saatiin haettua {0} sovelluksen käyttötunnisteet.</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>Saatiin haettua yhteensä {0} sovelluksen käyttötunnisteet.</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>Haetaan kaikkia depotteja yhteensä {0} sovellukselle...</value>
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
</data>
<data name="BotRetrievingAppInfos" xml:space="preserve">
<value>Haetaan {0} sovelluksen tietoja...</value>
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
<value>Saatiin haettua {0} sovelluksen tiedot.</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>Haetaan {0} depot-avainta...</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>Saatiin haettua {0} depot-avainta.</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>Saatiin haettua kaikki depot-avaimet yhteensä {0} sovellukselle.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
</data>
<data name="SubmissionNoNewData" xml:space="preserve">
<value>Uutta dataa ei ole lähetettäväksi, kaikki on ajan tasalla.</value>
</data>
<data name="SubmissionNoContributorSet" xml:space="preserve">
<value>Tietoja ei voitu lähettää, koska ei ole voimassa olevaa SteamID-ryhmää, jonka voisimme luokitella osallistujaksi. Harkitse ominaisuuden {0} asettamista.</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>Lähetetään yhteensä {0}/{1}/{2} rekisteröityjä sovelluksia/paketteja/varikoita...</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>Lähetys epäonnistui liian monen pyynnön vuoksi, yritämme uudelleen noin {0} kuluttua.</value>
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="SubmissionSuccessful" xml:space="preserve">
<value>Tiedot on lähetetty onnistuneesti. Palvelin on rekisteröinyt yhteensä uusia sovelluksia/paketteja/depoteja: {0} ({1} vahvistettu)/{2} ({3} vahvistettu)/{4} ({5} vahvistettu).</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>Uudet sovellukset: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
<value>Vahvistetut sovellukset: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
<value>Uudet paketit: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
<value>Vahvistetut paketit: {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>Uudet depotit: {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>Vahvistetut depotit: {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} alustettu, laajennus ei käsittele yhtään näistä: {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>Ladataan STD:n globaalia välimuistia...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Tarkistetaan STD-välimuistin eheys...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>STD:n globaalin välimuistin eheyden varmistaminen epäonnistui. Tämä viittaa mahdolliseen tiedoston/muistin korruptioon, uusi instanssi käynnistetään sen sijaan.</value>
</data>
</root>

View File

@@ -168,4 +168,13 @@
<value>{0} initialisé, le plugin ne résoudra aucun de ceux-ci : {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>Chargement du cache STD global...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Validation de l'intégrité du cache STD global...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Impossible de vérifier l'intégrité du cache STD global. Cela peut être due à une corruption potentielle de fichier/mémoire, une nouvelle instance va être créée.</value>
</data>
</root>

View File

@@ -82,11 +82,35 @@
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
<value>יישומים חדשים: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
<value>יישומים מאומתים: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
<value>חבילות חדשות: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
<value>חבילות מאומתות: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>
<data name="PluginSecretListInitialized" xml:space="preserve">
<value>{0} אותחל, הפלאגין לא יפתור אף אחד מאלה: {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>טוען מטמון עולמי מסוג STD...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>מאמת את שלמות מטמון ה-STD העולמי...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>נכשל אימות שלמות מטמון ה-STD העולמי. זה מרמז על פגיעה פוטנציאלית בקובץ/זיכרון, במקום זה הוא יאותחל מחדש.</value>
</data>
</root>

View File

@@ -88,6 +88,15 @@
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
<value>Új appok: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
<value>Új csomagok: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>

View File

@@ -101,13 +101,22 @@
<value>Ricezione di tutti i depositi per un totale di {0} app...</value>
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
</data>
<data name="BotRetrievingAppInfos" xml:space="preserve">
<value>Recuperate {0} informazioni app...</value>
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
<value>Hai completato il recupero di {0} informazioni app.</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>Recupero {0} chiavi...</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>Completato il recupero di {0} chiavi.</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>Finito il recupero di tutte le chiavi del deposito per un totale di {0} applicazioni.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
@@ -143,8 +152,29 @@
<value>Nuovi pacchetti: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
<value>Pacchetti verificati: {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>Nuove app: {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>App verificate: {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} inizializzato, il plugin non risolverà nessuno dei seguenti: {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>Caricamento cache globale STD...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Convalida integrità cache globale STD...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Impossibile verificare l'integrità globale della cache STD. Questo suggerisce un potenziale danneggiamento di file/memoria, una nuova istanza verrà inizializzata.</value>
</data>
</root>

View File

@@ -62,31 +62,87 @@
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
<value>{0} buvo išjungtas, dėl trūkstamos dalies</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginInitializedAndEnabled" xml:space="preserve">
<value>{0} buvo sėkmingai įrašytas, dėkojame už jūsų pagalbą. Pirma pateiktis įvyks už maždaug {1} nuo dabar.</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
<value>{0} nepavyko užkrauti, bus įrašyta nauja instancija...</value>
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
</data>
<data name="BotNoAppsToRefresh" xml:space="preserve">
<value>Šiame robote nėra jokių programų, kurias reikėtų atnaujinti.</value>
</data>
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>Iš viso gaunama {0} programos prieigos raktų...</value>
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
</data>
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
<value>Gaunama {0} programos prieigos raktų...</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
<value>Baigta gauti {0} programos prieigos raktų.</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>Iš viso baigta gauti {0} programos prieigos raktų.</value>
<comment>{0} will be replaced by the number (total count) of app access tokens retrieved</comment>
</data>
<data name="BotRetrievingAppInfos" xml:space="preserve">
<value>Gaunama {0} programos informacijos...</value>
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
<value>Baigta gauti {0} programos informacijos.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="SubmissionNoNewData" xml:space="preserve">
<value>Nėra jokių naujų duomenų, kuriuos būtų galima pateikti, viskas jau atnaujinta.</value>
</data>
<data name="SubmissionFailedTooManyRequests" xml:space="preserve">
<value>Pateiktis nepavyko dėl per daug išsiustų prašymų, pradėsime iš naujo už maždaug {0} nuo dabar.</value>
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
<value>Naujos programos: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
<value>Patvirtintos programos: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
<value>Nauji paketai: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
<value>Patvirtinti paketai: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>
<data name="PluginSecretListInitialized" xml:space="preserve">
<value>{0} inicijuota, įskiepis neišspręs nė vieno iš šių dalykų: {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>Kraunama STD global talpykla...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Tvirtinamas STD global talpyklos vientisumas...</value>
</data>
</root>

View File

@@ -82,7 +82,22 @@
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
<value>Jaunas aplikācijas: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
<value>Pārbaudītas aplikācijas: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
<value>Jaunas pakas: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
<value>Pārbaudītas pakas: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>

View File

@@ -168,4 +168,13 @@
<value>{0} zainicjowano, wtyczka nie rozwiąże żadnego z tych: {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>Ładowanie globalnej pamięci podręcznej STD...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Sprawdzanie integralność globalnej pamięci podręcznej STD...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Nie udało się zweryfikować integralności globalnej pamięci podręcznej STD. Sugeruje to potencjalne uszkodzenie pliku/pamięci, zamiast tego zostanie zainicjowana nowa instancja.</value>
</data>
</root>

View File

@@ -63,42 +63,42 @@
</value>
</resheader>
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
<value>{0} foi desativado devido à falta de um token de compilação</value>
<value>O {0} foi desativado devido a um token de compilação ausente</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginDisabledInConfig" xml:space="preserve">
<value>{0} está desativado de acordo com sua configuração. Se você gostaria de ajudar o SteamDB no envio de dados, por favor, confira nosso wiki.</value>
<value>O {0} está desativado de acordo com a sua configuração. Caso deseje ajudar o SteamDB com o envio de informações, dê uma olhada na nossa wiki.</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginInitializedAndEnabled" xml:space="preserve">
<value>{0} foi inicializado com sucesso, obrigado antecipadamente pela sua ajuda. O primeiro envio ocorrerá em aproximadamente {1} a partir de agora.</value>
<value>O {0} foi inicializado com sucesso, agradecemos a sua ajuda. O primeiro envio ocorrerá em aproximadamente {1}.</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
<value>{0} não pôde ser carregado, uma instância nova será inicializada...</value>
<value>O {0} não pôde ser carregado, uma instância nova será inicializada...</value>
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
</data>
<data name="BotNoAppsToRefresh" xml:space="preserve">
<value>Não há aplicativos que necessitem de ser atualizados nesta instância de bot.</value>
<value>Não há aplicativos que exijam atualizações na instância atual.</value>
</data>
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>Recuperando um total de {0} tokens de acesso a aplicativos...</value>
<value>Recuperando um total de {0} tokens de acesso para aplicativos...</value>
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
</data>
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
<value>Recuperando {0} tokens de acesso a aplicativos...</value>
<value>Recuperando {0} tokens...</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
<value>Concluímos a recuperação de {0} tokens de acesso ao aplicativo.</value>
<value>Recuperamos {0} tokens.</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>Obtivemos um total de {0} tokens de acesso aos aplicativos.</value>
<value>Recuperamos um total de {0} tokens de acesso.</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>Recuperando todos os depósitos por um total de {0} apps...</value>
<value>Recuperando depots para todos os {0} aplicativos...</value>
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
</data>
<data name="BotRetrievingAppInfos" xml:space="preserve">
@@ -106,38 +106,38 @@
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
<value>Concluímos a recuperação de {0} informações de aplicativo.</value>
<value>Recuperamos um total de {0} informações de aplicativos.</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>Recuperando {0} chaves de depósito...</value>
<value>Recuperando {0} códigos de depots...</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>Terminamos de recuperar {0} chaves de depósito.</value>
<value>Recuperamos códigos para {0} depots.</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>Terminamos de recuperar todas as chaves de depósito para um total de {0} apps.</value>
<value>Recuperamos códigos de acesso para {0} aplicativos.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
</data>
<data name="SubmissionNoNewData" xml:space="preserve">
<value>Não há novos dados para enviar, tudo está atualizado.</value>
<value>Não há novos dados a serem enviados. Tudo está atualizado.</value>
</data>
<data name="SubmissionNoContributorSet" xml:space="preserve">
<value>Não foi possível enviar os dados porque não há um conjunto SteamID válido que possamos classificar como colaborador. Considere configurar a propriedade {0}.</value>
<value>Não foi possível enviar os dados porque não conseguimos identificar um ID Steam válido para definir como colaborador. Considere configurar a propriedade "{0}".</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>Enviando um total de apps/pacotes/pacotes registrados: {0}/{1}/{2}...</value>
<value>Enviando um total de {0} aplicativos, {1} pacotes e {2} depots registrados...</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>O envio falhou devido a muitas solicitações enviadas, tentaremos novamente em aproximadamente {0} a partir de agora.</value>
<value>O envio falhou porque muitas solicitações foram enviadas pelo cliente. Tentaremos novamente em aproximadamente {0}.</value>
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="SubmissionSuccessful" xml:space="preserve">
<value>Os dados foram enviados com sucesso. O servidor registrou um total de novos aplicativos/pacotes/depósitos: {0} ({1} verificado)/{2} ({3} verificado)/{4} ({5} verificado).</value>
<value>Os dados foram enviados com sucesso. O servidor registrou um total de {0} aplicativos ({1} verificados), {2} pacotes ({3} verificados) e {4} depots ({5} verificados).</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">
@@ -157,15 +157,24 @@
<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>Novos depósitos: {0}</value>
<value>Novos depots: {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>Depósitos verificados: {0}</value>
<value>Depots verificados: {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} inicializado, o plugin não resolverá nenhum desses: {1}.</value>
<value>{0} inicializado, o plugin ignorará os seguintes pacotes: {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>Carregando cache global do STD...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Validando integridade do cache global do STD...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Falha ao verificar a integridade do cache global do STD. Isto pode indicar uma possível corrupção de arquivo/memória, uma nova instância será inicializada.</value>
</data>
</root>

View File

@@ -168,4 +168,13 @@
<value>{0} INITIALIZD, TEH PLUGIN WILL NOT RESOLVE ANY OV DOSE: {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>LOADIN STD GLOBAL CACHE...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>VALIDATIN STD GLOBAL CACHE INTEGRITY...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>FAILD 2 VERIFY STD GLOBAL CACHE INTEGRITY. DIS SUGGESTS POTENTIAL FILE/MEMS CORRUPSHUN, FRESH INSTANCE WILL BE INITIALIZD INSTEAD.</value>
</data>
</root>

View File

@@ -168,4 +168,13 @@
<value>{0} initialized, the plugin will not resolve any of those: {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>Loading STD global cache...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Validating STD global cache integrity...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Failed to verify STD global cache integrity. This suggests a potential file/memory corruption, a fresh instance will be initialized instead.</value>
</data>
</root>

View File

@@ -164,5 +164,17 @@
<value>Количество подтвержденных хранилищ: {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} инициализирован, плагин не взаимодействует ни с одним из их: {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>Загрузка глобального кэша STD...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Проверка целостности глобального кэша STD...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Не удалось проверить целостность глобального кэша STD. Это говорит о потенциальном повреждении файла/памяти, вместо этого будет инициализирован новый экземпляр.</value>
</data>
</root>

View File

@@ -168,4 +168,7 @@
<value>{0} inicializovaných, modul nebude pracovať so žiadnym: {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>
</root>

View File

@@ -168,4 +168,13 @@
<value>{0} başlatıldı, eklenti şunlardan hiçbirini çözemedi: {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>STD küresel önbelleği yükleniyor...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>STD küresel önbellek bütünlüğü doğrulanıyor...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>STD küresel önbellek bütünlüğü doğrulanamadı. Bu, olası bir dosya/bellek bozulması olduğunu gösterir, bunun yerine yeni bir örnek başlatılacaktır.</value>
</data>
</root>

View File

@@ -168,4 +168,13 @@
<value>{0} đã được khởi tạo, plugin sẽ không can thiệp với những thứ sau: {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>Đang tải bộ nhớ đệm STD chung...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Đang kiểm tra trạng thái bộ nhớ đệm STD chung...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Không thể xác minh trạng thái bộ nhớ đệm chung STD. Điều này có khả năng do tệp/bộ nhớ bị hỏng, một trạng thái mới sẽ được khởi tạo thay thế.</value>
</data>
</root>

View File

@@ -168,4 +168,13 @@
<value>{0} 已初始化,插件无法解析以下任何内容:{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>正在加载 STD 全局缓存……</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>正在验证 STD 全局缓存完整性……</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>验证 STD 全局缓存完整性失败。这意味着潜在的文件或内存损坏,即将初始化新实例作为替代。</value>
</data>
</root>

View File

@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns="" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
<value>由於 {0} 缺少組建權杖而被停用</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginDisabledInConfig" xml:space="preserve">
<value>目前 {0} 已根據您的設定被停用。如果您想幫助 SteamDB 提交資料,請查看我們的 Wiki。</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginInitializedAndEnabled" xml:space="preserve">
<value>已成功初始化 {0},先感謝您的幫助。第一次提交將大約在 {1} 後進行。</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
<value>無法載入 {0},將初始化一個新實例…</value>
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
</data>
<data name="BotNoAppsToRefresh" xml:space="preserve">
<value>此 Bot 中沒有需要再刷新的應用程式。</value>
</data>
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>正在檢索共 {0} 個應用程式存取權杖…</value>
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
</data>
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
<value>正在檢索 {0} 個應用程式存取權杖…</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
<value>已完成檢索 {0} 個應用程式存取權杖。</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>已完成檢索共 {0} 個應用程式存取權杖。</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>正在檢索共 {0} 個應用程式的 Depot…</value>
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
</data>
<data name="BotRetrievingAppInfos" xml:space="preserve">
<value>正在檢索 {0} 個應用程式資料…</value>
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
<value>已完成檢索 {0} 個應用程式資料。</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>正在檢索 {0} 個應用程式的 Depot 金鑰…</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>已完成檢索 {0} 個應用程式的 Depot 金鑰。</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>已完成檢索共 {0} 個應用程式的 Depot 金鑰。</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
</data>
<data name="SubmissionNoNewData" xml:space="preserve">
<value>沒有要提交的新資料,一切都是最新狀態。</value>
</data>
<data name="SubmissionNoContributorSet" xml:space="preserve">
<value>無法提交資料,因為沒有可以讓我們歸類為貢獻者的有效 SteamID 集。 考慮設定 {0} 屬性。</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>正在提交註冊的應用程式/程式包/Depot 共:{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>由於發送的請求過多導致提交失敗,我們將在約 {0} 後重試。</value>
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="SubmissionSuccessful" xml:space="preserve">
<value>已成功提交資料。伺服器共已註冊了新的應用程式/程式包/Depot 共:{0} ({1} 個已驗證)/{2} ({3} 個已驗證)/{4} ({5} 個已驗證)。</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>新的應用程式:{0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
<value>已驗證的應用程式:{0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
<value>新的程式包:{0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
<value>已驗證的程式包:{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>新的 Depot{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>已被驗證的 Depot{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} 已被初始化,外掛程式將無法解析其中任何一個:{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>正在載入 STD 全域快取…</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>正在驗證 STD 全域快取完整性…</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>無法驗證 STD 全域快取完整性。這表示可能有檔案/記憶損壞,將初始化一個新實例。</value>
</data>
</root>

View File

@@ -1,92 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns="" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
</root>

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -27,53 +27,45 @@ using ArchiSteamFarm.Core;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
internal sealed class RequestData {
[JsonProperty(PropertyName = "guid", Required = Required.Always)]
private static string Guid => ASF.GlobalDatabase?.Identifier.ToString("N") ?? throw new InvalidOperationException(nameof(ASF.GlobalDatabase.Identifier));
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
[JsonProperty(PropertyName = "token", Required = Required.Always)]
private static string Token => SharedInfo.Token;
internal sealed class RequestData {
[JsonProperty("guid", Required = Required.Always)]
private static string Guid => ASF.GlobalDatabase?.Identifier.ToString("N") ?? throw new InvalidOperationException(nameof(ASF.GlobalDatabase.Identifier));
[JsonProperty(PropertyName = "v", Required = Required.Always)]
private static byte Version => SharedInfo.ApiVersion;
[JsonProperty("token", Required = Required.Always)]
private static string Token => SharedInfo.Token;
[JsonProperty(PropertyName = "apps", Required = Required.Always)]
private readonly ImmutableDictionary<string, string> Apps;
[JsonProperty("v", Required = Required.Always)]
private static byte Version => SharedInfo.ApiVersion;
[JsonProperty(PropertyName = "depots", Required = Required.Always)]
private readonly ImmutableDictionary<string, string> Depots;
[JsonProperty("apps", Required = Required.Always)]
private readonly ImmutableDictionary<string, string> Apps;
private readonly ulong SteamID;
[JsonProperty("depots", Required = Required.Always)]
private readonly ImmutableDictionary<string, string> Depots;
[JsonProperty(PropertyName = "subs", Required = Required.Always)]
private readonly ImmutableDictionary<string, string> Subs;
private readonly ulong SteamID;
[JsonProperty(PropertyName = "steamid", Required = Required.Always)]
private string SteamIDText => new SteamID(SteamID).Render();
[JsonProperty("subs", Required = Required.Always)]
private readonly ImmutableDictionary<string, string> Subs;
internal RequestData(ulong steamID, IReadOnlyCollection<KeyValuePair<uint, ulong>> apps, IReadOnlyCollection<KeyValuePair<uint, ulong>> accessTokens, IReadOnlyCollection<KeyValuePair<uint, string>> depots) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
[JsonProperty("steamid", Required = Required.Always)]
private string SteamIDText => new SteamID(SteamID).Render();
if (apps == null) {
throw new ArgumentNullException(nameof(apps));
}
if (accessTokens == null) {
throw new ArgumentNullException(nameof(accessTokens));
}
if (depots == null) {
throw new ArgumentNullException(nameof(depots));
}
SteamID = steamID;
Apps = apps.ToImmutableDictionary(static app => app.Key.ToString(CultureInfo.InvariantCulture), static app => app.Value.ToString(CultureInfo.InvariantCulture));
Subs = accessTokens.ToImmutableDictionary(static package => package.Key.ToString(CultureInfo.InvariantCulture), static package => package.Value.ToString(CultureInfo.InvariantCulture));
Depots = depots.ToImmutableDictionary(static depot => depot.Key.ToString(CultureInfo.InvariantCulture), static depot => depot.Value);
internal RequestData(ulong steamID, IReadOnlyCollection<KeyValuePair<uint, ulong>> apps, IReadOnlyCollection<KeyValuePair<uint, ulong>> accessTokens, IReadOnlyCollection<KeyValuePair<uint, string>> depots) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
ArgumentNullException.ThrowIfNull(apps);
ArgumentNullException.ThrowIfNull(accessTokens);
ArgumentNullException.ThrowIfNull(depots);
SteamID = steamID;
Apps = apps.ToImmutableDictionary(static app => app.Key.ToString(CultureInfo.InvariantCulture), static app => app.Value.ToString(CultureInfo.InvariantCulture));
Subs = accessTokens.ToImmutableDictionary(static package => package.Key.ToString(CultureInfo.InvariantCulture), static package => package.Value.ToString(CultureInfo.InvariantCulture));
Depots = depots.ToImmutableDictionary(static depot => depot.Key.ToString(CultureInfo.InvariantCulture), static depot => depot.Value);
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,45 +23,45 @@ using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class ResponseData {
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class ResponseData {
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(PropertyName = "data", Required = Required.DisallowNull)]
internal readonly InternalData? Data;
[JsonProperty("data", Required = Required.DisallowNull)]
internal readonly InternalData? Data;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(PropertyName = "success", Required = Required.Always)]
internal readonly bool Success;
[JsonProperty("success", Required = Required.Always)]
internal readonly bool Success;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonConstructor]
private ResponseData() { }
internal sealed class InternalData {
[JsonProperty("new_apps", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> NewApps = ImmutableHashSet<uint>.Empty;
[JsonProperty("new_depots", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> NewDepots = ImmutableHashSet<uint>.Empty;
[JsonProperty("new_subs", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> NewPackages = ImmutableHashSet<uint>.Empty;
[JsonProperty("verified_apps", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> VerifiedApps = ImmutableHashSet<uint>.Empty;
[JsonProperty("verified_depots", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> VerifiedDepots = ImmutableHashSet<uint>.Empty;
[JsonProperty("verified_subs", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> VerifiedPackages = ImmutableHashSet<uint>.Empty;
[JsonConstructor]
private ResponseData() { }
internal sealed class InternalData {
[JsonProperty(PropertyName = "new_apps", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> NewApps = ImmutableHashSet<uint>.Empty;
[JsonProperty(PropertyName = "new_depots", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> NewDepots = ImmutableHashSet<uint>.Empty;
[JsonProperty(PropertyName = "new_subs", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> NewPackages = ImmutableHashSet<uint>.Empty;
[JsonProperty(PropertyName = "verified_apps", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> VerifiedApps = ImmutableHashSet<uint>.Empty;
[JsonProperty(PropertyName = "verified_depots", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> VerifiedDepots = ImmutableHashSet<uint>.Empty;
[JsonProperty(PropertyName = "verified_subs", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> VerifiedPackages = ImmutableHashSet<uint>.Empty;
[JsonConstructor]
private InternalData() { }
}
private InternalData() { }
}
#pragma warning restore CA1812 // False positive, the class is used during json deserialization
}
#pragma warning restore CA1812 // False positive, the class is used during json deserialization

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,17 +19,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
internal static class SharedInfo {
internal const byte ApiVersion = 2;
internal const byte AppInfosPerSingleRequest = byte.MaxValue;
internal const byte MaximumHoursBetweenRefresh = 8; // Per single bot account, makes sense to be 2 or 3 times less than MinimumHoursBetweenUploads
internal const byte MaximumMinutesBeforeFirstUpload = 60; // Must be greater or equal to MinimumMinutesBeforeFirstUpload
internal const byte MinimumHoursBetweenUploads = 24;
internal const byte MinimumMinutesBeforeFirstUpload = 10; // Must be less or equal to MaximumMinutesBeforeFirstUpload
internal const string ServerURL = "https://asf-token-dumper.xpaw.me";
internal const string Token = "STEAM_TOKEN_DUMPER_TOKEN"; // This is filled automatically during our CI build with API key provided by xPaw for ASF project
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
internal static bool HasValidToken => Token.Length == 128;
}
internal static class SharedInfo {
internal const byte ApiVersion = 2;
internal const byte AppInfosPerSingleRequest = byte.MaxValue;
internal const byte HoursBetweenUploads = 24;
internal const byte MaximumHoursBetweenRefresh = 8; // Per single bot account, makes sense to be 2 or 3 times less than MinimumHoursBetweenUploads
internal const byte MaximumMinutesBeforeFirstUpload = 60; // Must be greater or equal to MinimumMinutesBeforeFirstUpload
internal const byte MinimumMinutesBeforeFirstUpload = 10; // Must be less or equal to MaximumMinutesBeforeFirstUpload
internal const byte MinimumMinutesBetweenUploads = 5; // Rate limiting for the server
internal const string ServerURL = "https://asf-token-dumper.xpaw.me";
internal const string Token = "STEAM_TOKEN_DUMPER_TOKEN"; // This is filled automatically during our CI build with API key provided by xPaw for ASF project
internal static bool HasValidToken => Token.Length == 128;
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,28 +24,28 @@ using System.Diagnostics.CodeAnalysis;
using ArchiSteamFarm.IPC.Integration;
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class SteamTokenDumperConfig {
[JsonProperty(Required = Required.DisallowNull)]
public bool Enabled { get; internal set; }
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
[JsonProperty(Required = Required.DisallowNull)]
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
public ImmutableHashSet<uint> SecretAppIDs { get; private set; } = ImmutableHashSet<uint>.Empty;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class SteamTokenDumperConfig {
[JsonProperty(Required = Required.DisallowNull)]
public bool Enabled { get; internal set; }
[JsonProperty(Required = Required.DisallowNull)]
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
public ImmutableHashSet<uint> SecretDepotIDs { get; private set; } = ImmutableHashSet<uint>.Empty;
[JsonProperty(Required = Required.DisallowNull)]
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
public ImmutableHashSet<uint> SecretAppIDs { get; private set; } = ImmutableHashSet<uint>.Empty;
[JsonProperty(Required = Required.DisallowNull)]
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
public ImmutableHashSet<uint> SecretPackageIDs { get; private set; } = ImmutableHashSet<uint>.Empty;
[JsonProperty(Required = Required.DisallowNull)]
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
public ImmutableHashSet<uint> SecretDepotIDs { get; private set; } = ImmutableHashSet<uint>.Empty;
[JsonProperty(Required = Required.DisallowNull)]
public bool SkipAutoGrantPackages { get; private set; }
[JsonProperty(Required = Required.DisallowNull)]
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
public ImmutableHashSet<uint> SecretPackageIDs { get; private set; } = ImmutableHashSet<uint>.Empty;
[JsonConstructor]
internal SteamTokenDumperConfig() { }
}
[JsonProperty(Required = Required.DisallowNull)]
public bool SkipAutoGrantPackages { get; private set; } = true;
[JsonConstructor]
internal SteamTokenDumperConfig() { }
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,12 +24,12 @@ using ArchiSteamFarm.IPC.Controllers.Api;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
[Route("Api/SteamTokenDumperPlugin")]
public sealed class SteamTokenDumperController : ArchiController {
[HttpGet(nameof(GlobalConfigExtension))]
[ProducesResponseType(typeof(GlobalConfigExtension), (int) HttpStatusCode.OK)]
[SwaggerOperation(Tags = new[] { nameof(GlobalConfigExtension) })]
public ActionResult<GlobalConfigExtension> Get() => Ok(new GlobalConfigExtension());
}
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
[Route("Api/SteamTokenDumperPlugin")]
public sealed class SteamTokenDumperController : ArchiController {
[HttpGet(nameof(GlobalConfigExtension))]
[ProducesResponseType(typeof(GlobalConfigExtension), (int) HttpStatusCode.OK)]
[SwaggerOperation(Tags = new[] { nameof(GlobalConfigExtension) })]
public ActionResult<GlobalConfigExtension> Get() => Ok(new GlobalConfigExtension());
}

View File

@@ -5,6 +5,7 @@
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="MSTest.TestAdapter" />
<PackageReference Include="MSTest.TestFramework" />

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,485 +26,480 @@ using ArchiSteamFarm.Steam.Data;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static ArchiSteamFarm.Steam.Bot;
namespace ArchiSteamFarm.Tests {
[TestClass]
public sealed class Bot {
[TestMethod]
public void MaxItemsBarelyEnoughForOneSet() {
const uint relevantAppID = 42;
namespace ArchiSteamFarm.Tests;
Dictionary<uint, byte> itemsPerSet = new() {
{ relevantAppID, MinCardsPerBadge },
{ 43, MinCardsPerBadge + 1 }
};
[TestClass]
public sealed class Bot {
[TestMethod]
public void MaxItemsBarelyEnoughForOneSet() {
const uint relevantAppID = 42;
HashSet<Asset> items = new();
Dictionary<uint, byte> itemsPerSet = new() {
{ relevantAppID, MinCardsPerBadge },
{ 43, MinCardsPerBadge + 1 }
};
foreach ((uint appID, byte cards) in itemsPerSet) {
for (byte i = 1; i <= cards; i++) {
items.Add(CreateCard(i, appID));
}
HashSet<Asset> items = new();
foreach ((uint appID, byte cards) in itemsPerSet) {
for (byte i = 1; i <= cards; i++) {
items.Add(CreateCard(i, appID));
}
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, itemsPerSet, MinCardsPerBadge);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = items.Where(static item => item.RealAppID == relevantAppID).GroupBy(static item => (item.RealAppID, item.ContextID, item.ClassID)).ToDictionary(static group => group.Key, static group => (uint) group.Sum(static item => item.Amount));
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void MaxItemsTooSmall() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(2, appID)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, itemsPerSet, MinCardsPerBadge);
GetItemsForFullBadge(items, 2, appID, MinCardsPerBadge - 1);
Assert.Fail();
}
[TestMethod]
public void MoreCardsThanNeeded() {
const uint appID = 42;
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = items.Where(static item => item.RealAppID == relevantAppID).GroupBy(static item => (item.RealAppID, item.ContextID, item.ClassID)).ToDictionary(static group => group.Key, static group => (uint) group.Sum(static item => item.Amount));
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(1, appID),
CreateCard(2, appID),
CreateCard(3, appID)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 1 },
{ (appID, Asset.SteamCommunityContextID, 2), 1 },
{ (appID, Asset.SteamCommunityContextID, 3), 1 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void MultipleSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(1, appID),
CreateCard(2, appID),
CreateCard(2, appID)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void MaxItemsTooSmall() {
const uint appID = 42;
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 2 },
{ (appID, Asset.SteamCommunityContextID, 2), 2 }
};
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(2, appID)
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
GetItemsForFullBadge(items, 2, appID, MinCardsPerBadge - 1);
[TestMethod]
public void MultipleSetsDifferentAmount() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, 2),
CreateCard(2, appID),
CreateCard(2, appID)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 2 },
{ (appID, Asset.SteamCommunityContextID, 2), 2 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void MutliRarityAndType() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Common),
CreateCard(2, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Common),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Uncommon),
CreateCard(2, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Uncommon),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Rare),
CreateCard(2, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Rare),
// for better readability and easier verification when thinking about this test the items that shall be selected for sending are the ones below this comment
CreateCard(1, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Uncommon),
CreateCard(2, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Uncommon),
CreateCard(3, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Uncommon),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Common),
CreateCard(3, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Common),
CreateCard(7, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Common),
CreateCard(2, appID, type: Asset.EType.Unknown, rarity: Asset.ERarity.Rare),
CreateCard(3, appID, type: Asset.EType.Unknown, rarity: Asset.ERarity.Rare),
CreateCard(4, appID, type: Asset.EType.Unknown, rarity: Asset.ERarity.Rare)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 2 },
{ (appID, Asset.SteamCommunityContextID, 2), 2 },
{ (appID, Asset.SteamCommunityContextID, 3), 3 },
{ (appID, Asset.SteamCommunityContextID, 4), 1 },
{ (appID, Asset.SteamCommunityContextID, 7), 1 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void NotAllCardsPresent() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(2, appID)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0);
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OneSet() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(2, appID)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 1 },
{ (appID, Asset.SteamCommunityContextID, 2), 1 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherAppIDFullSets() {
const uint appID0 = 42;
const uint appID1 = 43;
HashSet<Asset> items = new() {
CreateCard(1, appID0),
CreateCard(1, appID1)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
{ appID0, 1 },
{ appID1, 1 }
}
);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID0, Asset.SteamCommunityContextID, 1), 1 },
{ (appID1, Asset.SteamCommunityContextID, 1), 1 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherAppIDNoSets() {
const uint appID0 = 42;
const uint appID1 = 43;
HashSet<Asset> items = new() {
CreateCard(1, appID0),
CreateCard(1, appID1)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
{ appID0, 2 },
{ appID1, 2 }
}
);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0);
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherAppIDOneSet() {
const uint appID0 = 42;
const uint appID1 = 43;
const uint appID2 = 44;
HashSet<Asset> items = new() {
CreateCard(1, appID0),
CreateCard(2, appID0),
CreateCard(1, appID1),
CreateCard(2, appID1),
CreateCard(3, appID1)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
{ appID0, 3 },
{ appID1, 3 },
{ appID2, 3 }
}
);
Assert.Fail();
}
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID1, Asset.SteamCommunityContextID, 1), 1 },
{ (appID1, Asset.SteamCommunityContextID, 2), 1 },
{ (appID1, Asset.SteamCommunityContextID, 3), 1 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherRarityFullSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, rarity: Asset.ERarity.Common),
CreateCard(1, appID, rarity: Asset.ERarity.Rare)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 2 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherRarityNoSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, rarity: Asset.ERarity.Common),
CreateCard(1, appID, rarity: Asset.ERarity.Rare)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0);
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherRarityOneSet() {
const uint appID = 42;
[TestMethod]
public void MoreCardsThanNeeded() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, rarity: Asset.ERarity.Common),
CreateCard(2, appID, rarity: Asset.ERarity.Common),
CreateCard(1, appID, rarity: Asset.ERarity.Uncommon),
CreateCard(2, appID, rarity: Asset.ERarity.Uncommon),
CreateCard(3, appID, rarity: Asset.ERarity.Uncommon)
};
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(1, appID),
CreateCard(2, appID),
CreateCard(3, appID)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 1 },
{ (appID, Asset.SteamCommunityContextID, 2), 1 },
{ (appID, Asset.SteamCommunityContextID, 3), 1 }
};
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 1 },
{ (appID, Asset.SteamCommunityContextID, 2), 1 },
{ (appID, Asset.SteamCommunityContextID, 3), 1 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherTypeFullSets() {
const uint appID = 42;
[TestMethod]
public void MultipleSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, type: Asset.EType.TradingCard),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard)
};
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(1, appID),
CreateCard(2, appID),
CreateCard(2, appID)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 2 }
};
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 2 },
{ (appID, Asset.SteamCommunityContextID, 2), 2 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherTypeNoSets() {
const uint appID = 42;
[TestMethod]
public void MultipleSetsDifferentAmount() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, type: Asset.EType.TradingCard),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard)
};
HashSet<Asset> items = new() {
CreateCard(1, appID, 2),
CreateCard(2, appID),
CreateCard(2, appID)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 2 },
{ (appID, Asset.SteamCommunityContextID, 2), 2 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherTypeOneSet() {
const uint appID = 42;
[TestMethod]
public void MutliRarityAndType() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, type: Asset.EType.TradingCard),
CreateCard(2, appID, type: Asset.EType.TradingCard),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard),
CreateCard(2, appID, type: Asset.EType.FoilTradingCard),
CreateCard(3, appID, type: Asset.EType.FoilTradingCard)
};
HashSet<Asset> items = new() {
CreateCard(1, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Common),
CreateCard(2, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Common),
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
CreateCard(1, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Uncommon),
CreateCard(2, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Uncommon),
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 1 },
{ (appID, Asset.SteamCommunityContextID, 2), 1 },
{ (appID, Asset.SteamCommunityContextID, 3), 1 }
};
CreateCard(1, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Rare),
CreateCard(2, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Rare),
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
// for better readability and easier verification when thinking about this test the items that shall be selected for sending are the ones below this comment
CreateCard(1, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Uncommon),
CreateCard(2, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Uncommon),
CreateCard(3, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Uncommon),
[TestMethod]
public void TooHighAmount() {
const uint appID0 = 42;
CreateCard(1, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Common),
CreateCard(3, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Common),
CreateCard(7, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Common),
CreateCard(2, appID, type: Asset.EType.Unknown, rarity: Asset.ERarity.Rare),
CreateCard(3, appID, type: Asset.EType.Unknown, rarity: Asset.ERarity.Rare),
CreateCard(4, appID, type: Asset.EType.Unknown, rarity: Asset.ERarity.Rare)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 2 },
{ (appID, Asset.SteamCommunityContextID, 2), 2 },
{ (appID, Asset.SteamCommunityContextID, 3), 3 },
{ (appID, Asset.SteamCommunityContextID, 4), 1 },
{ (appID, Asset.SteamCommunityContextID, 7), 1 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void NotAllCardsPresent() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(2, appID)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0);
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
HashSet<Asset> items = new() {
CreateCard(1, appID0, 2),
CreateCard(2, appID0)
};
[TestMethod]
public void OneSet() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(2, appID)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID0);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 1 },
{ (appID, Asset.SteamCommunityContextID, 2), 1 }
};
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID0, Asset.SteamCommunityContextID, 1), 1 },
{ (appID0, Asset.SteamCommunityContextID, 2), 1 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherAppIDFullSets() {
const uint appID0 = 42;
const uint appID1 = 43;
[TestMethod]
public void TooManyCardsForSingleTrade() {
const uint appID = 42;
HashSet<Asset> items = new();
for (byte i = 0; i < Steam.Exchange.Trading.MaxItemsPerTrade; i++) {
items.Add(CreateCard(1, appID));
items.Add(CreateCard(2, appID));
HashSet<Asset> items = new() {
CreateCard(1, appID0),
CreateCard(1, appID1)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
{ appID0, 1 },
{ appID1, 1 }
}
);
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID0, Asset.SteamCommunityContextID, 1), 1 },
{ (appID1, Asset.SteamCommunityContextID, 1), 1 }
};
Assert.IsTrue(itemsToSend.Count <= Steam.Exchange.Trading.MaxItemsPerTrade);
}
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void TooManyCardsForSingleTradeMultipleAppIDs() {
const uint appID0 = 42;
const uint appID1 = 43;
[TestMethod]
public void OtherAppIDNoSets() {
const uint appID0 = 42;
const uint appID1 = 43;
HashSet<Asset> items = new();
HashSet<Asset> items = new() {
CreateCard(1, appID0),
CreateCard(1, appID1)
};
for (byte i = 0; i < 100; i++) {
items.Add(CreateCard(1, appID0));
items.Add(CreateCard(2, appID0));
items.Add(CreateCard(1, appID1));
items.Add(CreateCard(2, appID1));
}
Dictionary<uint, byte> itemsPerSet = new() {
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
{ appID0, 2 },
{ appID1, 2 }
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, itemsPerSet);
Assert.IsTrue(itemsToSend.Count <= Steam.Exchange.Trading.MaxItemsPerTrade);
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void TooManyCardsPerSet() {
const uint appID0 = 42;
const uint appID1 = 43;
const uint appID2 = 44;
HashSet<Asset> items = new() {
CreateCard(1, appID0),
CreateCard(2, appID0),
CreateCard(3, appID0),
CreateCard(4, appID0)
};
GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
{ appID0, 3 },
{ appID1, 3 },
{ appID2, 3 }
}
);
Assert.Fail();
}
private static void AssertResultMatchesExpectation(IReadOnlyDictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult, IReadOnlyCollection<Asset> itemsToSend) {
if (expectedResult == null) {
throw new ArgumentNullException(nameof(expectedResult));
}
);
if (itemsToSend == null) {
throw new ArgumentNullException(nameof(itemsToSend));
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0);
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherAppIDOneSet() {
const uint appID0 = 42;
const uint appID1 = 43;
const uint appID2 = 44;
HashSet<Asset> items = new() {
CreateCard(1, appID0),
CreateCard(2, appID0),
CreateCard(1, appID1),
CreateCard(2, appID1),
CreateCard(3, appID1)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
{ appID0, 3 },
{ appID1, 3 },
{ appID2, 3 }
}
);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), long> realResult = itemsToSend.GroupBy(static asset => (asset.RealAppID, asset.ContextID, asset.ClassID)).ToDictionary(static group => group.Key, static group => group.Sum(static asset => asset.Amount));
Assert.AreEqual(expectedResult.Count, realResult.Count);
Assert.IsTrue(expectedResult.All(expectation => realResult.TryGetValue(expectation.Key, out long reality) && (expectation.Value == reality)));
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID1, Asset.SteamCommunityContextID, 1), 1 },
{ (appID1, Asset.SteamCommunityContextID, 2), 1 },
{ (appID1, Asset.SteamCommunityContextID, 3), 1 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherRarityFullSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, rarity: Asset.ERarity.Common),
CreateCard(1, appID, rarity: Asset.ERarity.Rare)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 2 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherRarityNoSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, rarity: Asset.ERarity.Common),
CreateCard(1, appID, rarity: Asset.ERarity.Rare)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0);
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherRarityOneSet() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, rarity: Asset.ERarity.Common),
CreateCard(2, appID, rarity: Asset.ERarity.Common),
CreateCard(1, appID, rarity: Asset.ERarity.Uncommon),
CreateCard(2, appID, rarity: Asset.ERarity.Uncommon),
CreateCard(3, appID, rarity: Asset.ERarity.Uncommon)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 1 },
{ (appID, Asset.SteamCommunityContextID, 2), 1 },
{ (appID, Asset.SteamCommunityContextID, 3), 1 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherTypeFullSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, type: Asset.EType.TradingCard),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 2 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherTypeNoSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, type: Asset.EType.TradingCard),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0);
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void OtherTypeOneSet() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, type: Asset.EType.TradingCard),
CreateCard(2, appID, type: Asset.EType.TradingCard),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard),
CreateCard(2, appID, type: Asset.EType.FoilTradingCard),
CreateCard(3, appID, type: Asset.EType.FoilTradingCard)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID, Asset.SteamCommunityContextID, 1), 1 },
{ (appID, Asset.SteamCommunityContextID, 2), 1 },
{ (appID, Asset.SteamCommunityContextID, 3), 1 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void TooHighAmount() {
const uint appID0 = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID0, 2),
CreateCard(2, appID0)
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID0);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
{ (appID0, Asset.SteamCommunityContextID, 1), 1 },
{ (appID0, Asset.SteamCommunityContextID, 2), 1 }
};
AssertResultMatchesExpectation(expectedResult, itemsToSend);
}
[TestMethod]
public void TooManyCardsForSingleTrade() {
const uint appID = 42;
HashSet<Asset> items = new();
for (byte i = 0; i < Steam.Exchange.Trading.MaxItemsPerTrade; i++) {
items.Add(CreateCard(1, appID));
items.Add(CreateCard(2, appID));
}
private static Asset CreateCard(ulong classID, uint realAppID, uint amount = 1, Asset.EType type = Asset.EType.TradingCard, Asset.ERarity rarity = Asset.ERarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, realAppID: realAppID, type: type, rarity: rarity);
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
private static HashSet<Asset> GetItemsForFullBadge(IReadOnlyCollection<Asset> inventory, byte cardsPerSet, uint appID, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) => GetItemsForFullBadge(inventory, new Dictionary<uint, byte> { { appID, cardsPerSet } }, maxItems);
Assert.IsTrue(itemsToSend.Count <= Steam.Exchange.Trading.MaxItemsPerTrade);
}
private static HashSet<Asset> GetItemsForFullBadge(IReadOnlyCollection<Asset> inventory, IDictionary<uint, byte> cardsPerSet, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) {
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), List<uint>> inventorySets = Steam.Exchange.Trading.GetInventorySets(inventory);
[TestMethod]
public void TooManyCardsForSingleTradeMultipleAppIDs() {
const uint appID0 = 42;
const uint appID1 = 43;
return GetItemsForFullSets(inventory, inventorySets.ToDictionary(static kv => kv.Key, kv => (SetsToExtract: inventorySets[kv.Key][0], cardsPerSet[kv.Key.RealAppID])), maxItems).ToHashSet();
HashSet<Asset> items = new();
for (byte i = 0; i < 100; i++) {
items.Add(CreateCard(1, appID0));
items.Add(CreateCard(2, appID0));
items.Add(CreateCard(1, appID1));
items.Add(CreateCard(2, appID1));
}
Dictionary<uint, byte> itemsPerSet = new() {
{ appID0, 2 },
{ appID1, 2 }
};
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, itemsPerSet);
Assert.IsTrue(itemsToSend.Count <= Steam.Exchange.Trading.MaxItemsPerTrade);
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void TooManyCardsPerSet() {
const uint appID0 = 42;
const uint appID1 = 43;
const uint appID2 = 44;
HashSet<Asset> items = new() {
CreateCard(1, appID0),
CreateCard(2, appID0),
CreateCard(3, appID0),
CreateCard(4, appID0)
};
GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
{ appID0, 3 },
{ appID1, 3 },
{ appID2, 3 }
}
);
Assert.Fail();
}
private static void AssertResultMatchesExpectation(IReadOnlyDictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult, IReadOnlyCollection<Asset> itemsToSend) {
ArgumentNullException.ThrowIfNull(expectedResult);
ArgumentNullException.ThrowIfNull(itemsToSend);
Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), long> realResult = itemsToSend.GroupBy(static asset => (asset.RealAppID, asset.ContextID, asset.ClassID)).ToDictionary(static group => group.Key, static group => group.Sum(static asset => asset.Amount));
Assert.AreEqual(expectedResult.Count, realResult.Count);
Assert.IsTrue(expectedResult.All(expectation => realResult.TryGetValue(expectation.Key, out long reality) && (expectation.Value == reality)));
}
private static Asset CreateCard(ulong classID, uint realAppID, uint amount = 1, Asset.EType type = Asset.EType.TradingCard, Asset.ERarity rarity = Asset.ERarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, realAppID: realAppID, type: type, rarity: rarity);
private static HashSet<Asset> GetItemsForFullBadge(IReadOnlyCollection<Asset> inventory, byte cardsPerSet, uint appID, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) => GetItemsForFullBadge(inventory, new Dictionary<uint, byte> { { appID, cardsPerSet } }, maxItems);
private static HashSet<Asset> GetItemsForFullBadge(IReadOnlyCollection<Asset> inventory, IDictionary<uint, byte> cardsPerSet, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) {
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), List<uint>> inventorySets = Steam.Exchange.Trading.GetInventorySets(inventory);
return GetItemsForFullSets(inventory, inventorySets.ToDictionary(static kv => kv.Key, kv => (SetsToExtract: inventorySets[kv.Key][0], cardsPerSet[kv.Key.RealAppID])), maxItems).ToHashSet();
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -27,179 +27,180 @@ using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static ArchiSteamFarm.Steam.Integration.SteamChatMessage;
namespace ArchiSteamFarm.Tests {
[TestClass]
public sealed class SteamChatMessage {
[TestMethod]
public async Task CanSplitEvenWithStupidlyLongPrefix() {
string prefix = new('x', MaxMessagePrefixBytes);
namespace ArchiSteamFarm.Tests;
const string emoji = "😎";
const string message = emoji + emoji + emoji + emoji;
[TestClass]
public sealed class SteamChatMessage {
[TestMethod]
public async Task CanSplitEvenWithStupidlyLongPrefix() {
string prefix = new('x', MaxMessagePrefixBytes);
List<string> output = await GetMessageParts(message, prefix, true).ToListAsync().ConfigureAwait(false);
const string emoji = "😎";
const string message = $"{emoji}{emoji}{emoji}{emoji}";
Assert.AreEqual(4, output.Count);
List<string> output = await GetMessageParts(message, prefix, true).ToListAsync().ConfigureAwait(false);
Assert.AreEqual(prefix + emoji + ContinuationCharacter, output[0]);
Assert.AreEqual(prefix + ContinuationCharacter + emoji + ContinuationCharacter, output[1]);
Assert.AreEqual(prefix + ContinuationCharacter + emoji + ContinuationCharacter, output[2]);
Assert.AreEqual(prefix + ContinuationCharacter + emoji, output[3]);
}
Assert.AreEqual(4, output.Count);
[TestMethod]
public void ContinuationCharacterSizeIsProperlyCalculated() => Assert.AreEqual(ContinuationCharacterBytes, Encoding.UTF8.GetByteCount(ContinuationCharacter.ToString()));
Assert.AreEqual($"{prefix}{emoji}{ContinuationCharacter}", output[0]);
Assert.AreEqual($"{prefix}{ContinuationCharacter}{emoji}{ContinuationCharacter}", output[1]);
Assert.AreEqual($"{prefix}{ContinuationCharacter}{emoji}{ContinuationCharacter}", output[2]);
Assert.AreEqual($"{prefix}{ContinuationCharacter}{emoji}", output[3]);
}
[TestMethod]
public async Task DoesntSkipEmptyNewlines() {
string message = $"asdf{Environment.NewLine}{Environment.NewLine}asdf";
[TestMethod]
public void ContinuationCharacterSizeIsProperlyCalculated() => Assert.AreEqual(ContinuationCharacterBytes, Encoding.UTF8.GetByteCount(ContinuationCharacter.ToString()));
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
[TestMethod]
public async Task DoesntSkipEmptyNewlines() {
string message = $"asdf{Environment.NewLine}{Environment.NewLine}asdf";
Assert.AreEqual(1, output.Count);
Assert.AreEqual(message, output.First());
}
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task DoesntSplitInTheMiddleOfMultiByteChar(bool isAccountLimited) {
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
int longLineLength = maxMessageBytes - ReservedContinuationMessageBytes;
Assert.AreEqual(1, output.Count);
Assert.AreEqual(message, output.First());
}
const string emoji = "😎";
[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task DoesntSplitInTheMiddleOfMultiByteChar(bool isAccountLimited) {
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
int longLineLength = maxMessageBytes - ReservedContinuationMessageBytes;
string longSequence = new('a', longLineLength - 1);
string message = longSequence + emoji;
const string emoji = "😎";
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
string longSequence = new('a', longLineLength - 1);
string message = $"{longSequence}{emoji}";
Assert.AreEqual(2, output.Count);
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
Assert.AreEqual(longSequence + ContinuationCharacter, output[0]);
Assert.AreEqual(ContinuationCharacter + emoji, output[1]);
}
Assert.AreEqual(2, output.Count);
[TestMethod]
public async Task DoesntSplitJustBecauseOfLastEscapableCharacter() {
const string message = "abcdef[";
const string escapedMessage = @"abcdef\[";
Assert.AreEqual($"{longSequence}{ContinuationCharacter}", output[0]);
Assert.AreEqual($"{ContinuationCharacter}{emoji}", output[1]);
}
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
[TestMethod]
public async Task DoesntSplitJustBecauseOfLastEscapableCharacter() {
const string message = "abcdef[";
const string escapedMessage = @"abcdef\[";
Assert.AreEqual(1, output.Count);
Assert.AreEqual(escapedMessage, output.First());
}
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task DoesntSplitOnBackslashNotUsedForEscaping(bool isAccountLimited) {
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
int longLineLength = maxMessageBytes - ReservedContinuationMessageBytes;
Assert.AreEqual(1, output.Count);
Assert.AreEqual(escapedMessage, output.First());
}
string longLine = new('a', longLineLength - 2);
string message = $@"{longLine}\";
[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task DoesntSplitOnBackslashNotUsedForEscaping(bool isAccountLimited) {
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
int longLineLength = maxMessageBytes - ReservedContinuationMessageBytes;
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
string longLine = new('a', longLineLength - 2);
string message = $@"{longLine}\";
Assert.AreEqual(1, output.Count);
Assert.AreEqual($@"{message}\", output.First());
}
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task DoesntSplitOnEscapeCharacter(bool isAccountLimited) {
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
int longLineLength = maxMessageBytes - ReservedContinuationMessageBytes;
Assert.AreEqual(1, output.Count);
Assert.AreEqual($@"{message}\", output.First());
}
string longLine = new('a', longLineLength - 1);
string message = $"{longLine}[";
[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task DoesntSplitOnEscapeCharacter(bool isAccountLimited) {
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
int longLineLength = maxMessageBytes - ReservedContinuationMessageBytes;
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
string longLine = new('a', longLineLength - 1);
string message = $"{longLine}[";
Assert.AreEqual(2, output.Count);
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
Assert.AreEqual(longLine + ContinuationCharacter, output[0]);
Assert.AreEqual($@"{ContinuationCharacter}\[", output[1]);
}
Assert.AreEqual(2, output.Count);
[TestMethod]
public async Task NoNeedForAnySplittingWithNewlines() {
string message = $"abcdef{Environment.NewLine}ghijkl{Environment.NewLine}mnopqr";
Assert.AreEqual($"{longLine}{ContinuationCharacter}", output[0]);
Assert.AreEqual($@"{ContinuationCharacter}\[", output[1]);
}
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
[TestMethod]
public async Task NoNeedForAnySplittingWithNewlines() {
string message = $"abcdef{Environment.NewLine}ghijkl{Environment.NewLine}mnopqr";
Assert.AreEqual(1, output.Count);
Assert.AreEqual(message, output.First());
}
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
[TestMethod]
public async Task NoNeedForAnySplittingWithoutNewlines() {
const string message = "abcdef";
Assert.AreEqual(1, output.Count);
Assert.AreEqual(message, output.First());
}
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
[TestMethod]
public async Task NoNeedForAnySplittingWithoutNewlines() {
const string message = "abcdef";
Assert.AreEqual(1, output.Count);
Assert.AreEqual(message, output.First());
}
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
[TestMethod]
public void ParagraphCharacterSizeIsLessOrEqualToContinuationCharacterSize() => Assert.IsTrue(ContinuationCharacterBytes >= Encoding.UTF8.GetByteCount(ParagraphCharacter.ToString()));
Assert.AreEqual(1, output.Count);
Assert.AreEqual(message, output.First());
}
[TestMethod]
public async Task ProperlyEscapesCharacters() {
const string message = @"[b]bold[/b] \n";
const string escapedMessage = @"\[b]bold\[/b] \\n";
[TestMethod]
public void ParagraphCharacterSizeIsLessOrEqualToContinuationCharacterSize() => Assert.IsTrue(ContinuationCharacterBytes >= Encoding.UTF8.GetByteCount(ParagraphCharacter.ToString()));
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
[TestMethod]
public async Task ProperlyEscapesCharacters() {
const string message = @"[b]bold[/b] \n";
const string escapedMessage = @"\[b]bold\[/b] \\n";
Assert.AreEqual(1, output.Count);
Assert.AreEqual(escapedMessage, output.First());
}
List<string> output = await GetMessageParts(message).ToListAsync().ConfigureAwait(false);
[TestMethod]
public async Task ProperlyEscapesSteamMessagePrefix() {
const string prefix = "/pre []";
const string escapedPrefix = @"/pre \[]";
Assert.AreEqual(1, output.Count);
Assert.AreEqual(escapedMessage, output.First());
}
const string message = "asdf";
[TestMethod]
public async Task ProperlyEscapesSteamMessagePrefix() {
const string prefix = "/pre []";
const string escapedPrefix = @"/pre \[]";
List<string> output = await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
const string message = "asdf";
Assert.AreEqual(1, output.Count);
Assert.AreEqual(escapedPrefix + message, output.First());
}
List<string> output = await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task ProperlySplitsLongSingleLine(bool isAccountLimited) {
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
int longLineLength = maxMessageBytes - ReservedContinuationMessageBytes;
Assert.AreEqual(1, output.Count);
Assert.AreEqual($"{escapedPrefix}{message}", output.First());
}
string longLine = new('a', longLineLength);
string message = longLine + longLine + longLine + longLine;
[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task ProperlySplitsLongSingleLine(bool isAccountLimited) {
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
int longLineLength = maxMessageBytes - ReservedContinuationMessageBytes;
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
string longLine = new('a', longLineLength);
string message = $"{longLine}{longLine}{longLine}{longLine}";
Assert.AreEqual(4, output.Count);
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
Assert.AreEqual(longLine + ContinuationCharacter, output[0]);
Assert.AreEqual(ContinuationCharacter + longLine + ContinuationCharacter, output[1]);
Assert.AreEqual(ContinuationCharacter + longLine + ContinuationCharacter, output[2]);
Assert.AreEqual(ContinuationCharacter + longLine, output[3]);
}
Assert.AreEqual(4, output.Count);
[TestMethod]
public void ReservedSizeForEscapingIsProperlyCalculated() => Assert.AreEqual(ReservedEscapeMessageBytes, Encoding.UTF8.GetByteCount(@"\") + 4); // Maximum amount of bytes per single UTF-8 character is 4, not 6 as from Encoding.UTF8.GetMaxByteCount(1)
Assert.AreEqual($"{longLine}{ContinuationCharacter}", output[0]);
Assert.AreEqual($"{ContinuationCharacter}{longLine}{ContinuationCharacter}", output[1]);
Assert.AreEqual($"{ContinuationCharacter}{longLine}{ContinuationCharacter}", output[2]);
Assert.AreEqual($"{ContinuationCharacter}{longLine}", output[3]);
}
[TestMethod]
public async Task RyzhehvostInitialTestForSplitting() {
const string prefix = "/me ";
[TestMethod]
public void ReservedSizeForEscapingIsProperlyCalculated() => Assert.AreEqual(ReservedEscapeMessageBytes, Encoding.UTF8.GetByteCount(@"\") + 4); // Maximum amount of bytes per single UTF-8 character is 4, not 6 as from Encoding.UTF8.GetMaxByteCount(1)
const string message = @"<XLimited5> Уже имеет: app/1493800 | Aircraft Carrier Survival: Prolouge
[TestMethod]
public async Task RyzhehvostInitialTestForSplitting() {
const string prefix = "/me ";
const string message = @"<XLimited5> Уже имеет: app/1493800 | Aircraft Carrier Survival: Prolouge
<XLimited5> Уже имеет: app/349520 | Armillo
<XLimited5> Уже имеет: app/346330 | BrainBread 2
<XLimited5> Уже имеет: app/1086690 | C-War 2
@@ -254,82 +255,81 @@ namespace ArchiSteamFarm.Tests {
<ASF> 1/1 ботов уже имеют игру app/269710 | Tumblestone.
<ASF> 1/1 ботов уже имеют игру app/304930 | Unturned.";
List<string> output = await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
List<string> output = await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
Assert.AreEqual(2, output.Count);
Assert.AreEqual(2, output.Count);
foreach (string messagePart in output) {
if ((messagePart.Length <= prefix.Length) || !messagePart.StartsWith(prefix, StringComparison.Ordinal)) {
Assert.Fail();
foreach (string messagePart in output) {
if ((messagePart.Length <= prefix.Length) || !messagePart.StartsWith(prefix, StringComparison.Ordinal)) {
Assert.Fail();
return;
}
string[] lines = messagePart.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
int bytes = lines.Where(static line => line.Length > 0).Sum(Encoding.UTF8.GetByteCount) + ((lines.Length - 1) * NewlineWeight);
if (bytes > MaxMessageBytesForUnlimitedAccounts) {
Assert.Fail();
return;
}
}
}
[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task SplitsOnNewlinesWithParagraphCharacter(bool isAccountLimited) {
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
StringBuilder newlinePartBuilder = new();
for (ushort bytes = 0; bytes < maxMessageBytes - ReservedContinuationMessageBytes - NewlineWeight;) {
if (newlinePartBuilder.Length > 0) {
bytes += NewlineWeight;
newlinePartBuilder.Append(Environment.NewLine);
}
bytes++;
newlinePartBuilder.Append('a');
return;
}
string newlinePart = newlinePartBuilder.ToString();
string message = newlinePart + Environment.NewLine + newlinePart + Environment.NewLine + newlinePart + Environment.NewLine + newlinePart;
string[] lines = messagePart.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
int bytes = lines.Where(static line => line.Length > 0).Sum(Encoding.UTF8.GetByteCount) + ((lines.Length - 1) * NewlineWeight);
Assert.AreEqual(4, output.Count);
if (bytes > MaxMessageBytesForUnlimitedAccounts) {
Assert.Fail();
Assert.AreEqual(newlinePart + ParagraphCharacter, output[0]);
Assert.AreEqual(newlinePart + ParagraphCharacter, output[1]);
Assert.AreEqual(newlinePart + ParagraphCharacter, output[2]);
Assert.AreEqual(newlinePart, output[3]);
}
[ExpectedException(typeof(ArgumentOutOfRangeException))]
[TestMethod]
public async Task ThrowsOnTooLongNewlinesPrefix() {
string prefix = new('\n', (MaxMessagePrefixBytes / NewlineWeight) + 1);
const string message = "asdf";
await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
Assert.Fail();
}
[ExpectedException(typeof(ArgumentOutOfRangeException))]
[TestMethod]
public async Task ThrowsOnTooLongPrefix() {
string prefix = new('x', MaxMessagePrefixBytes + 1);
const string message = "asdf";
await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
Assert.Fail();
return;
}
}
}
[DataRow(false)]
[DataRow(true)]
[DataTestMethod]
public async Task SplitsOnNewlinesWithParagraphCharacter(bool isAccountLimited) {
int maxMessageBytes = isAccountLimited ? MaxMessageBytesForLimitedAccounts : MaxMessageBytesForUnlimitedAccounts;
StringBuilder newlinePartBuilder = new();
for (ushort bytes = 0; bytes < maxMessageBytes - ReservedContinuationMessageBytes - NewlineWeight;) {
if (newlinePartBuilder.Length > 0) {
bytes += NewlineWeight;
newlinePartBuilder.Append(Environment.NewLine);
}
bytes++;
newlinePartBuilder.Append('a');
}
string newlinePart = newlinePartBuilder.ToString();
string message = $"{newlinePart}{Environment.NewLine}{newlinePart}{Environment.NewLine}{newlinePart}{Environment.NewLine}{newlinePart}";
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
Assert.AreEqual(4, output.Count);
Assert.AreEqual($"{newlinePart}{ParagraphCharacter}", output[0]);
Assert.AreEqual($"{newlinePart}{ParagraphCharacter}", output[1]);
Assert.AreEqual($"{newlinePart}{ParagraphCharacter}", output[2]);
Assert.AreEqual(newlinePart, output[3]);
}
[ExpectedException(typeof(ArgumentOutOfRangeException))]
[TestMethod]
public async Task ThrowsOnTooLongNewlinesPrefix() {
string prefix = new('\n', (MaxMessagePrefixBytes / NewlineWeight) + 1);
const string message = "asdf";
await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
Assert.Fail();
}
[ExpectedException(typeof(ArgumentOutOfRangeException))]
[TestMethod]
public async Task ThrowsOnTooLongPrefix() {
string prefix = new('x', MaxMessagePrefixBytes + 1);
const string message = "asdf";
await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
Assert.Fail();
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,369 +24,369 @@ using ArchiSteamFarm.Steam.Data;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static ArchiSteamFarm.Steam.Exchange.Trading;
namespace ArchiSteamFarm.Tests {
[TestClass]
public sealed class Trading {
[TestMethod]
public void MismatchRarityIsNotFair() {
HashSet<Asset> itemsToGive = new() { CreateItem(1, rarity: Asset.ERarity.Rare) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
namespace ArchiSteamFarm.Tests;
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
}
[TestClass]
public sealed class Trading {
[TestMethod]
public void MismatchRarityIsNotFair() {
HashSet<Asset> itemsToGive = new() { CreateItem(1, rarity: Asset.ERarity.Rare) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
[TestMethod]
public void MismatchRealAppIDsIsNotFair() {
HashSet<Asset> itemsToGive = new() { CreateItem(1, realAppID: 570) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
}
[TestMethod]
public void MismatchTypesIsNotFair() {
HashSet<Asset> itemsToGive = new() { CreateItem(1, type: Asset.EType.Emoticon) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
}
[TestMethod]
public void MultiGameMultiTypeBadReject() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, 9, 730, Asset.EType.Emoticon),
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void MultiGameMultiTypeNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void MultiGameSingleTypeBadReject() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, realAppID: 730),
CreateItem(4, realAppID: 730)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(3, realAppID: 730)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(4, realAppID: 730)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void MultiGameSingleTypeNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 2),
CreateItem(3, realAppID: 730)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(3, realAppID: 730)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(4, realAppID: 730)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameAbrynosWasWrongNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1),
CreateItem(2, 2),
CreateItem(3),
CreateItem(4),
CreateItem(5)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(2)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(3)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameDonationAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(3, type: Asset.EType.SteamGems)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameMultiTypeBadReject() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, 9, type: Asset.EType.Emoticon),
CreateItem(4, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(4, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(3, type: Asset.EType.Emoticon)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameMultiTypeNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(3, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(4, type: Asset.EType.Emoticon)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameQuantityBadReject() {
HashSet<Asset> inventory = new() {
CreateItem(1),
CreateItem(2),
CreateItem(3)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(2),
CreateItem(3)
};
HashSet<Asset> itemsToReceive = new() { CreateItem(4, 3) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameQuantityBadReject2() {
HashSet<Asset> inventory = new() {
CreateItem(1),
CreateItem(2, 2)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(2, 2)
};
HashSet<Asset> itemsToReceive = new() { CreateItem(3, 3) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameQuantityNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 2),
CreateItem(2)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(2)
};
HashSet<Asset> itemsToReceive = new() { CreateItem(3, 2) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeBadReject() {
HashSet<Asset> inventory = new() {
CreateItem(1),
CreateItem(2)
};
HashSet<Asset> itemsToGive = new() { CreateItem(1) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeBadWithOverpayingReject() {
HashSet<Asset> inventory = new() {
CreateItem(1, 2),
CreateItem(2, 2),
CreateItem(3, 2)
};
HashSet<Asset> itemsToGive = new() { CreateItem(2) };
HashSet<Asset> itemsToReceive = new() {
CreateItem(1),
CreateItem(3)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeBigDifferenceAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1),
CreateItem(2, 5),
CreateItem(3)
};
HashSet<Asset> itemsToGive = new() { CreateItem(2) };
HashSet<Asset> itemsToReceive = new() { CreateItem(3) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeBigDifferenceReject() {
HashSet<Asset> inventory = new() {
CreateItem(1),
CreateItem(2, 2),
CreateItem(3, 2),
CreateItem(4, 3),
CreateItem(5, 10)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(2),
CreateItem(5)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(3),
CreateItem(4)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeGoodAccept() {
HashSet<Asset> inventory = new() { CreateItem(1, 2) };
HashSet<Asset> itemsToGive = new() { CreateItem(1) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeNeutralAccept() {
HashSet<Asset> inventory = new() { CreateItem(1) };
HashSet<Asset> itemsToGive = new() { CreateItem(1) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeNeutralWithOverpayingAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 2),
CreateItem(2, 2)
};
HashSet<Asset> itemsToGive = new() { CreateItem(2) };
HashSet<Asset> itemsToReceive = new() {
CreateItem(1),
CreateItem(3)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
private static Asset CreateItem(ulong classID, uint amount = 1, uint realAppID = Asset.SteamAppID, Asset.EType type = Asset.EType.TradingCard, Asset.ERarity rarity = Asset.ERarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, realAppID: realAppID, type: type, rarity: rarity);
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
}
[TestMethod]
public void MismatchRealAppIDsIsNotFair() {
HashSet<Asset> itemsToGive = new() { CreateItem(1, realAppID: 570) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
}
[TestMethod]
public void MismatchTypesIsNotFair() {
HashSet<Asset> itemsToGive = new() { CreateItem(1, type: Asset.EType.Emoticon) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
}
[TestMethod]
public void MultiGameMultiTypeBadReject() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, 9, 730, Asset.EType.Emoticon),
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void MultiGameMultiTypeNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void MultiGameSingleTypeBadReject() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, realAppID: 730),
CreateItem(4, realAppID: 730)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(3, realAppID: 730)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(4, realAppID: 730)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void MultiGameSingleTypeNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 2),
CreateItem(3, realAppID: 730)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(3, realAppID: 730)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(4, realAppID: 730)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameAbrynosWasWrongNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1),
CreateItem(2, 2),
CreateItem(3),
CreateItem(4),
CreateItem(5)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(2)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(3)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameDonationAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(3, type: Asset.EType.SteamGems)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameMultiTypeBadReject() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, 9, type: Asset.EType.Emoticon),
CreateItem(4, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(4, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(3, type: Asset.EType.Emoticon)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameMultiTypeNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(3, type: Asset.EType.Emoticon)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(2),
CreateItem(4, type: Asset.EType.Emoticon)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameQuantityBadReject() {
HashSet<Asset> inventory = new() {
CreateItem(1),
CreateItem(2),
CreateItem(3)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(2),
CreateItem(3)
};
HashSet<Asset> itemsToReceive = new() { CreateItem(4, 3) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameQuantityBadReject2() {
HashSet<Asset> inventory = new() {
CreateItem(1),
CreateItem(2, 2)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(2, 2)
};
HashSet<Asset> itemsToReceive = new() { CreateItem(3, 3) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameQuantityNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 2),
CreateItem(2)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(1),
CreateItem(2)
};
HashSet<Asset> itemsToReceive = new() { CreateItem(3, 2) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeBadReject() {
HashSet<Asset> inventory = new() {
CreateItem(1),
CreateItem(2)
};
HashSet<Asset> itemsToGive = new() { CreateItem(1) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeBadWithOverpayingReject() {
HashSet<Asset> inventory = new() {
CreateItem(1, 2),
CreateItem(2, 2),
CreateItem(3, 2)
};
HashSet<Asset> itemsToGive = new() { CreateItem(2) };
HashSet<Asset> itemsToReceive = new() {
CreateItem(1),
CreateItem(3)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeBigDifferenceAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1),
CreateItem(2, 5),
CreateItem(3)
};
HashSet<Asset> itemsToGive = new() { CreateItem(2) };
HashSet<Asset> itemsToReceive = new() { CreateItem(3) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeBigDifferenceReject() {
HashSet<Asset> inventory = new() {
CreateItem(1),
CreateItem(2, 2),
CreateItem(3, 2),
CreateItem(4, 3),
CreateItem(5, 10)
};
HashSet<Asset> itemsToGive = new() {
CreateItem(2),
CreateItem(5)
};
HashSet<Asset> itemsToReceive = new() {
CreateItem(3),
CreateItem(4)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeGoodAccept() {
HashSet<Asset> inventory = new() { CreateItem(1, 2) };
HashSet<Asset> itemsToGive = new() { CreateItem(1) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeNeutralAccept() {
HashSet<Asset> inventory = new() { CreateItem(1) };
HashSet<Asset> itemsToGive = new() { CreateItem(1) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
[TestMethod]
public void SingleGameSingleTypeNeutralWithOverpayingAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 2),
CreateItem(2, 2)
};
HashSet<Asset> itemsToGive = new() { CreateItem(2) };
HashSet<Asset> itemsToReceive = new() {
CreateItem(1),
CreateItem(3)
};
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
}
private static Asset CreateItem(ulong classID, uint amount = 1, uint realAppID = Asset.SteamAppID, Asset.EType type = Asset.EType.TradingCard, Asset.ERarity rarity = Asset.ERarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, realAppID: realAppID, type: type, rarity: rarity);
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,34 +19,62 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static ArchiSteamFarm.Core.Utilities;
namespace ArchiSteamFarm.Tests {
[TestClass]
namespace ArchiSteamFarm.Tests;
[TestClass]
#pragma warning disable CA1724 // We don't care about the potential conflict, as ASF class name has a priority
public sealed class Utilities {
[TestMethod]
public void AdditionallyForbiddenWordsWeakenPassphrases() => Assert.IsTrue(TestPasswordStrength("10chars<!>asdf", new HashSet<string> { "chars<!>" }).IsWeak);
public sealed class Utilities {
[TestMethod]
public void AdditionallyForbiddenWordsWeakenPassphrases() => Assert.IsTrue(TestPasswordStrength("10chars<!>asdf", new HashSet<string> { "chars<!>" }).IsWeak);
[TestMethod]
public void ContextSpecificWordsWeakenPassphrases() => Assert.IsTrue(TestPasswordStrength("archisteamfarmpassword").IsWeak);
[TestMethod]
public void ContextSpecificWordsWeakenPassphrases() => Assert.IsTrue(TestPasswordStrength("archisteamfarmpassword").IsWeak);
[TestMethod]
public void LongPassphraseIsNotWeak() => Assert.IsFalse(TestPasswordStrength("10chars<!>asdf").IsWeak);
[TestMethod]
public void EasyPasswordsHaveMeaningfulReason() {
(bool isWeak, string? reason) = TestPasswordStrength("CorrectHorse");
[TestMethod]
public void RepetitiveCharactersWeakenPassphrases() => Assert.IsTrue(TestPasswordStrength("testaaaatest").IsWeak);
[TestMethod]
public void SequentialCharactersWeakenPassphrases() => Assert.IsTrue(TestPasswordStrength("testabcdtest").IsWeak);
[TestMethod]
public void SequentialDescendingCharactersWeakenPassphrases() => Assert.IsTrue(TestPasswordStrength("testdcbatest").IsWeak);
[TestMethod]
public void ShortPassphraseIsWeak() => Assert.IsTrue(TestPasswordStrength("four").IsWeak);
Assert.IsTrue(isWeak);
Assert.IsTrue(reason?.Contains("Capitalization doesn't help very much", StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
public void LongPassphraseIsNotWeak() => Assert.IsFalse(TestPasswordStrength("10chars<!>asdf").IsWeak);
[TestMethod]
public void MemePasswordIsNotWeak() => Assert.IsFalse(TestPasswordStrength("correcthorsebatterystaple").IsWeak);
[TestMethod]
public void RepeatedPasswordsHaveMeaningfulReason() {
(bool isWeak, string? reason) = TestPasswordStrength("abcabcabc");
Assert.IsTrue(isWeak);
Assert.IsTrue(reason?.Contains("Avoid repeated words and characters", StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
public void RepetitiveCharactersWeakenPassphrases() => Assert.IsTrue(TestPasswordStrength("testaaaatest").IsWeak);
[TestMethod]
public void SequentialCharactersWeakenPassphrases() => Assert.IsTrue(TestPasswordStrength("testabcdtest").IsWeak);
[TestMethod]
public void SequentialDescendingCharactersWeakenPassphrases() => Assert.IsTrue(TestPasswordStrength("testdcbatest").IsWeak);
[TestMethod]
public void ShortPassphraseIsWeak() => Assert.IsTrue(TestPasswordStrength("four").IsWeak);
[TestMethod]
public void StraightRowsPasswordsHaveMeaningfulReason() {
(bool isWeak, string? reason) = TestPasswordStrength("`1234567890-=");
Assert.IsTrue(isWeak);
Assert.IsTrue(reason?.Contains("Straight rows of keys are easy to guess", StringComparison.OrdinalIgnoreCase));
}
#pragma warning restore CA1724 // We don't care about the potential conflict, as ASF class name has a priority
}
#pragma warning restore CA1724 // We don't care about the potential conflict, as ASF class name has a priority

File diff suppressed because one or more lines are too long

View File

@@ -25,7 +25,7 @@
<PackageReference Include="zxcvbn-core" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'net48' AND ('$(TargetGeneric)' == 'true' OR '$(TargetWindows)' == 'true')">
<ItemGroup Condition="'$(TargetFramework)' != 'net48'">
<PackageReference Include="System.Security.Cryptography.ProtectedData" />
</ItemGroup>
@@ -33,7 +33,6 @@
<PackageReference Include="Microsoft.AspNetCore.Cors" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" />
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" />
<PackageReference Include="Microsoft.AspNetCore.Localization" />
<PackageReference Include="Microsoft.AspNetCore.ResponseCaching" />
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" />
@@ -64,7 +63,7 @@
</ItemGroup>
<ItemGroup>
<Content Include="..\LICENSE-2.0.txt">
<Content Include="..\LICENSE.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
@@ -79,4 +78,20 @@
<Link>www\%(RecursiveDir)%(Filename)%(Extension)</Link>
</Content>
</ItemGroup>
<ItemGroup Condition="Exists($([System.IO.Path]::Combine('overlay', 'variant-base', $(ASFVariant.Split('-')[0]))))">
<Content Include="overlay/variant-base/$(ASFVariant.Split('-')[0])/**/*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
</Content>
</ItemGroup>
<ItemGroup Condition="Exists($([System.IO.Path]::Combine('overlay', 'variant-specific', $(ASFVariant))))">
<Content Include="overlay/variant-specific/$(ASFVariant)/**/*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
</Content>
</ItemGroup>
</Project>

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,10 +24,10 @@ using System.Runtime.CompilerServices;
[assembly: CLSCompliant(false)]
#if DEBUG
[assembly: InternalsVisibleTo("ArchiSteamFarm.Tests")]
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper")]
#else
#if ASF_SIGNED_BUILD
[assembly: InternalsVisibleTo("ArchiSteamFarm.Tests, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")]
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")]
#else
[assembly: InternalsVisibleTo("ArchiSteamFarm.Tests")]
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper")]
#endif

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,30 +23,28 @@ using System;
using System.Collections;
using System.Collections.Generic;
namespace ArchiSteamFarm.Collections {
internal sealed class ConcurrentEnumerator<T> : IEnumerator<T> {
public T Current => Enumerator.Current;
namespace ArchiSteamFarm.Collections;
private readonly IEnumerator<T> Enumerator;
private readonly IDisposable LockObject;
internal sealed class ConcurrentEnumerator<T> : IEnumerator<T> {
public T Current => Enumerator.Current;
object? IEnumerator.Current => Current;
private readonly IEnumerator<T> Enumerator;
private readonly IDisposable LockObject;
internal ConcurrentEnumerator(IReadOnlyCollection<T> collection, IDisposable lockObject) {
if (collection == null) {
throw new ArgumentNullException(nameof(collection));
}
object? IEnumerator.Current => Current;
LockObject = lockObject ?? throw new ArgumentNullException(nameof(lockObject));
Enumerator = collection.GetEnumerator();
}
internal ConcurrentEnumerator(IReadOnlyCollection<T> collection, IDisposable lockObject) {
ArgumentNullException.ThrowIfNull(collection);
public void Dispose() {
Enumerator.Dispose();
LockObject.Dispose();
}
public bool MoveNext() => Enumerator.MoveNext();
public void Reset() => Enumerator.Reset();
LockObject = lockObject ?? throw new ArgumentNullException(nameof(lockObject));
Enumerator = collection.GetEnumerator();
}
public void Dispose() {
Enumerator.Dispose();
LockObject.Dispose();
}
public bool MoveNext() => Enumerator.MoveNext();
public void Reset() => Enumerator.Reset();
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,180 +26,174 @@ using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Collections {
public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where T : notnull {
public event EventHandler? OnModified;
namespace ArchiSteamFarm.Collections;
public int Count => BackingCollection.Count;
public bool IsReadOnly => false;
public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where T : notnull {
public event EventHandler? OnModified;
private readonly ConcurrentDictionary<T, bool> BackingCollection;
public int Count => BackingCollection.Count;
public bool IsReadOnly => false;
public ConcurrentHashSet() => BackingCollection = new ConcurrentDictionary<T, bool>();
private readonly ConcurrentDictionary<T, bool> BackingCollection;
public ConcurrentHashSet(IEqualityComparer<T> comparer) {
if (comparer == null) {
throw new ArgumentNullException(nameof(comparer));
}
public ConcurrentHashSet() => BackingCollection = new ConcurrentDictionary<T, bool>();
BackingCollection = new ConcurrentDictionary<T, bool>(comparer);
public ConcurrentHashSet(IEqualityComparer<T> comparer) {
ArgumentNullException.ThrowIfNull(comparer);
BackingCollection = new ConcurrentDictionary<T, bool>(comparer);
}
public bool Add(T item) {
if (!BackingCollection.TryAdd(item, true)) {
return false;
}
public bool Add(T item) {
if (!BackingCollection.TryAdd(item, true)) {
return false;
}
OnModified?.Invoke(this, EventArgs.Empty);
OnModified?.Invoke(this, EventArgs.Empty);
return true;
}
return true;
public void Clear() {
if (BackingCollection.IsEmpty) {
return;
}
public void Clear() {
if (BackingCollection.IsEmpty) {
return;
}
BackingCollection.Clear();
BackingCollection.Clear();
OnModified?.Invoke(this, EventArgs.Empty);
}
OnModified?.Invoke(this, EventArgs.Empty);
}
public bool Contains(T item) => BackingCollection.ContainsKey(item);
public bool Contains(T item) => BackingCollection.ContainsKey(item);
public void CopyTo(T[] array, int arrayIndex) => BackingCollection.Keys.CopyTo(array, arrayIndex);
public void CopyTo(T[] array, int arrayIndex) => BackingCollection.Keys.CopyTo(array, arrayIndex);
public void ExceptWith(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
public void ExceptWith(IEnumerable<T> other) {
if (other == null) {
throw new ArgumentNullException(nameof(other));
}
foreach (T item in other) {
Remove(item);
}
}
public IEnumerator<T> GetEnumerator() => BackingCollection.Keys.GetEnumerator();
public void IntersectWith(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
foreach (T item in this.Where(item => !otherSet.Contains(item))) {
Remove(item);
}
}
public bool IsProperSubsetOf(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return (otherSet.Count > Count) && IsSubsetOf(otherSet);
}
public bool IsProperSupersetOf(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return (otherSet.Count < Count) && IsSupersetOf(otherSet);
}
public bool IsSubsetOf(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return this.All(otherSet.Contains);
}
public bool IsSupersetOf(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return otherSet.All(Contains);
}
public bool Overlaps(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return otherSet.Any(Contains);
}
public bool Remove(T item) {
if (!BackingCollection.TryRemove(item, out _)) {
return false;
}
OnModified?.Invoke(this, EventArgs.Empty);
return true;
}
public bool SetEquals(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return (otherSet.Count == Count) && otherSet.All(Contains);
}
public void SymmetricExceptWith(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
HashSet<T> removed = new();
foreach (T item in otherSet.Where(Contains)) {
removed.Add(item);
Remove(item);
}
foreach (T item in otherSet.Where(item => !removed.Contains(item))) {
Add(item);
}
}
public void UnionWith(IEnumerable<T> other) {
if (other == null) {
throw new ArgumentNullException(nameof(other));
}
foreach (T otherElement in other) {
Add(otherElement);
}
}
void ICollection<T>.Add(T item) => Add(item);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
[PublicAPI]
public bool AddRange(IEnumerable<T> items) {
bool result = false;
foreach (T _ in items.Where(Add)) {
result = true;
}
return result;
}
[PublicAPI]
public bool RemoveRange(IEnumerable<T> items) {
bool result = false;
foreach (T _ in items.Where(Remove)) {
result = true;
}
return result;
}
[PublicAPI]
public bool ReplaceIfNeededWith(IReadOnlyCollection<T> other) {
if (SetEquals(other)) {
return false;
}
ReplaceWith(other);
return true;
}
[PublicAPI]
public void ReplaceWith(IEnumerable<T> other) {
Clear();
UnionWith(other);
foreach (T item in other) {
Remove(item);
}
}
public IEnumerator<T> GetEnumerator() => BackingCollection.Keys.GetEnumerator();
public void IntersectWith(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
foreach (T item in this.Where(item => !otherSet.Contains(item))) {
Remove(item);
}
}
public bool IsProperSubsetOf(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return (otherSet.Count > Count) && IsSubsetOf(otherSet);
}
public bool IsProperSupersetOf(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return (otherSet.Count < Count) && IsSupersetOf(otherSet);
}
public bool IsSubsetOf(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return this.All(otherSet.Contains);
}
public bool IsSupersetOf(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return otherSet.All(Contains);
}
public bool Overlaps(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return otherSet.Any(Contains);
}
public bool Remove(T item) {
if (!BackingCollection.TryRemove(item, out _)) {
return false;
}
OnModified?.Invoke(this, EventArgs.Empty);
return true;
}
public bool SetEquals(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return (otherSet.Count == Count) && otherSet.All(Contains);
}
public void SymmetricExceptWith(IEnumerable<T> other) {
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
HashSet<T> removed = new();
foreach (T item in otherSet.Where(Contains)) {
removed.Add(item);
Remove(item);
}
foreach (T item in otherSet.Where(item => !removed.Contains(item))) {
Add(item);
}
}
public void UnionWith(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
foreach (T otherElement in other) {
Add(otherElement);
}
}
void ICollection<T>.Add(T item) => Add(item);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
[PublicAPI]
public bool AddRange(IEnumerable<T> items) {
bool result = false;
foreach (T _ in items.Where(Add)) {
result = true;
}
return result;
}
[PublicAPI]
public bool RemoveRange(IEnumerable<T> items) {
bool result = false;
foreach (T _ in items.Where(Remove)) {
result = true;
}
return result;
}
[PublicAPI]
public bool ReplaceIfNeededWith(IReadOnlyCollection<T> other) {
if (SetEquals(other)) {
return false;
}
ReplaceWith(other);
return true;
}
[PublicAPI]
public void ReplaceWith(IEnumerable<T> other) {
Clear();
UnionWith(other);
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,95 +23,95 @@ using System.Collections;
using System.Collections.Generic;
using Nito.AsyncEx;
namespace ArchiSteamFarm.Collections {
internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
public bool IsReadOnly => false;
namespace ArchiSteamFarm.Collections;
internal int Count {
get {
using (Lock.ReaderLock()) {
return BackingCollection.Count;
}
}
}
internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
public bool IsReadOnly => false;
private readonly List<T> BackingCollection = new();
private readonly AsyncReaderWriterLock Lock = new();
int ICollection<T>.Count => Count;
int IReadOnlyCollection<T>.Count => Count;
public T this[int index] {
get {
using (Lock.ReaderLock()) {
return BackingCollection[index];
}
}
set {
using (Lock.WriterLock()) {
BackingCollection[index] = value;
}
}
}
public void Add(T item) {
using (Lock.WriterLock()) {
BackingCollection.Add(item);
}
}
public void Clear() {
using (Lock.WriterLock()) {
BackingCollection.Clear();
}
}
public bool Contains(T item) {
internal int Count {
get {
using (Lock.ReaderLock()) {
return BackingCollection.Contains(item);
}
}
public void CopyTo(T[] array, int arrayIndex) {
using (Lock.ReaderLock()) {
BackingCollection.CopyTo(array, arrayIndex);
}
}
public IEnumerator<T> GetEnumerator() => new ConcurrentEnumerator<T>(BackingCollection, Lock.ReaderLock());
public int IndexOf(T item) {
using (Lock.ReaderLock()) {
return BackingCollection.IndexOf(item);
}
}
public void Insert(int index, T item) {
using (Lock.WriterLock()) {
BackingCollection.Insert(index, item);
}
}
public bool Remove(T item) {
using (Lock.WriterLock()) {
return BackingCollection.Remove(item);
}
}
public void RemoveAt(int index) {
using (Lock.WriterLock()) {
BackingCollection.RemoveAt(index);
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
internal void ReplaceWith(IEnumerable<T> collection) {
using (Lock.WriterLock()) {
BackingCollection.Clear();
BackingCollection.AddRange(collection);
return BackingCollection.Count;
}
}
}
private readonly List<T> BackingCollection = new();
private readonly AsyncReaderWriterLock Lock = new();
int ICollection<T>.Count => Count;
int IReadOnlyCollection<T>.Count => Count;
public T this[int index] {
get {
using (Lock.ReaderLock()) {
return BackingCollection[index];
}
}
set {
using (Lock.WriterLock()) {
BackingCollection[index] = value;
}
}
}
public void Add(T item) {
using (Lock.WriterLock()) {
BackingCollection.Add(item);
}
}
public void Clear() {
using (Lock.WriterLock()) {
BackingCollection.Clear();
}
}
public bool Contains(T item) {
using (Lock.ReaderLock()) {
return BackingCollection.Contains(item);
}
}
public void CopyTo(T[] array, int arrayIndex) {
using (Lock.ReaderLock()) {
BackingCollection.CopyTo(array, arrayIndex);
}
}
public IEnumerator<T> GetEnumerator() => new ConcurrentEnumerator<T>(BackingCollection, Lock.ReaderLock());
public int IndexOf(T item) {
using (Lock.ReaderLock()) {
return BackingCollection.IndexOf(item);
}
}
public void Insert(int index, T item) {
using (Lock.WriterLock()) {
BackingCollection.Insert(index, item);
}
}
public bool Remove(T item) {
using (Lock.WriterLock()) {
return BackingCollection.Remove(item);
}
}
public void RemoveAt(int index) {
using (Lock.WriterLock()) {
BackingCollection.RemoveAt(index);
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
internal void ReplaceWith(IEnumerable<T> collection) {
using (Lock.WriterLock()) {
BackingCollection.Clear();
BackingCollection.AddRange(collection);
}
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,53 +25,53 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using ArchiSteamFarm.Core;
namespace ArchiSteamFarm.Collections {
internal sealed class FixedSizeConcurrentQueue<T> : IEnumerable<T> {
private readonly ConcurrentQueue<T> BackingQueue = new();
namespace ArchiSteamFarm.Collections;
internal byte MaxCount {
get => BackingMaxCount;
internal sealed class FixedSizeConcurrentQueue<T> : IEnumerable<T> {
private readonly ConcurrentQueue<T> BackingQueue = new();
set {
if (value == 0) {
ASF.ArchiLogger.LogNullError(nameof(value));
internal byte MaxCount {
get => BackingMaxCount;
return;
}
set {
if (value == 0) {
ASF.ArchiLogger.LogNullError(value);
BackingMaxCount = value;
Resize();
}
}
private byte BackingMaxCount;
internal FixedSizeConcurrentQueue(byte maxCount) {
if (maxCount == 0) {
throw new ArgumentNullException(nameof(maxCount));
}
MaxCount = maxCount;
}
public IEnumerator<T> GetEnumerator() => BackingQueue.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
internal void Enqueue(T obj) {
BackingQueue.Enqueue(obj);
Resize();
}
private void Resize() {
if (BackingQueue.Count <= MaxCount) {
return;
}
lock (BackingQueue) {
while ((BackingQueue.Count > MaxCount) && BackingQueue.TryDequeue(out _)) { }
}
BackingMaxCount = value;
Resize();
}
}
private byte BackingMaxCount;
internal FixedSizeConcurrentQueue(byte maxCount) {
if (maxCount == 0) {
throw new ArgumentNullException(nameof(maxCount));
}
MaxCount = maxCount;
}
public IEnumerator<T> GetEnumerator() => BackingQueue.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
internal void Enqueue(T obj) {
BackingQueue.Enqueue(obj);
Resize();
}
private void Resize() {
if (BackingQueue.Count <= MaxCount) {
return;
}
lock (BackingQueue) {
while ((BackingQueue.Count > MaxCount) && BackingQueue.TryDequeue(out _)) { }
}
}
}

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);
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,55 +23,55 @@ using System;
using System.Globalization;
using System.Threading;
namespace ArchiSteamFarm.Core {
internal static class AprilFools {
private static readonly object LockObject = new();
namespace ArchiSteamFarm.Core;
// We don't care about CurrentCulture global config property, because April Fools are never initialized in this case
private static readonly CultureInfo OriginalCulture = CultureInfo.CurrentCulture;
internal static class AprilFools {
private static readonly object LockObject = new();
private static readonly Timer Timer = new(Init);
// We don't care about CurrentCulture global config property, because April Fools are never initialized in this case
private static readonly CultureInfo OriginalCulture = CultureInfo.CurrentCulture;
internal static void Init(object? state = null) {
DateTime now = DateTime.Now;
private static readonly Timer Timer = new(Init);
if ((now.Month == 4) && (now.Day == 1)) {
try {
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CreateSpecificCulture(SharedInfo.LolcatCultureName);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericDebuggingException(e);
return;
}
TimeSpan aprilFoolsEnd = TimeSpan.FromDays(1) - now.TimeOfDay;
lock (LockObject) {
Timer.Change(aprilFoolsEnd + TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan);
}
return;
}
internal static void Init(object? state = null) {
DateTime now = DateTime.Now;
if ((now.Month == 4) && (now.Day == 1)) {
try {
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = OriginalCulture;
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CreateSpecificCulture(SharedInfo.LolcatCultureName);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericDebuggingException(e);
return;
}
// Since we already verified that it's not April Fools right now, either we're in months 1-3 before 1st April this year, or 4-12 already after the 1st April
DateTime nextAprilFools = new(now.Month >= 4 ? now.Year + 1 : now.Year, 4, 1, 0, 0, 0, DateTimeKind.Local);
TimeSpan aprilFoolsStart = nextAprilFools - now;
// Timer can accept only dueTimes up to 2^32 - 2
uint dueTime = (uint) Math.Min(uint.MaxValue - 1, (ulong) aprilFoolsStart.TotalMilliseconds + 100);
TimeSpan aprilFoolsEnd = TimeSpan.FromDays(1) - now.TimeOfDay;
lock (LockObject) {
Timer.Change(dueTime, Timeout.Infinite);
Timer.Change(aprilFoolsEnd + TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan);
}
return;
}
try {
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = OriginalCulture;
} catch (Exception e) {
ASF.ArchiLogger.LogGenericDebuggingException(e);
return;
}
// Since we already verified that it's not April Fools right now, either we're in months 1-3 before 1st April this year, or 4-12 already after the 1st April
DateTime nextAprilFools = new(now.Month >= 4 ? now.Year + 1 : now.Year, 4, 1, 0, 0, 0, DateTimeKind.Local);
TimeSpan aprilFoolsStart = nextAprilFools - now;
// Timer can accept only dueTimes up to 2^32 - 2
uint dueTime = (uint) Math.Min(uint.MaxValue - 1, (ulong) aprilFoolsStart.TotalMilliseconds + 100);
lock (LockObject) {
Timer.Change(dueTime, Timeout.Infinite);
}
}
}

View File

@@ -0,0 +1,269 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Storage;
using ArchiSteamFarm.Storage;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.Web.Responses;
using Newtonsoft.Json;
namespace ArchiSteamFarm.Core;
internal static class ArchiNet {
private static Uri URL => new("https://asf.JustArchi.net");
internal static async Task<HttpStatusCode?> AnnounceForListing(Bot bot, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, string tradeToken, string? nickname = null, string? avatarHash = null) {
ArgumentNullException.ThrowIfNull(bot);
if ((inventory == null) || (inventory.Count == 0)) {
throw new ArgumentNullException(nameof(inventory));
}
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
}
if (string.IsNullOrEmpty(tradeToken)) {
throw new ArgumentNullException(nameof(tradeToken));
}
if (tradeToken.Length != BotConfig.SteamTradeTokenLength) {
throw new ArgumentOutOfRangeException(nameof(tradeToken));
}
Uri request = new(URL, "/Api/Announce");
Dictionary<string, string> data = new(10, StringComparer.Ordinal) {
{ "AvatarHash", avatarHash ?? "" },
{ "GamesCount", inventory.Select(static item => item.RealAppID).Distinct().Count().ToString(CultureInfo.InvariantCulture) },
{ "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") },
{ "ItemsCount", inventory.Count.ToString(CultureInfo.InvariantCulture) },
{ "MatchableTypes", JsonConvert.SerializeObject(acceptedMatchableTypes) },
{ "MatchEverything", bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) ? "1" : "0" },
{ "MaxTradeHoldDuration", (ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration).ToString(CultureInfo.InvariantCulture) },
{ "Nickname", nickname ?? "" },
{ "SteamID", bot.SteamID.ToString(CultureInfo.InvariantCulture) },
{ "TradeToken", tradeToken }
};
BasicResponse? response = await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
return response?.StatusCode;
}
internal static async Task<string?> FetchBuildChecksum(Version version, string variant) {
ArgumentNullException.ThrowIfNull(version);
if (string.IsNullOrEmpty(variant)) {
throw new ArgumentNullException(nameof(variant));
}
if (ASF.WebBrowser == null) {
throw new InvalidOperationException(nameof(ASF.WebBrowser));
}
Uri request = new(URL, $"/Api/Checksum/{version}/{variant}");
ObjectResponse<ChecksumResponse>? response = await ASF.WebBrowser.UrlGetToJsonObject<ChecksumResponse>(request).ConfigureAwait(false);
if (response?.Content == null) {
return null;
}
return response.Content.Checksum ?? "";
}
internal static async Task<ImmutableHashSet<ListedUser>?> GetListedUsers(Bot bot) {
ArgumentNullException.ThrowIfNull(bot);
Uri request = new(URL, "/Api/Bots");
ObjectResponse<ImmutableHashSet<ListedUser>>? response = await bot.ArchiWebHandler.WebBrowser.UrlGetToJsonObject<ImmutableHashSet<ListedUser>>(request).ConfigureAwait(false);
return response?.Content;
}
internal static async Task<HttpStatusCode?> HeartBeatForListing(Bot bot) {
ArgumentNullException.ThrowIfNull(bot);
Uri request = new(URL, "/Api/HeartBeat");
Dictionary<string, string> data = new(2, StringComparer.Ordinal) {
{ "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") },
{ "SteamID", bot.SteamID.ToString(CultureInfo.InvariantCulture) }
};
BasicResponse? response = await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
return response?.StatusCode;
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class ListedUser {
[JsonProperty("items_count", Required = Required.Always)]
internal readonly ushort ItemsCount;
internal readonly HashSet<Asset.EType> MatchableTypes = new();
[JsonProperty("max_trade_hold_duration", Required = Required.Always)]
internal readonly byte MaxTradeHoldDuration;
[JsonProperty("steam_id", Required = Required.Always)]
internal readonly ulong SteamID;
[JsonProperty("trade_token", Required = Required.Always)]
internal readonly string TradeToken = "";
internal float Score => GamesCount / (float) ItemsCount;
#pragma warning disable CS0649 // False positive, it's a field set during json deserialization
[JsonProperty("games_count", Required = Required.Always)]
private readonly ushort GamesCount;
#pragma warning restore CS0649 // False positive, it's a field set during json deserialization
internal bool MatchEverything { get; private set; }
[JsonProperty("matchable_backgrounds", Required = Required.Always)]
private byte MatchableBackgroundsNumber {
set {
switch (value) {
case 0:
MatchableTypes.Remove(Asset.EType.ProfileBackground);
break;
case 1:
MatchableTypes.Add(Asset.EType.ProfileBackground);
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonProperty("matchable_cards", Required = Required.Always)]
private byte MatchableCardsNumber {
set {
switch (value) {
case 0:
MatchableTypes.Remove(Asset.EType.TradingCard);
break;
case 1:
MatchableTypes.Add(Asset.EType.TradingCard);
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonProperty("matchable_emoticons", Required = Required.Always)]
private byte MatchableEmoticonsNumber {
set {
switch (value) {
case 0:
MatchableTypes.Remove(Asset.EType.Emoticon);
break;
case 1:
MatchableTypes.Add(Asset.EType.Emoticon);
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonProperty("matchable_foil_cards", Required = Required.Always)]
private byte MatchableFoilCardsNumber {
set {
switch (value) {
case 0:
MatchableTypes.Remove(Asset.EType.FoilTradingCard);
break;
case 1:
MatchableTypes.Add(Asset.EType.FoilTradingCard);
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonProperty("match_everything", Required = Required.Always)]
private byte MatchEverythingNumber {
set {
switch (value) {
case 0:
MatchEverything = false;
break;
case 1:
MatchEverything = true;
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonConstructor]
private ListedUser() { }
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
private sealed class ChecksumResponse {
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty("checksum", Required = Required.AllowNull)]
internal readonly string? Checksum;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonConstructor]
private ChecksumResponse() { }
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,28 +20,29 @@
// limitations under the License.
using System;
using ArchiSteamFarm.Storage;
using SteamKit2;
namespace ArchiSteamFarm.Core {
internal static class Debugging {
namespace ArchiSteamFarm.Core;
internal static class Debugging {
#if DEBUG
internal static bool IsDebugBuild => true;
internal static bool IsDebugBuild => true;
#else
internal static bool IsDebugBuild => false;
internal static bool IsDebugBuild => false;
#endif
internal static bool IsDebugConfigured => ASF.GlobalConfig?.Debug ?? throw new InvalidOperationException(nameof(ASF.GlobalConfig));
internal static bool IsDebugConfigured => ASF.GlobalConfig?.Debug ?? GlobalConfig.DefaultDebug;
internal static bool IsUserDebugging => IsDebugBuild || IsDebugConfigured;
internal static bool IsUserDebugging => IsDebugBuild || IsDebugConfigured;
internal sealed class DebugListener : IDebugListener {
public void WriteLine(string category, string msg) {
if (string.IsNullOrEmpty(category) && string.IsNullOrEmpty(msg)) {
throw new InvalidOperationException($"{nameof(category)} && {nameof(msg)}");
}
ASF.ArchiLogger.LogGenericDebug($"{category} | {msg}");
internal sealed class DebugListener : IDebugListener {
public void WriteLine(string category, string msg) {
if (string.IsNullOrEmpty(category) && string.IsNullOrEmpty(msg)) {
throw new InvalidOperationException($"{nameof(category)} && {nameof(msg)}");
}
ASF.ArchiLogger.LogGenericDebug($"{category} | {msg}");
}
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,23 +24,23 @@ using System.Threading.Tasks;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
namespace ArchiSteamFarm.Core {
internal static class Events {
internal static async Task OnBotShutdown() {
if (Program.ProcessRequired || ((Bot.Bots != null) && Bot.Bots.Values.Any(static bot => bot.KeepRunning))) {
return;
}
namespace ArchiSteamFarm.Core;
ASF.ArchiLogger.LogGenericInfo(Strings.NoBotsAreRunning);
// We give user extra 5 seconds for eventual config changes
await Task.Delay(5000).ConfigureAwait(false);
if (Program.ProcessRequired || ((Bot.Bots != null) && Bot.Bots.Values.Any(static bot => bot.KeepRunning))) {
return;
}
await Program.Exit().ConfigureAwait(false);
internal static class Events {
internal static async Task OnBotShutdown() {
if (Program.ProcessRequired || ((Bot.Bots != null) && Bot.Bots.Values.Any(static bot => bot.KeepRunning))) {
return;
}
ASF.ArchiLogger.LogGenericInfo(Strings.NoBotsAreRunning);
// We give user extra 5 seconds for eventual config changes
await Task.Delay(5000).ConfigureAwait(false);
if (Program.ProcessRequired || ((Bot.Bots != null) && Bot.Bots.Values.Any(static bot => bot.KeepRunning))) {
return;
}
await Program.Exit().ConfigureAwait(false);
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,6 +20,7 @@
// limitations under the License.
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@@ -35,223 +36,215 @@ using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Storage;
using ArchiSteamFarm.Web;
namespace ArchiSteamFarm.Core {
internal static class OS {
// We need to keep this one assigned and not calculated on-demand
internal static readonly string ProcessFileName = Process.GetCurrentProcess().MainModule?.FileName ?? throw new InvalidOperationException(nameof(ProcessFileName));
namespace ArchiSteamFarm.Core;
internal static DateTime ProcessStartTime {
internal static class OS {
// We need to keep this one assigned and not calculated on-demand
internal static readonly string ProcessFileName = Environment.ProcessPath ?? throw new InvalidOperationException(nameof(ProcessFileName));
internal static DateTime ProcessStartTime {
#if NETFRAMEWORK
get => RuntimeMadness.ProcessStartTime.ToUniversalTime();
get => RuntimeMadness.ProcessStartTime.ToUniversalTime();
#else
get {
using Process process = Process.GetCurrentProcess();
get {
using Process process = Process.GetCurrentProcess();
return process.StartTime.ToUniversalTime();
}
#endif
return process.StartTime.ToUniversalTime();
}
#endif
}
internal static string Version {
get {
if (!string.IsNullOrEmpty(BackingVersion)) {
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
return BackingVersion!;
}
internal static string Version {
get {
if (!string.IsNullOrEmpty(BackingVersion)) {
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
return BackingVersion!;
}
string framework = RuntimeInformation.FrameworkDescription.Trim();
string framework = RuntimeInformation.FrameworkDescription.Trim();
if (framework.Length == 0) {
framework = "Unknown Framework";
}
if (framework.Length == 0) {
framework = "Unknown Framework";
}
#if NETFRAMEWORK
string runtime = RuntimeInformation.OSArchitecture.ToString();
string runtime = RuntimeInformation.OSArchitecture.ToString();
#else
string runtime = RuntimeInformation.RuntimeIdentifier.Trim();
string runtime = RuntimeInformation.RuntimeIdentifier.Trim();
if (runtime.Length == 0) {
runtime = "Unknown Runtime";
}
#endif
string description = RuntimeInformation.OSDescription.Trim();
if (description.Length == 0) {
description = "Unknown OS";
}
BackingVersion = $"{framework}; {runtime}; {description}";
return BackingVersion;
}
}
private static string? BackingVersion;
private static Mutex? SingleInstance;
#if TARGET_GENERIC || TARGET_WINDOWS
internal static void CoreInit(bool systemRequired) {
if (OperatingSystem.IsWindows()) {
if (systemRequired) {
WindowsKeepSystemActive();
}
if (!Console.IsOutputRedirected) {
// Normally we should use UTF-8 console encoding as it's the most correct one for our case, and we already use it on other OSes such as Linux
// However, older Windows versions, mainly 7/8.1 can't into UTF-8 without appropriate console font, and expecting from users to change it manually is unwanted
// As irrational as it can sound, those versions actually can work with unicode encoding instead, as they magically map it into proper chars despite of incorrect font
// See https://github.com/JustArchiNET/ArchiSteamFarm/issues/1289 for more details
Console.OutputEncoding = OperatingSystem.IsWindowsVersionAtLeast(10) ? Encoding.UTF8 : Encoding.Unicode;
// Quick edit mode will freeze when user start selecting something on the console until the selection is cancelled
// Users are very often doing it accidentally without any real purpose, and we want to avoid this common issue which causes the whole process to hang
// See http://stackoverflow.com/questions/30418886/how-and-why-does-quickedit-mode-in-command-prompt-freeze-applications for more details
WindowsDisableQuickEditMode();
}
}
}
#else
internal static void CoreInit(bool _) { }
#endif
internal static string GetOsResourceName(string objectName) {
if (string.IsNullOrEmpty(objectName)) {
throw new ArgumentNullException(nameof(objectName));
}
return $"{SharedInfo.AssemblyName}-{objectName}";
}
internal static void Init(GlobalConfig.EOptimizationMode optimizationMode) {
if (!Enum.IsDefined(typeof(GlobalConfig.EOptimizationMode), optimizationMode)) {
throw new ArgumentNullException(nameof(optimizationMode));
}
switch (optimizationMode) {
case GlobalConfig.EOptimizationMode.MaxPerformance:
// No specific tuning required for now, ASF is optimized for max performance by default
break;
case GlobalConfig.EOptimizationMode.MinMemoryUsage:
// We can disable regex cache which will slightly lower memory usage (for a huge performance hit)
Regex.CacheSize = 0;
break;
default:
throw new ArgumentOutOfRangeException(nameof(optimizationMode));
}
}
internal static bool IsRunningAsRoot() {
#if TARGET_GENERIC || TARGET_WINDOWS
if (OperatingSystem.IsWindows()) {
using WindowsIdentity identity = WindowsIdentity.GetCurrent();
return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
if (runtime.Length == 0) {
runtime = "Unknown Runtime";
}
#endif
#if TARGET_GENERIC || !TARGET_WINDOWS
if (OperatingSystem.IsFreeBSD() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) {
return NativeMethods.GetEUID() == 0;
}
#endif
string description = RuntimeInformation.OSDescription.Trim();
// We can't determine whether user is running as root or not, so fallback to that not happening
if (description.Length == 0) {
description = "Unknown OS";
}
BackingVersion = $"{framework}; {runtime}; {description}";
return BackingVersion;
}
}
private static string? BackingVersion;
private static Mutex? SingleInstance;
internal static void CoreInit(bool systemRequired) {
if (OperatingSystem.IsWindows()) {
if (systemRequired) {
WindowsKeepSystemActive();
}
if (!Console.IsOutputRedirected) {
// Normally we should use UTF-8 console encoding as it's the most correct one for our case, and we already use it on other OSes such as Linux
// However, older Windows versions, mainly 7/8.1 can't into UTF-8 without appropriate console font, and expecting from users to change it manually is unwanted
// As irrational as it can sound, those versions actually can work with unicode encoding instead, as they magically map it into proper chars despite of incorrect font
// See https://github.com/JustArchiNET/ArchiSteamFarm/issues/1289 for more details
Console.OutputEncoding = OperatingSystem.IsWindowsVersionAtLeast(10) ? Encoding.UTF8 : Encoding.Unicode;
// Quick edit mode will freeze when user start selecting something on the console until the selection is cancelled
// Users are very often doing it accidentally without any real purpose, and we want to avoid this common issue which causes the whole process to hang
// See http://stackoverflow.com/questions/30418886/how-and-why-does-quickedit-mode-in-command-prompt-freeze-applications for more details
WindowsDisableQuickEditMode();
}
}
}
internal static string GetOsResourceName(string objectName) {
if (string.IsNullOrEmpty(objectName)) {
throw new ArgumentNullException(nameof(objectName));
}
return $"{SharedInfo.AssemblyName}-{objectName}";
}
internal static void Init(GlobalConfig.EOptimizationMode optimizationMode) {
if (!Enum.IsDefined(optimizationMode)) {
throw new InvalidEnumArgumentException(nameof(optimizationMode), (int) optimizationMode, typeof(GlobalConfig.EOptimizationMode));
}
switch (optimizationMode) {
case GlobalConfig.EOptimizationMode.MaxPerformance:
// No specific tuning required for now, ASF is optimized for max performance by default
break;
case GlobalConfig.EOptimizationMode.MinMemoryUsage:
// We can disable regex cache which will slightly lower memory usage (for a huge performance hit)
Regex.CacheSize = 0;
break;
default:
throw new ArgumentOutOfRangeException(nameof(optimizationMode));
}
}
internal static bool IsRunningAsRoot() {
if (OperatingSystem.IsWindows()) {
using WindowsIdentity identity = WindowsIdentity.GetCurrent();
return identity.IsSystem || new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
}
if (OperatingSystem.IsFreeBSD() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) {
return NativeMethods.GetEUID() == 0;
}
// We can't determine whether user is running as root or not, so fallback to that not happening
return false;
}
internal static async Task<bool> RegisterProcess() {
if (SingleInstance != null) {
return false;
}
internal static async Task<bool> RegisterProcess() {
if (SingleInstance != null) {
return false;
// The only purpose of using hashing here is to cut on a potential size of the resource name - paths can be really long, and we almost certainly have some upper limit on the resource name we can allocate
// At the same time it'd be the best if we avoided all special characters, such as '/' found e.g. in base64, as we can't be sure that it's not a prohibited character in regards to native OS implementation
// Because of that, SHA256 is sufficient for our case, as it generates alphanumeric characters only, and is barely 256-bit long. We don't need any kind of complex cryptography or collision detection here, any hashing will do, and the shorter the better
string uniqueName = $"Global\\{GetOsResourceName(nameof(SingleInstance))}-{Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(Directory.GetCurrentDirectory())))}";
Mutex? singleInstance = null;
for (byte i = 0; i < WebBrowser.MaxTries; i++) {
if (i > 0) {
await Task.Delay(1000).ConfigureAwait(false);
}
string uniqueName;
singleInstance = new Mutex(true, uniqueName, out bool result);
// The only purpose of using hashingAlgorithm here is to cut on a potential size of the resource name - paths can be really long, and we almost certainly have some upper limit on the resource name we can allocate
// At the same time it'd be the best if we avoided all special characters, such as '/' found e.g. in base64, as we can't be sure that it's not a prohibited character in regards to native OS implementation
// Because of that, SHA256 is sufficient for our case, as it generates alphanumeric characters only, and is barely 256-bit long. We don't need any kind of complex cryptography or collision detection here, any hashing algorithm will do, and the shorter the better
using (SHA256 hashingAlgorithm = SHA256.Create()) {
uniqueName = $"Global\\{GetOsResourceName(nameof(SingleInstance))}-{BitConverter.ToString(hashingAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(Directory.GetCurrentDirectory()))).Replace("-", "", StringComparison.Ordinal)}";
if (result) {
break;
}
Mutex? singleInstance = null;
singleInstance.Dispose();
singleInstance = null;
}
for (byte i = 0; i < WebBrowser.MaxTries; i++) {
if (i > 0) {
await Task.Delay(1000).ConfigureAwait(false);
}
if (singleInstance == null) {
return false;
}
singleInstance = new Mutex(true, uniqueName, out bool result);
SingleInstance = singleInstance;
if (result) {
break;
}
return true;
}
singleInstance.Dispose();
singleInstance = null;
}
[SupportedOSPlatform("FreeBSD")]
[SupportedOSPlatform("Linux")]
[SupportedOSPlatform("MacOS")]
internal static void UnixSetFileAccess(string path, EUnixPermission permission) {
if (string.IsNullOrEmpty(path)) {
throw new ArgumentNullException(nameof(path));
}
if (singleInstance == null) {
return false;
}
if (!OperatingSystem.IsFreeBSD() && !OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS()) {
throw new PlatformNotSupportedException();
}
SingleInstance = singleInstance;
if (!File.Exists(path) && !Directory.Exists(path)) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"!{nameof(path)}"));
return;
}
// Chmod() returns 0 on success, -1 on failure
if (NativeMethods.Chmod(path, (int) permission) != 0) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, Marshal.GetLastWin32Error()));
}
}
internal static void UnregisterProcess() {
if (SingleInstance == null) {
return;
}
// We should release the mutex here, but that can be done only from the same thread due to thread affinity
// Instead, we'll dispose the mutex which should automatically release it by the CLR
SingleInstance.Dispose();
SingleInstance = null;
}
internal static bool VerifyEnvironment() {
// We're not going to analyze source builds, as we don't know what changes the author has made, assume they have a point
if (SharedInfo.BuildInfo.IsCustomBuild) {
return true;
}
#if TARGET_GENERIC || !TARGET_WINDOWS
[SupportedOSPlatform("FreeBSD")]
[SupportedOSPlatform("Linux")]
[SupportedOSPlatform("MacOS")]
internal static void UnixSetFileAccess(string path, EUnixPermission permission) {
if (string.IsNullOrEmpty(path)) {
throw new ArgumentNullException(nameof(path));
}
if (!OperatingSystem.IsFreeBSD() && !OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS()) {
throw new PlatformNotSupportedException();
}
if (!File.Exists(path) && !Directory.Exists(path)) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"!{nameof(path)}"));
return;
}
// Chmod() returns 0 on success, -1 on failure
if (NativeMethods.Chmod(path, (int) permission) != 0) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, Marshal.GetLastWin32Error()));
}
}
#endif
internal static void UnregisterProcess() {
if (SingleInstance == null) {
return;
}
// We should release the mutex here, but that can be done only from the same thread due to thread affinity
// Instead, we'll dispose the mutex which should automatically release it by the CLR
SingleInstance.Dispose();
SingleInstance = null;
}
internal static bool VerifyEnvironment() {
if (SharedInfo.BuildInfo.Variant.EndsWith("-netf", StringComparison.Ordinal)) {
#if NETFRAMEWORK
// This is .NET Framework build, we support that one only on mono for platforms not supported by .NET Core
// We're not going to analyze source builds, as we don't know what changes the author has made, assume they have a point
if (SharedInfo.BuildInfo.IsCustomBuild) {
return true;
}
// All windows variants have valid .NET Core build, and generic-netf is supported only on mono
if (OperatingSystem.IsWindows() || !RuntimeMadness.IsRunningOnMono) {
// All Windows variants (7+) have valid .NET Core build
if (OperatingSystem.IsWindows()) {
return false;
}
// Non-Windows variants of generic-netf are supported only in Mono
if (!RuntimeMadness.IsRunningOnMono) {
return false;
}
// Platforms not supported by .NET Core
return RuntimeInformation.OSArchitecture switch {
// Sadly we can't tell a difference between ARMv6 and ARMv7 reliably, we'll believe that this linux-arm user knows what he's doing and he's indeed in need of generic-netf on ARMv6
Architecture.Arm => true,
@@ -264,135 +257,147 @@ namespace ArchiSteamFarm.Core {
};
#else
// This is .NET Core build, we support all scenarios
// .NET Framework build running on .NET Core? Very funny - only if somebody lied during build process
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(SharedInfo.BuildInfo.Variant), SharedInfo.BuildInfo.Variant));
return false;
#endif
}
if (SharedInfo.BuildInfo.Variant == "generic") {
// Generic is supported everywhere
return true;
#endif
}
#if TARGET_GENERIC || TARGET_WINDOWS
if ((SharedInfo.BuildInfo.Variant == "docker") || SharedInfo.BuildInfo.Variant.StartsWith("linux-", StringComparison.Ordinal)) {
// OS-specific Linux and Docker builds are supported only on Linux
return OperatingSystem.IsLinux();
}
if (SharedInfo.BuildInfo.Variant.StartsWith("osx-", StringComparison.Ordinal)) {
// OS-specific macOS build is supported only on macOS
return OperatingSystem.IsMacOS();
}
if (SharedInfo.BuildInfo.Variant.StartsWith("win-", StringComparison.Ordinal)) {
// OS-specific Windows build is supported only on Windows
return OperatingSystem.IsWindows();
}
// Unknown combination, we intend to cover all of the available ones above, so this results in an error
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(SharedInfo.BuildInfo.Variant), SharedInfo.BuildInfo.Variant));
return false;
}
[SupportedOSPlatform("Windows")]
private static void WindowsDisableQuickEditMode() {
if (!OperatingSystem.IsWindows()) {
throw new PlatformNotSupportedException();
}
IntPtr consoleHandle = NativeMethods.GetStdHandle(NativeMethods.StandardInputHandle);
if (!NativeMethods.GetConsoleMode(consoleHandle, out uint consoleMode)) {
ASF.ArchiLogger.LogGenericError(Strings.WarningFailed);
return;
}
consoleMode &= ~NativeMethods.EnableQuickEditMode;
if (!NativeMethods.SetConsoleMode(consoleHandle, consoleMode)) {
ASF.ArchiLogger.LogGenericError(Strings.WarningFailed);
}
}
[SupportedOSPlatform("Windows")]
private static void WindowsKeepSystemActive() {
if (!OperatingSystem.IsWindows()) {
throw new PlatformNotSupportedException();
}
// This function calls unmanaged API in order to tell Windows OS that it should not enter sleep state while the program is running
// If user wishes to enter sleep mode, then he should use ShutdownOnFarmingFinished or manage ASF process with third-party tool or script
// See https://docs.microsoft.com/windows/win32/api/winbase/nf-winbase-setthreadexecutionstate for more details
NativeMethods.EExecutionState result = NativeMethods.SetThreadExecutionState(NativeMethods.AwakeExecutionState);
// SetThreadExecutionState() returns NULL on failure, which is mapped to 0 (EExecutionState.None) in our case
if (result == NativeMethods.EExecutionState.None) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result));
}
}
[Flags]
[SupportedOSPlatform("FreeBSD")]
[SupportedOSPlatform("Linux")]
[SupportedOSPlatform("MacOS")]
internal enum EUnixPermission : ushort {
OtherExecute = 0x1,
OtherWrite = 0x2,
OtherRead = 0x4,
GroupExecute = 0x8,
GroupWrite = 0x10,
GroupRead = 0x20,
UserExecute = 0x40,
UserWrite = 0x80,
UserRead = 0x100,
Combined777 = UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute
}
private static class NativeMethods {
[SupportedOSPlatform("Windows")]
private static void WindowsDisableQuickEditMode() {
if (!OperatingSystem.IsWindows()) {
throw new PlatformNotSupportedException();
}
IntPtr consoleHandle = NativeMethods.GetStdHandle(NativeMethods.StandardInputHandle);
if (!NativeMethods.GetConsoleMode(consoleHandle, out uint consoleMode)) {
ASF.ArchiLogger.LogGenericError(Strings.WarningFailed);
return;
}
consoleMode &= ~NativeMethods.EnableQuickEditMode;
if (!NativeMethods.SetConsoleMode(consoleHandle, consoleMode)) {
ASF.ArchiLogger.LogGenericError(Strings.WarningFailed);
}
}
internal const EExecutionState AwakeExecutionState = EExecutionState.SystemRequired | EExecutionState.AwayModeRequired | EExecutionState.Continuous;
[SupportedOSPlatform("Windows")]
private static void WindowsKeepSystemActive() {
if (!OperatingSystem.IsWindows()) {
throw new PlatformNotSupportedException();
}
internal const uint EnableQuickEditMode = 0x0040;
// This function calls unmanaged API in order to tell Windows OS that it should not enter sleep state while the program is running
// If user wishes to enter sleep mode, then he should use ShutdownOnFarmingFinished or manage ASF process with third-party tool or script
// See https://docs.microsoft.com/windows/win32/api/winbase/nf-winbase-setthreadexecutionstate for more details
NativeMethods.EExecutionState result = NativeMethods.SetThreadExecutionState(NativeMethods.AwakeExecutionState);
[SupportedOSPlatform("Windows")]
internal const sbyte StandardInputHandle = -10;
// SetThreadExecutionState() returns NULL on failure, which is mapped to 0 (EExecutionState.None) in our case
if (result == NativeMethods.EExecutionState.None) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result));
}
}
#endif
#if TARGET_GENERIC || !TARGET_WINDOWS
[Flags]
#pragma warning disable CA2101 // False positive, we can't use unicode charset on Unix, and it uses UTF-8 by default anyway
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("libc", EntryPoint = "chmod", SetLastError = true)]
[SupportedOSPlatform("FreeBSD")]
[SupportedOSPlatform("Linux")]
[SupportedOSPlatform("MacOS")]
internal enum EUnixPermission : ushort {
OtherExecute = 0x1,
OtherWrite = 0x2,
OtherRead = 0x4,
GroupExecute = 0x8,
GroupWrite = 0x10,
GroupRead = 0x20,
UserExecute = 0x40,
UserWrite = 0x80,
UserRead = 0x100,
Combined755 = UserRead | UserWrite | UserExecute | GroupRead | GroupExecute | OtherRead | OtherExecute,
Combined777 = UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute
}
#endif
private static class NativeMethods {
#if TARGET_GENERIC || TARGET_WINDOWS
[SupportedOSPlatform("Windows")]
internal const EExecutionState AwakeExecutionState = EExecutionState.SystemRequired | EExecutionState.AwayModeRequired | EExecutionState.Continuous;
[SupportedOSPlatform("Windows")]
internal const uint EnableQuickEditMode = 0x0040;
[SupportedOSPlatform("Windows")]
internal const sbyte StandardInputHandle = -10;
#endif
#if TARGET_GENERIC || !TARGET_WINDOWS
#pragma warning disable CA2101 // False positive, we can't use unicode charset on Unix, and it uses UTF-8 by default anyway
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("libc", EntryPoint = "chmod", SetLastError = true)]
[SupportedOSPlatform("FreeBSD")]
[SupportedOSPlatform("Linux")]
[SupportedOSPlatform("MacOS")]
internal static extern int Chmod(string path, int mode);
internal static extern int Chmod(string path, int mode);
#pragma warning restore CA2101 // False positive, we can't use unicode charset on Unix, and it uses UTF-8 by default anyway
#endif
#if TARGET_GENERIC || TARGET_WINDOWS
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("kernel32.dll")]
[SupportedOSPlatform("Windows")]
internal static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
#endif
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("kernel32.dll")]
[SupportedOSPlatform("Windows")]
internal static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
#if TARGET_GENERIC || !TARGET_WINDOWS
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("libc", EntryPoint = "geteuid", SetLastError = true)]
[SupportedOSPlatform("FreeBSD")]
[SupportedOSPlatform("Linux")]
[SupportedOSPlatform("MacOS")]
internal static extern uint GetEUID();
#endif
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("libc", EntryPoint = "geteuid", SetLastError = true)]
[SupportedOSPlatform("FreeBSD")]
[SupportedOSPlatform("Linux")]
[SupportedOSPlatform("MacOS")]
internal static extern uint GetEUID();
#if TARGET_GENERIC || TARGET_WINDOWS
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("kernel32.dll")]
[SupportedOSPlatform("Windows")]
internal static extern IntPtr GetStdHandle(int nStdHandle);
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("kernel32.dll")]
[SupportedOSPlatform("Windows")]
internal static extern IntPtr GetStdHandle(int nStdHandle);
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("kernel32.dll")]
[SupportedOSPlatform("Windows")]
internal static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("kernel32.dll")]
[SupportedOSPlatform("Windows")]
internal static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("kernel32.dll")]
[SupportedOSPlatform("Windows")]
internal static extern EExecutionState SetThreadExecutionState(EExecutionState executionState);
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("kernel32.dll")]
[SupportedOSPlatform("Windows")]
internal static extern EExecutionState SetThreadExecutionState(EExecutionState executionState);
[Flags]
[SupportedOSPlatform("Windows")]
internal enum EExecutionState : uint {
None = 0,
SystemRequired = 0x00000001,
AwayModeRequired = 0x00000040,
Continuous = 0x80000000
}
#endif
[Flags]
[SupportedOSPlatform("Windows")]
internal enum EExecutionState : uint {
None = 0,
SystemRequired = 0x00000001,
AwayModeRequired = 0x00000040,
Continuous = 0x80000000
}
}
}

View File

@@ -0,0 +1,707 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Cards;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Exchange;
using ArchiSteamFarm.Steam.Integration;
using ArchiSteamFarm.Steam.Security;
using ArchiSteamFarm.Steam.Storage;
using ArchiSteamFarm.Storage;
using ArchiSteamFarm.Web;
namespace ArchiSteamFarm.Core;
internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
private const ushort MaxItemsForFairBots = ArchiWebHandler.MaxItemsInSingleInventoryRequest * WebBrowser.MaxTries; // Determines which fair bots we'll deprioritize when matching due to excessive number of inventory requests they need to make, which are likely to fail in the process or cause excessive delays
private const byte MaxMatchedBotsHard = 40; // Determines how many bots we can attempt to match in total, where match attempt is equal to analyzing bot's inventory
private const byte MaxMatchingRounds = 10; // Determines maximum amount of matching rounds we're going to consider before leaving the rest of work for the next batch
private const byte MinAnnouncementCheckTTL = 6; // Minimum amount of hours we must wait before checking eligibility for Announcement, should be lower than MinPersonaStateTTL
private const byte MinHeartBeatTTL = 10; // Minimum amount of minutes we must wait before sending next HeartBeat
private const byte MinItemsCount = 100; // Minimum amount of items to be eligible for public listing
private const byte MinPersonaStateTTL = 8; // Minimum amount of hours we must wait before requesting persona state update
private static readonly ImmutableHashSet<Asset.EType> AcceptedMatchableTypes = ImmutableHashSet.Create(
Asset.EType.Emoticon,
Asset.EType.FoilTradingCard,
Asset.EType.ProfileBackground,
Asset.EType.TradingCard
);
private readonly Bot Bot;
private readonly SemaphoreSlim MatchActivelySemaphore = new(1, 1);
private readonly Timer? MatchActivelyTimer;
private readonly SemaphoreSlim RequestsSemaphore = new(1, 1);
private DateTime LastAnnouncementCheck;
private DateTime LastHeartBeat;
private DateTime LastPersonaStateRequest;
private bool ShouldSendHeartBeats;
internal RemoteCommunication(Bot bot) {
Bot = bot ?? throw new ArgumentNullException(nameof(bot));
if (Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively)) {
MatchActivelyTimer = new Timer(
MatchActively,
null,
TimeSpan.FromHours(1) + TimeSpan.FromSeconds(ASF.LoadBalancingDelay * Bot.Bots?.Count ?? 0), // Delay
TimeSpan.FromHours(8) // Period
);
}
}
public void Dispose() {
// Those are objects that are always being created if constructor doesn't throw exception
MatchActivelySemaphore.Dispose();
RequestsSemaphore.Dispose();
// Those are objects that might be null and the check should be in-place
MatchActivelyTimer?.Dispose();
}
public async ValueTask DisposeAsync() {
// Those are objects that are always being created if constructor doesn't throw exception
MatchActivelySemaphore.Dispose();
RequestsSemaphore.Dispose();
// Those are objects that might be null and the check should be in-place
if (MatchActivelyTimer != null) {
await MatchActivelyTimer.DisposeAsync().ConfigureAwait(false);
}
}
internal async Task OnHeartBeat() {
if (!Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.PublicListing)) {
return;
}
// Request persona update if needed
if ((DateTime.UtcNow > LastPersonaStateRequest.AddHours(MinPersonaStateTTL)) && (DateTime.UtcNow > LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL))) {
LastPersonaStateRequest = DateTime.UtcNow;
Bot.RequestPersonaStateUpdate();
}
if (!ShouldSendHeartBeats || (DateTime.UtcNow < LastHeartBeat.AddMinutes(MinHeartBeatTTL))) {
return;
}
if (!await RequestsSemaphore.WaitAsync(0).ConfigureAwait(false)) {
return;
}
try {
HttpStatusCode? response = await ArchiNet.HeartBeatForListing(Bot).ConfigureAwait(false);
if (!response.HasValue) {
return;
}
if (response.Value.IsClientErrorCode()) {
LastHeartBeat = DateTime.MinValue;
ShouldSendHeartBeats = false;
return;
}
LastHeartBeat = DateTime.UtcNow;
} finally {
RequestsSemaphore.Release();
}
}
internal async Task OnLoggedOn() {
if (!Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.SteamGroup)) {
return;
}
if (!await Bot.ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID).ConfigureAwait(false)) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ArchiWebHandler.JoinGroup)));
}
}
internal async Task OnPersonaState(string? nickname = null, string? avatarHash = null) {
if (!Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.PublicListing)) {
return;
}
if ((DateTime.UtcNow < LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL)) && (ShouldSendHeartBeats || (LastHeartBeat == DateTime.MinValue))) {
return;
}
await RequestsSemaphore.WaitAsync().ConfigureAwait(false);
try {
if ((DateTime.UtcNow < LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL)) && (ShouldSendHeartBeats || (LastHeartBeat == DateTime.MinValue))) {
return;
}
// Don't announce if we don't meet conditions
bool? eligible = await IsEligibleForListing().ConfigureAwait(false);
if (!eligible.HasValue) {
// This is actually network failure, so we'll stop sending heartbeats but not record it as valid check
ShouldSendHeartBeats = false;
return;
}
if (!eligible.Value) {
LastAnnouncementCheck = DateTime.UtcNow;
ShouldSendHeartBeats = false;
return;
}
string? tradeToken = await Bot.ArchiHandler.GetTradeToken().ConfigureAwait(false);
if (string.IsNullOrEmpty(tradeToken)) {
// This is actually network failure, so we'll stop sending heartbeats but not record it as valid check
ShouldSendHeartBeats = false;
return;
}
HashSet<Asset.EType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
if (acceptedMatchableTypes.Count == 0) {
Bot.ArchiLogger.LogNullError(acceptedMatchableTypes);
LastAnnouncementCheck = DateTime.UtcNow;
ShouldSendHeartBeats = false;
return;
}
HashSet<Asset> inventory;
try {
inventory = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => item.Tradable && acceptedMatchableTypes.Contains(item.Type)).ToHashSetAsync().ConfigureAwait(false);
} catch (HttpRequestException e) {
Bot.ArchiLogger.LogGenericWarningException(e);
// This is actually inventory failure, so we'll stop sending heartbeats but not record it as valid check
ShouldSendHeartBeats = false;
return;
} catch (Exception e) {
Bot.ArchiLogger.LogGenericException(e);
// This is actually inventory failure, so we'll stop sending heartbeats but not record it as valid check
ShouldSendHeartBeats = false;
return;
}
LastAnnouncementCheck = DateTime.UtcNow;
// This is actual inventory
if (inventory.Count < MinItemsCount) {
ShouldSendHeartBeats = false;
return;
}
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
HttpStatusCode? response = await ArchiNet.AnnounceForListing(Bot, inventory, acceptedMatchableTypes, tradeToken!, nickname, avatarHash).ConfigureAwait(false);
if (!response.HasValue) {
return;
}
if (response.Value.IsClientErrorCode()) {
LastHeartBeat = DateTime.MinValue;
ShouldSendHeartBeats = false;
return;
}
LastHeartBeat = DateTime.UtcNow;
ShouldSendHeartBeats = true;
} finally {
RequestsSemaphore.Release();
}
}
private async Task<bool?> IsEligibleForListing() {
// Bot must be eligible for matching first
bool? isEligibleForMatching = await IsEligibleForMatching().ConfigureAwait(false);
if (isEligibleForMatching != true) {
return isEligibleForMatching;
}
// Bot must have STM enabled in TradingPreferences
if (!Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.SteamTradeMatcher)) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.BotConfig.TradingPreferences)}: {Bot.BotConfig.TradingPreferences}"));
return false;
}
// Bot must have public inventory
bool? hasPublicInventory = await Bot.HasPublicInventory().ConfigureAwait(false);
if (hasPublicInventory != true) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.HasPublicInventory)}: {hasPublicInventory?.ToString() ?? "null"}"));
return hasPublicInventory;
}
return true;
}
private async Task<bool?> IsEligibleForMatching() {
// Bot must have ASF 2FA
if (!Bot.HasMobileAuthenticator) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.HasMobileAuthenticator)}: {Bot.HasMobileAuthenticator}"));
return false;
}
// Bot must have at least one accepted matchable type set
if ((Bot.BotConfig.MatchableTypes.Count == 0) || Bot.BotConfig.MatchableTypes.All(static type => !AcceptedMatchableTypes.Contains(type))) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.BotConfig.MatchableTypes)}: {string.Join(", ", Bot.BotConfig.MatchableTypes)}"));
return false;
}
// Bot must have valid API key (e.g. not being restricted account)
bool? hasValidApiKey = await Bot.ArchiWebHandler.HasValidApiKey().ConfigureAwait(false);
if (hasValidApiKey != true) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.ArchiWebHandler.HasValidApiKey)}: {hasValidApiKey?.ToString() ?? "null"}"));
return hasValidApiKey;
}
return true;
}
private async void MatchActively(object? state = null) {
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) || !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively) || (await IsEligibleForMatching().ConfigureAwait(false) != true)) {
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
return;
}
HashSet<Asset.EType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
if (acceptedMatchableTypes.Count == 0) {
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
return;
}
if (!await MatchActivelySemaphore.WaitAsync(0).ConfigureAwait(false)) {
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
return;
}
try {
Bot.ArchiLogger.LogGenericTrace(Strings.Starting);
Dictionary<ulong, (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs)> triedSteamIDs = new();
bool shouldContinueMatching = true;
bool tradedSomething = false;
for (byte i = 0; (i < MaxMatchingRounds) && shouldContinueMatching; i++) {
if ((i > 0) && tradedSomething) {
// After each round we wait at least 5 minutes for all bots to react
await Task.Delay(5 * 60 * 1000).ConfigureAwait(false);
}
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) || !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively) || (await IsEligibleForMatching().ConfigureAwait(false) != true)) {
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
break;
}
#pragma warning disable CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
using (await Bot.Actions.GetTradingLock().ConfigureAwait(false)) {
#pragma warning restore CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ActivelyMatchingItems, i));
(shouldContinueMatching, tradedSomething) = await MatchActivelyRound(acceptedMatchableTypes, triedSteamIDs).ConfigureAwait(false);
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.DoneActivelyMatchingItems, i));
}
}
Bot.ArchiLogger.LogGenericTrace(Strings.Done);
} finally {
MatchActivelySemaphore.Release();
}
}
private async Task<(bool ShouldContinueMatching, bool TradedSomething)> MatchActivelyRound(IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, IDictionary<ulong, (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs)> triedSteamIDs) {
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
}
ArgumentNullException.ThrowIfNull(triedSteamIDs);
HashSet<Asset> ourInventory;
try {
ourInventory = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => acceptedMatchableTypes.Contains(item.Type) && !Bot.BotDatabase.MatchActivelyBlacklistAppIDs.Contains(item.RealAppID)).ToHashSetAsync().ConfigureAwait(false);
} catch (HttpRequestException e) {
Bot.ArchiLogger.LogGenericWarningException(e);
return (false, false);
} catch (Exception e) {
Bot.ArchiLogger.LogGenericException(e);
return (false, false);
}
if (ourInventory.Count == 0) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(ourInventory)));
return (false, false);
}
(Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourFullState, Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourTradableState) = Trading.GetDividedInventoryState(ourInventory);
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
// User doesn't have any more dupes in the inventory
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, $"{nameof(ourFullState)} || {nameof(ourTradableState)}"));
return (false, false);
}
ImmutableHashSet<ArchiNet.ListedUser>? listedUsers = await ArchiNet.GetListedUsers(Bot).ConfigureAwait(false);
if ((listedUsers == null) || (listedUsers.Count == 0)) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(listedUsers)));
return (false, false);
}
byte maxTradeHoldDuration = ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration;
byte totalMatches = 0;
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisRound = new();
foreach (ArchiNet.ListedUser? listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && (!triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) attempt) || (attempt.Tries < byte.MaxValue)) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) attempt) ? attempt.Tries : 0).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenByDescending(static listedUser => listedUser.MatchEverything || (listedUser.ItemsCount < MaxItemsForFairBots)).ThenByDescending(static listedUser => listedUser.Score)) {
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> wantedSets = ourTradableState.Keys.Where(set => !skippedSetsThisRound.Contains(set) && listedUser.MatchableTypes.Contains(set.Type)).ToHashSet();
if (wantedSets.Count == 0) {
continue;
}
if (++totalMatches > MaxMatchedBotsHard) {
break;
}
Bot.ArchiLogger.LogGenericTrace($"{listedUser.SteamID}...");
byte? tradeHoldDuration = await Bot.ArchiWebHandler.GetCombinedTradeHoldDurationAgainstUser(listedUser.SteamID, listedUser.TradeToken).ConfigureAwait(false);
switch (tradeHoldDuration) {
case null:
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(tradeHoldDuration)));
continue;
case > 0 when (tradeHoldDuration.Value > maxTradeHoldDuration) || (tradeHoldDuration.Value > listedUser.MaxTradeHoldDuration):
Bot.ArchiLogger.LogGenericTrace($"{tradeHoldDuration.Value} > {maxTradeHoldDuration} || {listedUser.MaxTradeHoldDuration}");
continue;
}
HashSet<Asset> theirInventory;
try {
theirInventory = await Bot.ArchiWebHandler.GetInventoryAsync(listedUser.SteamID).Where(item => (!listedUser.MatchEverything || item.Tradable) && wantedSets.Contains((item.RealAppID, item.Type, item.Rarity)) && ((tradeHoldDuration.Value == 0) || !(item.Type is Asset.EType.FoilTradingCard or Asset.EType.TradingCard && CardsFarmer.SalesBlacklist.Contains(item.RealAppID)))).ToHashSetAsync().ConfigureAwait(false);
} catch (HttpRequestException e) {
Bot.ArchiLogger.LogGenericWarningException(e);
continue;
} catch (Exception e) {
Bot.ArchiLogger.LogGenericException(e);
continue;
}
if (theirInventory.Count == 0) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(theirInventory)));
continue;
}
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisUser = new();
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> theirTradableState = Trading.GetTradableInventoryState(theirInventory);
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> inventoryStateChanges = new();
for (byte i = 0; i < Trading.MaxTradesPerAccount; i++) {
byte itemsInTrade = 0;
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisTrade = new();
Dictionary<ulong, uint> classIDsToGive = new();
Dictionary<ulong, uint> classIDsToReceive = new();
Dictionary<ulong, uint> fairClassIDsToGive = new();
Dictionary<ulong, uint> fairClassIDsToReceive = new();
foreach (((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) set, Dictionary<ulong, uint> ourFullItems) in ourFullState.Where(set => !skippedSetsThisUser.Contains(set.Key) && listedUser.MatchableTypes.Contains(set.Key.Type) && set.Value.Values.Any(static count => count > 1))) {
if (!ourTradableState.TryGetValue(set, out Dictionary<ulong, uint>? ourTradableItems) || (ourTradableItems.Count == 0)) {
continue;
}
if (!theirTradableState.TryGetValue(set, out Dictionary<ulong, uint>? theirTradableItems) || (theirTradableItems.Count == 0)) {
continue;
}
// Those 2 collections are on user-basis since we can't be sure that the trade passes through (and therefore we need to keep original state in case of failure)
Dictionary<ulong, uint> ourFullSet = new(ourFullItems);
Dictionary<ulong, uint> ourTradableSet = new(ourTradableItems);
// We also have to take into account changes that happened in previous trades with this user, so this block will adapt to that
if (inventoryStateChanges.TryGetValue(set, out Dictionary<ulong, uint>? pastChanges) && (pastChanges.Count > 0)) {
foreach ((ulong classID, uint amount) in pastChanges) {
if (!ourFullSet.TryGetValue(classID, out uint fullAmount) || (fullAmount == 0) || (fullAmount < amount)) {
Bot.ArchiLogger.LogNullError(fullAmount);
return (false, skippedSetsThisRound.Count > 0);
}
if (fullAmount > amount) {
ourFullSet[classID] = fullAmount - amount;
} else {
ourFullSet.Remove(classID);
}
if (!ourTradableSet.TryGetValue(classID, out uint tradableAmount) || (tradableAmount == 0) || (tradableAmount < amount)) {
Bot.ArchiLogger.LogNullError(tradableAmount);
return (false, skippedSetsThisRound.Count > 0);
}
if (fullAmount > amount) {
ourTradableSet[classID] = fullAmount - amount;
} else {
ourTradableSet.Remove(classID);
}
}
if (Trading.IsEmptyForMatching(ourFullSet, ourTradableSet)) {
continue;
}
}
bool match;
do {
match = false;
foreach ((ulong ourItem, uint ourFullAmount) in ourFullSet.Where(static item => item.Value > 1).OrderByDescending(static item => item.Value)) {
if (!ourTradableSet.TryGetValue(ourItem, out uint ourTradableAmount) || (ourTradableAmount == 0)) {
continue;
}
foreach ((ulong theirItem, uint theirTradableAmount) in theirTradableItems.OrderBy(item => ourFullSet.TryGetValue(item.Key, out uint ourAmountOfTheirItem) ? ourAmountOfTheirItem : 0)) {
if (ourFullSet.TryGetValue(theirItem, out uint ourAmountOfTheirItem) && (ourFullAmount <= ourAmountOfTheirItem + 1)) {
continue;
}
if (!listedUser.MatchEverything) {
// We have a potential match, let's check fairness for them
fairClassIDsToGive.TryGetValue(ourItem, out uint fairGivenAmount);
fairClassIDsToReceive.TryGetValue(theirItem, out uint fairReceivedAmount);
fairClassIDsToGive[ourItem] = ++fairGivenAmount;
fairClassIDsToReceive[theirItem] = ++fairReceivedAmount;
// Filter their inventory for the sets we're trading or have traded with this user
HashSet<Asset> fairFiltered = theirInventory.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).Select(static item => item.CreateShallowCopy()).ToHashSet();
// Copy list to HashSet<Steam.Asset>
HashSet<Asset> fairItemsToGive = Trading.GetTradableItemsFromInventory(ourInventory.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).Select(static item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToGive.ToDictionary(static classID => classID.Key, static classID => classID.Value));
HashSet<Asset> fairItemsToReceive = Trading.GetTradableItemsFromInventory(fairFiltered.Select(static item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToReceive.ToDictionary(static classID => classID.Key, static classID => classID.Value));
// Actual check:
if (!Trading.IsTradeNeutralOrBetter(fairFiltered, fairItemsToReceive, fairItemsToGive)) {
if (fairGivenAmount > 1) {
fairClassIDsToGive[ourItem] = fairGivenAmount - 1;
} else {
fairClassIDsToGive.Remove(ourItem);
}
if (fairReceivedAmount > 1) {
fairClassIDsToReceive[theirItem] = fairReceivedAmount - 1;
} else {
fairClassIDsToReceive.Remove(theirItem);
}
continue;
}
}
// Skip this set from the remaining of this round
skippedSetsThisTrade.Add(set);
// Update our state based on given items
classIDsToGive[ourItem] = classIDsToGive.TryGetValue(ourItem, out uint ourGivenAmount) ? ourGivenAmount + 1 : 1;
ourFullSet[ourItem] = ourFullAmount - 1; // We don't need to remove anything here because we can guarantee that ourItem.Value is at least 2
if (inventoryStateChanges.TryGetValue(set, out Dictionary<ulong, uint>? currentChanges)) {
currentChanges[ourItem] = currentChanges.TryGetValue(ourItem, out uint amount) ? amount + 1 : 1;
} else {
inventoryStateChanges[set] = new Dictionary<ulong, uint> {
{ ourItem, 1 }
};
}
// Update our state based on received items
classIDsToReceive[theirItem] = classIDsToReceive.TryGetValue(theirItem, out uint ourReceivedAmount) ? ourReceivedAmount + 1 : 1;
ourFullSet[theirItem] = ourAmountOfTheirItem + 1;
if (ourTradableAmount > 1) {
ourTradableSet[ourItem] = ourTradableAmount - 1;
} else {
ourTradableSet.Remove(ourItem);
}
// Update their state based on taken items
if (theirTradableAmount > 1) {
theirTradableItems[theirItem] = theirTradableAmount - 1;
} else {
theirTradableItems.Remove(theirItem);
}
itemsInTrade += 2;
match = true;
break;
}
if (match) {
break;
}
}
} while (match && (itemsInTrade < Trading.MaxItemsPerTrade - 1));
if (itemsInTrade >= Trading.MaxItemsPerTrade - 1) {
break;
}
}
if (skippedSetsThisTrade.Count == 0) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(skippedSetsThisTrade)));
break;
}
// Remove the items from inventories
HashSet<Asset> itemsToGive = Trading.GetTradableItemsFromInventory(ourInventory, classIDsToGive);
HashSet<Asset> itemsToReceive = Trading.GetTradableItemsFromInventory(theirInventory, classIDsToReceive);
if ((itemsToGive.Count != itemsToReceive.Count) || !Trading.IsFairExchange(itemsToGive, itemsToReceive)) {
// Failsafe
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, Strings.ErrorAborted));
return (false, skippedSetsThisRound.Count > 0);
}
if (triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) previousAttempt)) {
if ((previousAttempt.GivenAssetIDs == null) || (previousAttempt.ReceivedAssetIDs == null) || (itemsToGive.Select(static item => item.AssetID).All(previousAttempt.GivenAssetIDs.Contains) && itemsToReceive.Select(static item => item.AssetID).All(previousAttempt.ReceivedAssetIDs.Contains))) {
// This user didn't respond in our previous round, avoid him for remaining ones
triedSteamIDs[listedUser.SteamID] = (byte.MaxValue, previousAttempt.GivenAssetIDs, previousAttempt.ReceivedAssetIDs);
break;
}
previousAttempt.GivenAssetIDs.UnionWith(itemsToGive.Select(static item => item.AssetID));
previousAttempt.ReceivedAssetIDs.UnionWith(itemsToReceive.Select(static item => item.AssetID));
} else {
previousAttempt.GivenAssetIDs = new HashSet<ulong>(itemsToGive.Select(static item => item.AssetID));
previousAttempt.ReceivedAssetIDs = new HashSet<ulong>(itemsToReceive.Select(static item => item.AssetID));
}
triedSteamIDs[listedUser.SteamID] = (++previousAttempt.Tries, previousAttempt.GivenAssetIDs, previousAttempt.ReceivedAssetIDs);
Bot.ArchiLogger.LogGenericTrace($"{Bot.SteamID} <- {string.Join(", ", itemsToReceive.Select(static item => $"{item.RealAppID}/{item.Type}-{item.ClassID} #{item.Amount}"))} | {string.Join(", ", itemsToGive.Select(static item => $"{item.RealAppID}/{item.Type}-{item.ClassID} #{item.Amount}"))} -> {listedUser.SteamID}");
(bool success, HashSet<ulong>? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(listedUser.SteamID, itemsToGive, itemsToReceive, listedUser.TradeToken, true).ConfigureAwait(false);
if ((mobileTradeOfferIDs?.Count > 0) && Bot.HasMobileAuthenticator) {
(bool twoFactorSuccess, _, _) = await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false);
if (!twoFactorSuccess) {
Bot.ArchiLogger.LogGenericTrace(Strings.WarningFailed);
return (false, skippedSetsThisRound.Count > 0);
}
}
if (!success) {
Bot.ArchiLogger.LogGenericTrace(Strings.WarningFailed);
break;
}
// Add itemsToGive to theirInventory to reflect their current state if we're over MaxItemsPerTrade
theirInventory.UnionWith(itemsToGive);
skippedSetsThisUser.UnionWith(skippedSetsThisTrade);
Bot.ArchiLogger.LogGenericTrace(Strings.Success);
}
if (skippedSetsThisUser.Count == 0) {
if (skippedSetsThisRound.Count == 0) {
// If we didn't find any match on clean round, this user isn't going to have anything interesting for us anytime soon
triedSteamIDs[listedUser.SteamID] = (byte.MaxValue, null, null);
}
continue;
}
skippedSetsThisRound.UnionWith(skippedSetsThisUser);
foreach ((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) skippedSet in skippedSetsThisUser) {
ourFullState.Remove(skippedSet);
ourTradableState.Remove(skippedSet);
}
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
// User doesn't have any more dupes in the inventory
break;
}
ourFullState.TrimExcess();
ourTradableState.TrimExcess();
}
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ActivelyMatchingItemsRound, skippedSetsThisRound.Count));
// Keep matching when we either traded something this round (so it makes sense for a refresh) or if we didn't try all available bots yet (so it makes sense to keep going)
return ((totalMatches > 0) && ((skippedSetsThisRound.Count > 0) || triedSteamIDs.Values.All(static data => data.Tries < 2)), skippedSetsThisRound.Count > 0);
}
}

View File

@@ -1,848 +0,0 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2021 Ł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.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Cards;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Exchange;
using ArchiSteamFarm.Steam.Integration;
using ArchiSteamFarm.Steam.Security;
using ArchiSteamFarm.Steam.Storage;
using ArchiSteamFarm.Storage;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.Web.Responses;
using Newtonsoft.Json;
namespace ArchiSteamFarm.Core {
internal sealed class Statistics : IAsyncDisposable {
private const ushort MaxItemsForFairBots = ArchiWebHandler.MaxItemsInSingleInventoryRequest * WebBrowser.MaxTries; // Determines which fair bots we'll deprioritize when matching due to excessive number of inventory requests they need to make, which are likely to fail in the process or cause excessive delays
private const byte MaxMatchedBotsHard = 40; // Determines how many bots we can attempt to match in total, where match attempt is equal to analyzing bot's inventory
private const byte MaxMatchingRounds = 10; // Determines maximum amount of matching rounds we're going to consider before leaving the rest of work for the next batch
private const byte MinAnnouncementCheckTTL = 6; // Minimum amount of hours we must wait before checking eligibility for Announcement, should be lower than MinPersonaStateTTL
private const byte MinHeartBeatTTL = 10; // Minimum amount of minutes we must wait before sending next HeartBeat
private const byte MinItemsCount = 100; // Minimum amount of items to be eligible for public listing
private const byte MinPersonaStateTTL = 8; // Minimum amount of hours we must wait before requesting persona state update
private const string URL = "https://" + SharedInfo.StatisticsServer;
private static readonly ImmutableHashSet<Asset.EType> AcceptedMatchableTypes = ImmutableHashSet.Create(
Asset.EType.Emoticon,
Asset.EType.FoilTradingCard,
Asset.EType.ProfileBackground,
Asset.EType.TradingCard
);
private readonly Bot Bot;
private readonly SemaphoreSlim MatchActivelySemaphore = new(1, 1);
#pragma warning disable CA2213 // False positive, .NET Framework can't understand DisposeAsync()
private readonly Timer MatchActivelyTimer;
#pragma warning restore CA2213 // False positive, .NET Framework can't understand DisposeAsync()
private readonly SemaphoreSlim RequestsSemaphore = new(1, 1);
private DateTime LastAnnouncementCheck;
private DateTime LastHeartBeat;
private DateTime LastPersonaStateRequest;
private bool ShouldSendHeartBeats;
internal Statistics(Bot bot) {
Bot = bot ?? throw new ArgumentNullException(nameof(bot));
MatchActivelyTimer = new Timer(
MatchActively,
null,
TimeSpan.FromHours(1) + TimeSpan.FromSeconds(ASF.LoadBalancingDelay * Bot.Bots?.Count ?? 0), // Delay
TimeSpan.FromHours(8) // Period
);
}
public async ValueTask DisposeAsync() {
MatchActivelySemaphore.Dispose();
RequestsSemaphore.Dispose();
await MatchActivelyTimer.DisposeAsync().ConfigureAwait(false);
}
internal async Task OnHeartBeat() {
// Request persona update if needed
if ((DateTime.UtcNow > LastPersonaStateRequest.AddHours(MinPersonaStateTTL)) && (DateTime.UtcNow > LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL))) {
LastPersonaStateRequest = DateTime.UtcNow;
Bot.RequestPersonaStateUpdate();
}
if (!ShouldSendHeartBeats || (DateTime.UtcNow < LastHeartBeat.AddMinutes(MinHeartBeatTTL))) {
return;
}
if (!await RequestsSemaphore.WaitAsync(0).ConfigureAwait(false)) {
return;
}
try {
Uri request = new($"{URL}/Api/HeartBeat");
Dictionary<string, string> data = new(2, StringComparer.Ordinal) {
{ "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") },
{ "SteamID", Bot.SteamID.ToString(CultureInfo.InvariantCulture) }
};
BasicResponse? response = await Bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
if (response == null) {
return;
}
if (response.StatusCode.IsClientErrorCode()) {
LastHeartBeat = DateTime.MinValue;
ShouldSendHeartBeats = false;
return;
}
LastHeartBeat = DateTime.UtcNow;
} finally {
RequestsSemaphore.Release();
}
}
internal async Task OnLoggedOn() {
if (!await Bot.ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID).ConfigureAwait(false)) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ArchiWebHandler.JoinGroup)));
}
}
internal async Task OnPersonaState(string? nickname = null, string? avatarHash = null) {
if ((DateTime.UtcNow < LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL)) && (ShouldSendHeartBeats || (LastHeartBeat == DateTime.MinValue))) {
return;
}
await RequestsSemaphore.WaitAsync().ConfigureAwait(false);
try {
if ((DateTime.UtcNow < LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL)) && (ShouldSendHeartBeats || (LastHeartBeat == DateTime.MinValue))) {
return;
}
// Don't announce if we don't meet conditions
bool? eligible = await IsEligibleForListing().ConfigureAwait(false);
if (!eligible.HasValue) {
// This is actually network failure, so we'll stop sending heartbeats but not record it as valid check
ShouldSendHeartBeats = false;
return;
}
if (!eligible.Value) {
LastAnnouncementCheck = DateTime.UtcNow;
ShouldSendHeartBeats = false;
return;
}
string? tradeToken = await Bot.ArchiHandler.GetTradeToken().ConfigureAwait(false);
if (string.IsNullOrEmpty(tradeToken)) {
// This is actually network failure, so we'll stop sending heartbeats but not record it as valid check
ShouldSendHeartBeats = false;
return;
}
HashSet<Asset.EType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
if (acceptedMatchableTypes.Count == 0) {
Bot.ArchiLogger.LogNullError(nameof(acceptedMatchableTypes));
LastAnnouncementCheck = DateTime.UtcNow;
ShouldSendHeartBeats = false;
return;
}
HashSet<Asset> inventory;
try {
inventory = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => item.Tradable && acceptedMatchableTypes.Contains(item.Type)).ToHashSetAsync().ConfigureAwait(false);
} catch (HttpRequestException e) {
Bot.ArchiLogger.LogGenericWarningException(e);
// This is actually inventory failure, so we'll stop sending heartbeats but not record it as valid check
ShouldSendHeartBeats = false;
return;
} catch (Exception e) {
Bot.ArchiLogger.LogGenericException(e);
// This is actually inventory failure, so we'll stop sending heartbeats but not record it as valid check
ShouldSendHeartBeats = false;
return;
}
LastAnnouncementCheck = DateTime.UtcNow;
// This is actual inventory
if (inventory.Count < MinItemsCount) {
ShouldSendHeartBeats = false;
return;
}
Uri request = new($"{URL}/Api/Announce");
Dictionary<string, string> data = new(9, StringComparer.Ordinal) {
{ "AvatarHash", avatarHash ?? "" },
{ "GamesCount", inventory.Select(static item => item.RealAppID).Distinct().Count().ToString(CultureInfo.InvariantCulture) },
{ "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") },
{ "ItemsCount", inventory.Count.ToString(CultureInfo.InvariantCulture) },
{ "MatchableTypes", JsonConvert.SerializeObject(acceptedMatchableTypes) },
{ "MatchEverything", Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) ? "1" : "0" },
{ "Nickname", nickname ?? "" },
{ "SteamID", Bot.SteamID.ToString(CultureInfo.InvariantCulture) },
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
{ "TradeToken", tradeToken! }
};
BasicResponse? response = await Bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
if (response == null) {
return;
}
if (response.StatusCode.IsClientErrorCode()) {
LastHeartBeat = DateTime.MinValue;
ShouldSendHeartBeats = false;
return;
}
LastHeartBeat = DateTime.UtcNow;
ShouldSendHeartBeats = true;
} finally {
RequestsSemaphore.Release();
}
}
private async Task<ImmutableHashSet<ListedUser>?> GetListedUsers() {
Uri request = new($"{URL}/Api/Bots");
ObjectResponse<ImmutableHashSet<ListedUser>>? response = await Bot.ArchiWebHandler.WebBrowser.UrlGetToJsonObject<ImmutableHashSet<ListedUser>>(request).ConfigureAwait(false);
return response?.Content;
}
private async Task<bool?> IsEligibleForListing() {
bool? isEligibleForMatching = await IsEligibleForMatching().ConfigureAwait(false);
if (isEligibleForMatching != true) {
return isEligibleForMatching;
}
// Bot must have public inventory
bool? hasPublicInventory = await Bot.HasPublicInventory().ConfigureAwait(false);
if (hasPublicInventory != true) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.HasPublicInventory)}: {hasPublicInventory?.ToString() ?? "null"}"));
return hasPublicInventory;
}
return true;
}
private async Task<bool?> IsEligibleForMatching() {
// Bot must have ASF 2FA
if (!Bot.HasMobileAuthenticator) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.HasMobileAuthenticator)}: {Bot.HasMobileAuthenticator}"));
return false;
}
// Bot must have STM enable in TradingPreferences
if (!Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.SteamTradeMatcher)) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.BotConfig.TradingPreferences)}: {Bot.BotConfig.TradingPreferences}"));
return false;
}
// Bot must have at least one accepted matchable type set
if ((Bot.BotConfig.MatchableTypes.Count == 0) || Bot.BotConfig.MatchableTypes.All(static type => !AcceptedMatchableTypes.Contains(type))) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.BotConfig.MatchableTypes)}: {string.Join(", ", Bot.BotConfig.MatchableTypes)}"));
return false;
}
// Bot must have valid API key (e.g. not being restricted account)
bool? hasValidApiKey = await Bot.ArchiWebHandler.HasValidApiKey().ConfigureAwait(false);
if (hasValidApiKey != true) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.ArchiWebHandler.HasValidApiKey)}: {hasValidApiKey?.ToString() ?? "null"}"));
return hasValidApiKey;
}
return true;
}
private async void MatchActively(object? state = null) {
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) || !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively) || (await IsEligibleForMatching().ConfigureAwait(false) != true)) {
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
return;
}
HashSet<Asset.EType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
if (acceptedMatchableTypes.Count == 0) {
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
return;
}
if (!await MatchActivelySemaphore.WaitAsync(0).ConfigureAwait(false)) {
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
return;
}
try {
Bot.ArchiLogger.LogGenericTrace(Strings.Starting);
Dictionary<ulong, (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs)> triedSteamIDs = new();
bool shouldContinueMatching = true;
bool tradedSomething = false;
for (byte i = 0; (i < MaxMatchingRounds) && shouldContinueMatching; i++) {
if ((i > 0) && tradedSomething) {
// After each round we wait at least 5 minutes for all bots to react
await Task.Delay(5 * 60 * 1000).ConfigureAwait(false);
}
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) || !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively) || (await IsEligibleForMatching().ConfigureAwait(false) != true)) {
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
break;
}
#pragma warning disable CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
using (await Bot.Actions.GetTradingLock().ConfigureAwait(false)) {
#pragma warning restore CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ActivelyMatchingItems, i));
(shouldContinueMatching, tradedSomething) = await MatchActivelyRound(acceptedMatchableTypes, triedSteamIDs).ConfigureAwait(false);
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.DoneActivelyMatchingItems, i));
}
}
Bot.ArchiLogger.LogGenericTrace(Strings.Done);
} finally {
MatchActivelySemaphore.Release();
}
}
private async Task<(bool ShouldContinueMatching, bool TradedSomething)> MatchActivelyRound(IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, IDictionary<ulong, (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs)> triedSteamIDs) {
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
}
if (triedSteamIDs == null) {
throw new ArgumentNullException(nameof(triedSteamIDs));
}
HashSet<Asset> ourInventory;
try {
ourInventory = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => acceptedMatchableTypes.Contains(item.Type) && !Bot.BotDatabase.MatchActivelyBlacklistedAppIDs.Contains(item.RealAppID)).ToHashSetAsync().ConfigureAwait(false);
} catch (HttpRequestException e) {
Bot.ArchiLogger.LogGenericWarningException(e);
return (false, false);
} catch (Exception e) {
Bot.ArchiLogger.LogGenericException(e);
return (false, false);
}
if (ourInventory.Count == 0) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(ourInventory)));
return (false, false);
}
(Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourFullState, Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourTradableState) = Trading.GetDividedInventoryState(ourInventory);
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
// User doesn't have any more dupes in the inventory
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, $"{nameof(ourFullState)} || {nameof(ourTradableState)}"));
return (false, false);
}
ImmutableHashSet<ListedUser>? listedUsers = await GetListedUsers().ConfigureAwait(false);
if ((listedUsers == null) || (listedUsers.Count == 0)) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(listedUsers)));
return (false, false);
}
byte maxTradeHoldDuration = ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration;
byte totalMatches = 0;
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisRound = new();
foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && (!triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) attempt) || (attempt.Tries < byte.MaxValue)) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) attempt) ? attempt.Tries : 0).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenByDescending(static listedUser => listedUser.MatchEverything || (listedUser.ItemsCount < MaxItemsForFairBots)).ThenByDescending(static listedUser => listedUser.Score)) {
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> wantedSets = ourTradableState.Keys.Where(set => !skippedSetsThisRound.Contains(set) && listedUser.MatchableTypes.Contains(set.Type)).ToHashSet();
if (wantedSets.Count == 0) {
continue;
}
if (++totalMatches > MaxMatchedBotsHard) {
break;
}
Bot.ArchiLogger.LogGenericTrace($"{listedUser.SteamID}...");
byte? holdDuration = await Bot.ArchiWebHandler.GetTradeHoldDurationForUser(listedUser.SteamID, listedUser.TradeToken).ConfigureAwait(false);
switch (holdDuration) {
case null:
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(holdDuration)));
continue;
case > 0 when holdDuration.Value > maxTradeHoldDuration:
Bot.ArchiLogger.LogGenericTrace($"{holdDuration.Value} > {maxTradeHoldDuration}");
continue;
}
HashSet<Asset> theirInventory;
try {
theirInventory = await Bot.ArchiWebHandler.GetInventoryAsync(listedUser.SteamID).Where(item => (!listedUser.MatchEverything || item.Tradable) && wantedSets.Contains((item.RealAppID, item.Type, item.Rarity)) && ((holdDuration.Value == 0) || !(item.Type is Asset.EType.FoilTradingCard or Asset.EType.TradingCard && CardsFarmer.SalesBlacklist.Contains(item.RealAppID)))).ToHashSetAsync().ConfigureAwait(false);
} catch (HttpRequestException e) {
Bot.ArchiLogger.LogGenericWarningException(e);
continue;
} catch (Exception e) {
Bot.ArchiLogger.LogGenericException(e);
continue;
}
if (theirInventory.Count == 0) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(theirInventory)));
continue;
}
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisUser = new();
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> theirTradableState = Trading.GetTradableInventoryState(theirInventory);
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> inventoryStateChanges = new();
for (byte i = 0; i < Trading.MaxTradesPerAccount; i++) {
byte itemsInTrade = 0;
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisTrade = new();
Dictionary<ulong, uint> classIDsToGive = new();
Dictionary<ulong, uint> classIDsToReceive = new();
Dictionary<ulong, uint> fairClassIDsToGive = new();
Dictionary<ulong, uint> fairClassIDsToReceive = new();
foreach (((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) set, Dictionary<ulong, uint> ourFullItems) in ourFullState.Where(set => !skippedSetsThisUser.Contains(set.Key) && listedUser.MatchableTypes.Contains(set.Key.Type) && set.Value.Values.Any(static count => count > 1))) {
if (!ourTradableState.TryGetValue(set, out Dictionary<ulong, uint>? ourTradableItems) || (ourTradableItems.Count == 0)) {
continue;
}
if (!theirTradableState.TryGetValue(set, out Dictionary<ulong, uint>? theirTradableItems) || (theirTradableItems.Count == 0)) {
continue;
}
// Those 2 collections are on user-basis since we can't be sure that the trade passes through (and therefore we need to keep original state in case of failure)
Dictionary<ulong, uint> ourFullSet = new(ourFullItems);
Dictionary<ulong, uint> ourTradableSet = new(ourTradableItems);
// We also have to take into account changes that happened in previous trades with this user, so this block will adapt to that
if (inventoryStateChanges.TryGetValue(set, out Dictionary<ulong, uint>? pastChanges) && (pastChanges.Count > 0)) {
foreach ((ulong classID, uint amount) in pastChanges) {
if (!ourFullSet.TryGetValue(classID, out uint fullAmount) || (fullAmount == 0) || (fullAmount < amount)) {
Bot.ArchiLogger.LogNullError(nameof(fullAmount));
return (false, skippedSetsThisRound.Count > 0);
}
if (fullAmount > amount) {
ourFullSet[classID] = fullAmount - amount;
} else {
ourFullSet.Remove(classID);
}
if (!ourTradableSet.TryGetValue(classID, out uint tradableAmount) || (tradableAmount == 0) || (tradableAmount < amount)) {
Bot.ArchiLogger.LogNullError(nameof(tradableAmount));
return (false, skippedSetsThisRound.Count > 0);
}
if (fullAmount > amount) {
ourTradableSet[classID] = fullAmount - amount;
} else {
ourTradableSet.Remove(classID);
}
}
if (Trading.IsEmptyForMatching(ourFullSet, ourTradableSet)) {
continue;
}
}
bool match;
do {
match = false;
foreach ((ulong ourItem, uint ourFullAmount) in ourFullSet.Where(static item => item.Value > 1).OrderByDescending(static item => item.Value)) {
if (!ourTradableSet.TryGetValue(ourItem, out uint ourTradableAmount) || (ourTradableAmount == 0)) {
continue;
}
foreach ((ulong theirItem, uint theirTradableAmount) in theirTradableItems.OrderBy(item => ourFullSet.TryGetValue(item.Key, out uint ourAmountOfTheirItem) ? ourAmountOfTheirItem : 0)) {
if (ourFullSet.TryGetValue(theirItem, out uint ourAmountOfTheirItem) && (ourFullAmount <= ourAmountOfTheirItem + 1)) {
continue;
}
if (!listedUser.MatchEverything) {
// We have a potential match, let's check fairness for them
fairClassIDsToGive.TryGetValue(ourItem, out uint fairGivenAmount);
fairClassIDsToReceive.TryGetValue(theirItem, out uint fairReceivedAmount);
fairClassIDsToGive[ourItem] = ++fairGivenAmount;
fairClassIDsToReceive[theirItem] = ++fairReceivedAmount;
// Filter their inventory for the sets we're trading or have traded with this user
HashSet<Asset> fairFiltered = theirInventory.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).Select(static item => item.CreateShallowCopy()).ToHashSet();
// Copy list to HashSet<Steam.Asset>
HashSet<Asset> fairItemsToGive = Trading.GetTradableItemsFromInventory(ourInventory.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).Select(static item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToGive.ToDictionary(static classID => classID.Key, static classID => classID.Value));
HashSet<Asset> fairItemsToReceive = Trading.GetTradableItemsFromInventory(fairFiltered.Select(static item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToReceive.ToDictionary(static classID => classID.Key, static classID => classID.Value));
// Actual check:
if (!Trading.IsTradeNeutralOrBetter(fairFiltered, fairItemsToReceive, fairItemsToGive)) {
if (fairGivenAmount > 1) {
fairClassIDsToGive[ourItem] = fairGivenAmount - 1;
} else {
fairClassIDsToGive.Remove(ourItem);
}
if (fairReceivedAmount > 1) {
fairClassIDsToReceive[theirItem] = fairReceivedAmount - 1;
} else {
fairClassIDsToReceive.Remove(theirItem);
}
continue;
}
}
// Skip this set from the remaining of this round
skippedSetsThisTrade.Add(set);
// Update our state based on given items
classIDsToGive[ourItem] = classIDsToGive.TryGetValue(ourItem, out uint ourGivenAmount) ? ourGivenAmount + 1 : 1;
ourFullSet[ourItem] = ourFullAmount - 1; // We don't need to remove anything here because we can guarantee that ourItem.Value is at least 2
if (inventoryStateChanges.TryGetValue(set, out Dictionary<ulong, uint>? currentChanges)) {
currentChanges[ourItem] = currentChanges.TryGetValue(ourItem, out uint amount) ? amount + 1 : 1;
} else {
inventoryStateChanges[set] = new Dictionary<ulong, uint> {
{ ourItem, 1 }
};
}
// Update our state based on received items
classIDsToReceive[theirItem] = classIDsToReceive.TryGetValue(theirItem, out uint ourReceivedAmount) ? ourReceivedAmount + 1 : 1;
ourFullSet[theirItem] = ourAmountOfTheirItem + 1;
if (ourTradableAmount > 1) {
ourTradableSet[ourItem] = ourTradableAmount - 1;
} else {
ourTradableSet.Remove(ourItem);
}
// Update their state based on taken items
if (theirTradableAmount > 1) {
theirTradableItems[theirItem] = theirTradableAmount - 1;
} else {
theirTradableItems.Remove(theirItem);
}
itemsInTrade += 2;
match = true;
break;
}
if (match) {
break;
}
}
} while (match && (itemsInTrade < Trading.MaxItemsPerTrade - 1));
if (itemsInTrade >= Trading.MaxItemsPerTrade - 1) {
break;
}
}
if (skippedSetsThisTrade.Count == 0) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(skippedSetsThisTrade)));
break;
}
// Remove the items from inventories
HashSet<Asset> itemsToGive = Trading.GetTradableItemsFromInventory(ourInventory, classIDsToGive);
HashSet<Asset> itemsToReceive = Trading.GetTradableItemsFromInventory(theirInventory, classIDsToReceive);
if ((itemsToGive.Count != itemsToReceive.Count) || !Trading.IsFairExchange(itemsToGive, itemsToReceive)) {
// Failsafe
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, Strings.ErrorAborted));
return (false, skippedSetsThisRound.Count > 0);
}
if (triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) previousAttempt)) {
if ((previousAttempt.GivenAssetIDs == null) || (previousAttempt.ReceivedAssetIDs == null) || (itemsToGive.Select(static item => item.AssetID).All(previousAttempt.GivenAssetIDs.Contains) && itemsToReceive.Select(static item => item.AssetID).All(previousAttempt.ReceivedAssetIDs.Contains))) {
// This user didn't respond in our previous round, avoid him for remaining ones
triedSteamIDs[listedUser.SteamID] = (byte.MaxValue, previousAttempt.GivenAssetIDs, previousAttempt.ReceivedAssetIDs);
break;
}
previousAttempt.GivenAssetIDs.UnionWith(itemsToGive.Select(static item => item.AssetID));
previousAttempt.ReceivedAssetIDs.UnionWith(itemsToReceive.Select(static item => item.AssetID));
} else {
previousAttempt.GivenAssetIDs = new HashSet<ulong>(itemsToGive.Select(static item => item.AssetID));
previousAttempt.ReceivedAssetIDs = new HashSet<ulong>(itemsToReceive.Select(static item => item.AssetID));
}
triedSteamIDs[listedUser.SteamID] = (++previousAttempt.Tries, previousAttempt.GivenAssetIDs, previousAttempt.ReceivedAssetIDs);
Bot.ArchiLogger.LogGenericTrace($"{Bot.SteamID} <- {string.Join(", ", itemsToReceive.Select(static item => $"{item.RealAppID}/{item.Type}-{item.ClassID} #{item.Amount}"))} | {string.Join(", ", itemsToGive.Select(static item => $"{item.RealAppID}/{item.Type}-{item.ClassID} #{item.Amount}"))} -> {listedUser.SteamID}");
(bool success, HashSet<ulong>? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(listedUser.SteamID, itemsToGive, itemsToReceive, listedUser.TradeToken, true).ConfigureAwait(false);
if ((mobileTradeOfferIDs?.Count > 0) && Bot.HasMobileAuthenticator) {
(bool twoFactorSuccess, _, _) = await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false);
if (!twoFactorSuccess) {
Bot.ArchiLogger.LogGenericTrace(Strings.WarningFailed);
return (false, skippedSetsThisRound.Count > 0);
}
}
if (!success) {
Bot.ArchiLogger.LogGenericTrace(Strings.WarningFailed);
break;
}
// Add itemsToGive to theirInventory to reflect their current state if we're over MaxItemsPerTrade
theirInventory.UnionWith(itemsToGive);
skippedSetsThisUser.UnionWith(skippedSetsThisTrade);
Bot.ArchiLogger.LogGenericTrace(Strings.Success);
}
if (skippedSetsThisUser.Count == 0) {
if (skippedSetsThisRound.Count == 0) {
// If we didn't find any match on clean round, this user isn't going to have anything interesting for us anytime soon
triedSteamIDs[listedUser.SteamID] = (byte.MaxValue, null, null);
}
continue;
}
skippedSetsThisRound.UnionWith(skippedSetsThisUser);
foreach ((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) skippedSet in skippedSetsThisUser) {
ourFullState.Remove(skippedSet);
ourTradableState.Remove(skippedSet);
}
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
// User doesn't have any more dupes in the inventory
break;
}
ourFullState.TrimExcess();
ourTradableState.TrimExcess();
}
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ActivelyMatchingItemsRound, skippedSetsThisRound.Count));
// Keep matching when we either traded something this round (so it makes sense for a refresh) or if we didn't try all available bots yet (so it makes sense to keep going)
return ((totalMatches > 0) && ((skippedSetsThisRound.Count > 0) || triedSteamIDs.Values.All(static data => data.Tries < 2)), skippedSetsThisRound.Count > 0);
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
private sealed class ListedUser {
#pragma warning disable CS0649 // False positive, it's a field set during json deserialization
[JsonProperty(PropertyName = "items_count", Required = Required.Always)]
internal readonly ushort ItemsCount;
#pragma warning restore CS0649 // False positive, it's a field set during json deserialization
internal readonly HashSet<Asset.EType> MatchableTypes = new();
#pragma warning disable CS0649 // False positive, it's a field set during json deserialization
[JsonProperty(PropertyName = "steam_id", Required = Required.Always)]
internal readonly ulong SteamID;
#pragma warning restore CS0649 // False positive, it's a field set during json deserialization
[JsonProperty(PropertyName = "trade_token", Required = Required.Always)]
internal readonly string TradeToken = "";
internal float Score => GamesCount / (float) ItemsCount;
#pragma warning disable CS0649 // False positive, it's a field set during json deserialization
[JsonProperty(PropertyName = "games_count", Required = Required.Always)]
private readonly ushort GamesCount;
#pragma warning restore CS0649 // False positive, it's a field set during json deserialization
internal bool MatchEverything { get; private set; }
[JsonProperty(PropertyName = "matchable_backgrounds", Required = Required.Always)]
private byte MatchableBackgroundsNumber {
set {
switch (value) {
case 0:
MatchableTypes.Remove(Asset.EType.ProfileBackground);
break;
case 1:
MatchableTypes.Add(Asset.EType.ProfileBackground);
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonProperty(PropertyName = "matchable_cards", Required = Required.Always)]
private byte MatchableCardsNumber {
set {
switch (value) {
case 0:
MatchableTypes.Remove(Asset.EType.TradingCard);
break;
case 1:
MatchableTypes.Add(Asset.EType.TradingCard);
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonProperty(PropertyName = "matchable_emoticons", Required = Required.Always)]
private byte MatchableEmoticonsNumber {
set {
switch (value) {
case 0:
MatchableTypes.Remove(Asset.EType.Emoticon);
break;
case 1:
MatchableTypes.Add(Asset.EType.Emoticon);
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonProperty(PropertyName = "matchable_foil_cards", Required = Required.Always)]
private byte MatchableFoilCardsNumber {
set {
switch (value) {
case 0:
MatchableTypes.Remove(Asset.EType.FoilTradingCard);
break;
case 1:
MatchableTypes.Add(Asset.EType.FoilTradingCard);
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonProperty(PropertyName = "match_everything", Required = Required.Always)]
private byte MatchEverythingNumber {
set {
switch (value) {
case 0:
MatchEverything = false;
break;
case 1:
MatchEverything = true;
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonConstructor]
private ListedUser() { }
}
}
}

Some files were not shown because too many files have changed in this diff Show More