mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2025-12-24 18:26:49 +00:00
Compare commits
678 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0a8f96ec4 | ||
|
|
dae6f9d328 | ||
|
|
4258fed873 | ||
|
|
ab6e0a1e1b | ||
|
|
959056523a | ||
|
|
245e3aa250 | ||
|
|
170bd9fe42 | ||
|
|
2cf84d3691 | ||
|
|
ae0ec5feee | ||
|
|
c495ad4f4a | ||
|
|
01e4085a52 | ||
|
|
e89dad5792 | ||
|
|
8c6c7a5f3c | ||
|
|
32f52e9de3 | ||
|
|
aaabd81778 | ||
|
|
24200e3490 | ||
|
|
a896075e88 | ||
|
|
1bf35d1215 | ||
|
|
641aa435be | ||
|
|
d3e48e69d4 | ||
|
|
8548044038 | ||
|
|
cdffde2d76 | ||
|
|
afd7360676 | ||
|
|
7603efb289 | ||
|
|
3ad6f68bb9 | ||
|
|
065facb5db | ||
|
|
8140784903 | ||
|
|
c468f3e4e1 | ||
|
|
174317c674 | ||
|
|
25690056da | ||
|
|
1950c1326e | ||
|
|
876074a0ed | ||
|
|
8c06051f52 | ||
|
|
b7d9c7b6da | ||
|
|
ca048912cd | ||
|
|
290aa3ba34 | ||
|
|
8620a90787 | ||
|
|
189f998faf | ||
|
|
a5640f5a84 | ||
|
|
b343d81f56 | ||
|
|
edf2a19946 | ||
|
|
7e43a05517 | ||
|
|
db8ead92a1 | ||
|
|
e33c340183 | ||
|
|
a04781747e | ||
|
|
73bae63af6 | ||
|
|
bf4bb7225c | ||
|
|
1809028c77 | ||
|
|
c4b3899ae3 | ||
|
|
7c00e725d1 | ||
|
|
73dcb34c0c | ||
|
|
65049bc2e5 | ||
|
|
b3ed87c9ef | ||
|
|
2ea5f5a83b | ||
|
|
ba1f832f54 | ||
|
|
39e7a73cd2 | ||
|
|
d803887ef9 | ||
|
|
560d2400c0 | ||
|
|
6a0cc973f3 | ||
|
|
b21742d06e | ||
|
|
b76454ecfa | ||
|
|
376899ebe2 | ||
|
|
547bb13894 | ||
|
|
c7792c8a1c | ||
|
|
b67f92cc21 | ||
|
|
7ad05e1703 | ||
|
|
1ba2880071 | ||
|
|
fd05a2cab6 | ||
|
|
cd22d365ea | ||
|
|
6196fc175e | ||
|
|
cd5835bdcb | ||
|
|
07a7358493 | ||
|
|
475b8aa649 | ||
|
|
141c8835d0 | ||
|
|
6b498af3c9 | ||
|
|
640a794a3e | ||
|
|
82cea76901 | ||
|
|
ffccb98d79 | ||
|
|
31bf21973b | ||
|
|
7dbb8e23b0 | ||
|
|
43d1ccfb0e | ||
|
|
e2ff80cc46 | ||
|
|
457bacfef8 | ||
|
|
44f9f12263 | ||
|
|
80c2091e34 | ||
|
|
9e522e7196 | ||
|
|
335760c0bb | ||
|
|
e856ce8177 | ||
|
|
16f02740d8 | ||
|
|
7f5ada6dce | ||
|
|
65018efa7f | ||
|
|
3b87713fff | ||
|
|
d141dce93d | ||
|
|
81a92d6781 | ||
|
|
f3d491611a | ||
|
|
332d5d048c | ||
|
|
11f8b6aae5 | ||
|
|
6e5a02c380 | ||
|
|
22bbfe4e24 | ||
|
|
a2c278947d | ||
|
|
5db90e0eb8 | ||
|
|
1914f41ffe | ||
|
|
4754b3cbd9 | ||
|
|
31acd4e7dc | ||
|
|
f98d33bfa5 | ||
|
|
799b48d1b6 | ||
|
|
6444167ae4 | ||
|
|
543d03724d | ||
|
|
cdd4ff9128 | ||
|
|
004c72127c | ||
|
|
c08b2609fc | ||
|
|
eedb39e8df | ||
|
|
379b9454ec | ||
|
|
02d0610a04 | ||
|
|
f63723a157 | ||
|
|
d59bccf1db | ||
|
|
2a734344bc | ||
|
|
eb8946e480 | ||
|
|
55745c8093 | ||
|
|
5a5a573e46 | ||
|
|
dc6968b371 | ||
|
|
692a0e0c9d | ||
|
|
c5839d3cbe | ||
|
|
bc3275fa9d | ||
|
|
4c70a71072 | ||
|
|
fd2b9ff8d2 | ||
|
|
407b77428a | ||
|
|
494dd69819 | ||
|
|
71f4e16603 | ||
|
|
1c0995426c | ||
|
|
82702647b4 | ||
|
|
b826a64f88 | ||
|
|
d20c3257ed | ||
|
|
06622263c0 | ||
|
|
73b3fe4c8a | ||
|
|
429b030021 | ||
|
|
d60b932dfa | ||
|
|
5229f52f47 | ||
|
|
92a946c1cb | ||
|
|
03fc35dad0 | ||
|
|
225003c5d1 | ||
|
|
4f598d5c8f | ||
|
|
944df1cfc8 | ||
|
|
e259f9e32f | ||
|
|
c2cabfba49 | ||
|
|
dee8add183 | ||
|
|
06829beda4 | ||
|
|
2a35c82e0c | ||
|
|
dbd8fa9877 | ||
|
|
d8a0d2f22d | ||
|
|
88996d1e35 | ||
|
|
78a88979dc | ||
|
|
2513bd4163 | ||
|
|
d9a5c30659 | ||
|
|
c60ea2ba3d | ||
|
|
7a07b6a22b | ||
|
|
d89b6112dd | ||
|
|
5d33bca611 | ||
|
|
0f489f55e4 | ||
|
|
bf70f27449 | ||
|
|
0eab358af9 | ||
|
|
3eae143c55 | ||
|
|
a33d46c85b | ||
|
|
4aa524f03e | ||
|
|
861e7ded16 | ||
|
|
581d5167b9 | ||
|
|
b9108742d4 | ||
|
|
86c19a2dce | ||
|
|
f64abc02ab | ||
|
|
d6e569c970 | ||
|
|
a75d63cd7f | ||
|
|
8bfc48d8dc | ||
|
|
3de4069e3f | ||
|
|
874eb2d1c6 | ||
|
|
803d4554aa | ||
|
|
549ddb4271 | ||
|
|
9017c3970d | ||
|
|
1eecd8ace0 | ||
|
|
31da584f75 | ||
|
|
1b1cdb8c3e | ||
|
|
37e7f9f51c | ||
|
|
fc9dda13a0 | ||
|
|
94c214af96 | ||
|
|
a184fc555b | ||
|
|
ad2dae4faf | ||
|
|
aaf9cc67b3 | ||
|
|
fe866554d6 | ||
|
|
97200da414 | ||
|
|
876c332452 | ||
|
|
75bc0ed598 | ||
|
|
bcbc44cb1f | ||
|
|
db8b23031a | ||
|
|
586ad7c370 | ||
|
|
86867c8d99 | ||
|
|
6a824c2c6f | ||
|
|
ac02495e80 | ||
|
|
67c5e1f7c4 | ||
|
|
85437774de | ||
|
|
8cb813a354 | ||
|
|
d64669d563 | ||
|
|
a049bf39d6 | ||
|
|
c191a85966 | ||
|
|
a5cd6314e4 | ||
|
|
90bf83cf48 | ||
|
|
d5233c52af | ||
|
|
f97dc5f512 | ||
|
|
844de630a6 | ||
|
|
b83c06aec9 | ||
|
|
9f1f8a1daf | ||
|
|
ab982604cf | ||
|
|
b00e157349 | ||
|
|
4537571014 | ||
|
|
a1df1ed446 | ||
|
|
cc99e9844c | ||
|
|
c88a79327e | ||
|
|
d75b5194bb | ||
|
|
aada326d3a | ||
|
|
1c9f50ab62 | ||
|
|
e68210cf2e | ||
|
|
b030755eb6 | ||
|
|
b64ad59eff | ||
|
|
8b0e71e72d | ||
|
|
18f62b714d | ||
|
|
8f233acd32 | ||
|
|
5ba0ad8eed | ||
|
|
53c88725aa | ||
|
|
24bc249f64 | ||
|
|
8712b01137 | ||
|
|
3d1eab828b | ||
|
|
f0e213476d | ||
|
|
958c6bb704 | ||
|
|
d3737f0705 | ||
|
|
6497e2f3e9 | ||
|
|
da21fb355e | ||
|
|
30d448110f | ||
|
|
ece06abbc7 | ||
|
|
07348a5958 | ||
|
|
0db5d115db | ||
|
|
acfa02631d | ||
|
|
eb2c728361 | ||
|
|
a30c091387 | ||
|
|
5a9d4d3f70 | ||
|
|
cdd35ad29d | ||
|
|
035d4b9ed8 | ||
|
|
1f72a09282 | ||
|
|
0673b2e298 | ||
|
|
666a04a8a8 | ||
|
|
efd5079c32 | ||
|
|
7892f110ea | ||
|
|
7b51cca934 | ||
|
|
c709d529c1 | ||
|
|
99569ee3fe | ||
|
|
b7aee818b4 | ||
|
|
7373d6148b | ||
|
|
2c2a2016f6 | ||
|
|
e4b82a7714 | ||
|
|
67bc0b4eda | ||
|
|
62effc4af1 | ||
|
|
19f63c94bf | ||
|
|
f6ede9b949 | ||
|
|
cfacceddf9 | ||
|
|
f327409184 | ||
|
|
1094049986 | ||
|
|
93b6ffdc23 | ||
|
|
f6033fb5cd | ||
|
|
e8d0e870b9 | ||
|
|
7db932ecb7 | ||
|
|
941b704c41 | ||
|
|
9575b58258 | ||
|
|
89a50674ec | ||
|
|
f36e5618a4 | ||
|
|
fefcf12f2f | ||
|
|
ff85a88b42 | ||
|
|
c01a2ba863 | ||
|
|
66344a1a3d | ||
|
|
260875da7e | ||
|
|
951d9dc99f | ||
|
|
0c8d77b3d9 | ||
|
|
f5f5c810dc | ||
|
|
8e045fdf71 | ||
|
|
71089a4953 | ||
|
|
d1fc7ebb74 | ||
|
|
60376c4d93 | ||
|
|
ff8074aeb6 | ||
|
|
a9249a90f6 | ||
|
|
cc85b681f7 | ||
|
|
d1e8794fe3 | ||
|
|
258ad17930 | ||
|
|
52b32315cc | ||
|
|
d46e532458 | ||
|
|
1e6ab11d9f | ||
|
|
95ad16e26d | ||
|
|
7019445b84 | ||
|
|
0850a261cb | ||
|
|
ae3a60759a | ||
|
|
566be6e8c4 | ||
|
|
9aaf8d8215 | ||
|
|
0964cdac96 | ||
|
|
e62234892a | ||
|
|
55e3c064eb | ||
|
|
32575e69ec | ||
|
|
5cdeccc2ba | ||
|
|
695ffb4b1c | ||
|
|
0977b359c2 | ||
|
|
c7443bef69 | ||
|
|
e238026121 | ||
|
|
36d51e80d7 | ||
|
|
a4b5c9a6cc | ||
|
|
b305af6f7f | ||
|
|
9a42a08624 | ||
|
|
3eeafa9029 | ||
|
|
fb4eb0b03a | ||
|
|
41bfd8ad2b | ||
|
|
4a88549b76 | ||
|
|
f5b548f83b | ||
|
|
35ca44ee01 | ||
|
|
4929d35f11 | ||
|
|
0eee21360d | ||
|
|
fd93f0cd03 | ||
|
|
7e715e3478 | ||
|
|
417440340a | ||
|
|
e9898a53a1 | ||
|
|
5200fa771c | ||
|
|
f078bacd96 | ||
|
|
f3919651d1 | ||
|
|
63d6e33204 | ||
|
|
6ad64f405c | ||
|
|
62704be1f3 | ||
|
|
6d9dc5e54f | ||
|
|
be2bdf0727 | ||
|
|
d323918441 | ||
|
|
7104c34d2e | ||
|
|
b3fd8a21f4 | ||
|
|
2909d4320a | ||
|
|
1886a32ab5 | ||
|
|
b516980ad7 | ||
|
|
e59b45e63c | ||
|
|
cc59875769 | ||
|
|
8699be4c25 | ||
|
|
13b28c38e3 | ||
|
|
3b5a07aab3 | ||
|
|
a77f0c1ad3 | ||
|
|
b0b7184be5 | ||
|
|
8533b79c22 | ||
|
|
b1b7a54f5d | ||
|
|
6aba37d131 | ||
|
|
392fdcb633 | ||
|
|
e0e015255b | ||
|
|
02af5d21d5 | ||
|
|
775c137a2d | ||
|
|
3d7efcdcec | ||
|
|
fbbb50656d | ||
|
|
1c8b759624 | ||
|
|
a8f22e2833 | ||
|
|
04ab9a8e29 | ||
|
|
b175a088b9 | ||
|
|
9aaa4caed4 | ||
|
|
1cc49ba25c | ||
|
|
28d0068fdb | ||
|
|
785b43781a | ||
|
|
148472eda4 | ||
|
|
4268aac82f | ||
|
|
f13ec6a227 | ||
|
|
9265aee77c | ||
|
|
f5613d5967 | ||
|
|
4c52c43526 | ||
|
|
d894f3a830 | ||
|
|
ed0cb38147 | ||
|
|
8e289e38b3 | ||
|
|
96fabd986c | ||
|
|
eb876aa4d1 | ||
|
|
e874213d49 | ||
|
|
d340dc57f3 | ||
|
|
df76864996 | ||
|
|
d40132af4a | ||
|
|
03f3943f42 | ||
|
|
7a6018e011 | ||
|
|
f76771e1b6 | ||
|
|
bc15da5cce | ||
|
|
67ddef7fa1 | ||
|
|
9e3c50cd50 | ||
|
|
857e231f96 | ||
|
|
4a83ed5ccd | ||
|
|
00c6812e00 | ||
|
|
44503e82c6 | ||
|
|
2d45de06c4 | ||
|
|
aeb85afb84 | ||
|
|
1f4967653c | ||
|
|
731791e810 | ||
|
|
faa579535f | ||
|
|
08cc46c7ee | ||
|
|
3051f30ab3 | ||
|
|
85766e5279 | ||
|
|
618ff781eb | ||
|
|
f022822e94 | ||
|
|
095859a727 | ||
|
|
47696cc9a7 | ||
|
|
8580852f84 | ||
|
|
e702fca8bb | ||
|
|
bfbeb91633 | ||
|
|
cfe88d59ec | ||
|
|
6f8ea30a90 | ||
|
|
f447b7a0ed | ||
|
|
6450a11302 | ||
|
|
861d64e0dc | ||
|
|
ead9da524d | ||
|
|
e883937056 | ||
|
|
2db957aec4 | ||
|
|
14052d9bbf | ||
|
|
002be87f24 | ||
|
|
d7e8f2785d | ||
|
|
d5489df46d | ||
|
|
df817f4ffc | ||
|
|
f93143c133 | ||
|
|
aedc9f0b21 | ||
|
|
8d145dd98b | ||
|
|
8eade49343 | ||
|
|
14bab623b8 | ||
|
|
1456efc341 | ||
|
|
fc0c916137 | ||
|
|
be027523ac | ||
|
|
5af5e55cfe | ||
|
|
9b8050e2b3 | ||
|
|
0ff0fa8d95 | ||
|
|
ff870d25cf | ||
|
|
818ad8cc09 | ||
|
|
618a01912b | ||
|
|
f3ffa792ea | ||
|
|
976276a74a | ||
|
|
86173f6022 | ||
|
|
ed033349ec | ||
|
|
c69a650bba | ||
|
|
129cba80d6 | ||
|
|
8c1bff3d91 | ||
|
|
da6d2e1b13 | ||
|
|
62d58a41a3 | ||
|
|
b48e73d939 | ||
|
|
454e9cdef4 | ||
|
|
78feda15ba | ||
|
|
1e5df17130 | ||
|
|
fdac946ecc | ||
|
|
be18666764 | ||
|
|
b2f99c56cf | ||
|
|
11ff50048e | ||
|
|
c6872607c4 | ||
|
|
ff2c5af492 | ||
|
|
cb2413a4d7 | ||
|
|
28c575a8a5 | ||
|
|
100f55fd58 | ||
|
|
2910d437d1 | ||
|
|
09d4dcbef6 | ||
|
|
beaa383bb5 | ||
|
|
4a24872dac | ||
|
|
a7f414c0c7 | ||
|
|
965d3050a5 | ||
|
|
99205fa278 | ||
|
|
1edaf69b2f | ||
|
|
77675fb0e1 | ||
|
|
cd7736196f | ||
|
|
24c5aa1a92 | ||
|
|
7e9e90764b | ||
|
|
f2d3a2a894 | ||
|
|
de19010820 | ||
|
|
e79f41be42 | ||
|
|
196a056f06 | ||
|
|
eff08956bc | ||
|
|
79fb4da9a6 | ||
|
|
13996748e5 | ||
|
|
40610d8af4 | ||
|
|
7993e608b1 | ||
|
|
032dd6333d | ||
|
|
1bfedbe5e8 | ||
|
|
300c1c19ec | ||
|
|
c7fd08273b | ||
|
|
75a31410c0 | ||
|
|
49173a4b26 | ||
|
|
ea706b57e8 | ||
|
|
eeb1105aa7 | ||
|
|
d8975235d0 | ||
|
|
7268fad3d0 | ||
|
|
de399d9bfe | ||
|
|
383868351d | ||
|
|
93fc34a2fe | ||
|
|
a8ed029ed1 | ||
|
|
24728bc6ea | ||
|
|
832d9e7be6 | ||
|
|
1bc964b8d1 | ||
|
|
38e2bd10cd | ||
|
|
4407c78268 | ||
|
|
7fb5a470f0 | ||
|
|
4ad81586df | ||
|
|
c60d413a70 | ||
|
|
86ceaf2369 | ||
|
|
98721803eb | ||
|
|
6aaec3ce8f | ||
|
|
f7f264466b | ||
|
|
023d01c7b8 | ||
|
|
27f14802c4 | ||
|
|
8e3c341910 | ||
|
|
d8384d2232 | ||
|
|
e90100a847 | ||
|
|
acff71c42c | ||
|
|
6a22332cc8 | ||
|
|
f0b7e46595 | ||
|
|
c17074e98f | ||
|
|
9c0582452f | ||
|
|
61d591ea3a | ||
|
|
6e3e02c359 | ||
|
|
7c7c9750a6 | ||
|
|
c8c96fd50d | ||
|
|
668d4457e7 | ||
|
|
73367bb4fc | ||
|
|
8fe6d98883 | ||
|
|
a1d0a84bac | ||
|
|
0a3ae316da | ||
|
|
a72d12ef75 | ||
|
|
ad6ce38352 | ||
|
|
63d25d06aa | ||
|
|
91a0bce535 | ||
|
|
d8838c4c80 | ||
|
|
d9f5fe260f | ||
|
|
de60a0ce72 | ||
|
|
766b75321b | ||
|
|
8d0977d61e | ||
|
|
10deb785d5 | ||
|
|
d21489ff8f | ||
|
|
8e99f579d7 | ||
|
|
e90d002e77 | ||
|
|
604f76695a | ||
|
|
0f0974a0c0 | ||
|
|
4935ec3187 | ||
|
|
ca03d68eb5 | ||
|
|
8a7d0515e1 | ||
|
|
2808c5abe9 | ||
|
|
4502b66544 | ||
|
|
2788df033b | ||
|
|
3545aae09c | ||
|
|
64c371e122 | ||
|
|
3cdb4129ba | ||
|
|
20e6ba753d | ||
|
|
eb74265f2c | ||
|
|
84d4fd422a | ||
|
|
c08c9b2585 | ||
|
|
b32548d366 | ||
|
|
c62582dc4a | ||
|
|
f9391e493b | ||
|
|
e78fb5b1ec | ||
|
|
fbf8af754d | ||
|
|
4982bdbf12 | ||
|
|
bfbe794ba2 | ||
|
|
541ee89126 | ||
|
|
7ad756fbdc | ||
|
|
7cabbe090c | ||
|
|
27c345a002 | ||
|
|
1fc822a985 | ||
|
|
90ea23ea8b | ||
|
|
1f556f9264 | ||
|
|
01a7395105 | ||
|
|
9f26e72843 | ||
|
|
199e0e132d | ||
|
|
2f5d4cff50 | ||
|
|
79c510cb92 | ||
|
|
ba50dd818d | ||
|
|
3f426546c2 | ||
|
|
69e2a3590c | ||
|
|
47855ca705 | ||
|
|
5f5dcfbb99 | ||
|
|
882443711b | ||
|
|
e6a520b9f0 | ||
|
|
2e1694c65d | ||
|
|
a48b594398 | ||
|
|
9ddc0da7a0 | ||
|
|
a7da072104 | ||
|
|
03a962ae81 | ||
|
|
3b5b00f5ee | ||
|
|
3dec189e14 | ||
|
|
9fa55f31f9 | ||
|
|
2e03231ffc | ||
|
|
06b11a3129 | ||
|
|
a2ffe2cbcd | ||
|
|
1216d83d51 | ||
|
|
f749998b04 | ||
|
|
37e8bdb9ee | ||
|
|
799f14fe5c | ||
|
|
a8f0456f1f | ||
|
|
a046f1c531 | ||
|
|
6d18ea9c90 | ||
|
|
9c168fa0ba | ||
|
|
cf9b90a73c | ||
|
|
31db8c2458 | ||
|
|
5857c76b52 | ||
|
|
b456796d82 | ||
|
|
a3639c6d45 | ||
|
|
dff549bd27 | ||
|
|
648691a3eb | ||
|
|
926cbe7222 | ||
|
|
bb513921f1 | ||
|
|
1fda77a72d | ||
|
|
d388aa733a | ||
|
|
75ffee3dd0 | ||
|
|
9d31765834 | ||
|
|
64efae942f | ||
|
|
4dcd55d300 | ||
|
|
5f81c6fdfa | ||
|
|
4fd411fd56 | ||
|
|
e3711dde26 | ||
|
|
19c66a019d | ||
|
|
f9cf549dfe | ||
|
|
ae3010640e | ||
|
|
873c9e1d93 | ||
|
|
c78094ec73 | ||
|
|
b3d1f96824 | ||
|
|
de0ee3a497 | ||
|
|
350ef8c626 | ||
|
|
8b84c3c934 | ||
|
|
433732cd3b | ||
|
|
cd716f4cdf | ||
|
|
1bf3830973 | ||
|
|
f403e9c296 | ||
|
|
ccd54413c0 | ||
|
|
4e39cd629a | ||
|
|
0bf8fe7296 | ||
|
|
2c390908b3 | ||
|
|
443a1cb18d | ||
|
|
9d6eaca298 | ||
|
|
46b2376849 | ||
|
|
47c1b8fadd | ||
|
|
3702279786 | ||
|
|
23162ce5fd | ||
|
|
436003546e | ||
|
|
911680d606 | ||
|
|
9f281c6055 | ||
|
|
b47f290512 | ||
|
|
dbe61e98ce | ||
|
|
b741f49372 | ||
|
|
68968c99ae | ||
|
|
a9fd774988 | ||
|
|
4eb4bc98ed | ||
|
|
aa7fce300d | ||
|
|
444c10b246 | ||
|
|
5a6e3ed243 | ||
|
|
3b3ae1cf7c | ||
|
|
8cd57efe33 | ||
|
|
0cae6ab482 | ||
|
|
92fdeeb6a4 | ||
|
|
5c5225f970 | ||
|
|
d87d700ae2 | ||
|
|
d3037fd981 | ||
|
|
953c7da7c2 | ||
|
|
4a4ebc7721 | ||
|
|
e2d16f3978 | ||
|
|
6d18791e90 | ||
|
|
a6e0a155d2 | ||
|
|
a1955bc881 | ||
|
|
93a8af71e9 | ||
|
|
929d57709a | ||
|
|
f31da41ed8 | ||
|
|
ab7859c619 | ||
|
|
208a4c048b | ||
|
|
4b88fe92aa | ||
|
|
09201da6ec | ||
|
|
a83e3fa71a | ||
|
|
6435b873af | ||
|
|
4b967ec7ba | ||
|
|
4b2e5f3178 | ||
|
|
6ace2107a7 | ||
|
|
d50e848f84 | ||
|
|
d1f9ed6fec | ||
|
|
da194283ed | ||
|
|
a40bb74d35 | ||
|
|
4bdbbedfb1 | ||
|
|
7971585a32 | ||
|
|
4ac2774eaf | ||
|
|
b853bf0cf6 | ||
|
|
b3f490c328 | ||
|
|
61ba422729 | ||
|
|
f087e0a9f9 | ||
|
|
04732ce37d |
@@ -7,9 +7,6 @@
|
||||
# Ignore all files in custom in-tree config directory (if exists)
|
||||
ArchiSteamFarm/config
|
||||
|
||||
# Ignore private SNK key (if exists)
|
||||
resources/ArchiSteamFarm.snk
|
||||
|
||||
# Ignore local log + debug of development builds
|
||||
ArchiSteamFarm/log.txt
|
||||
ArchiSteamFarm/debug
|
||||
@@ -41,6 +38,7 @@ wiki
|
||||
# |_____||_||_| |_| \__,_|/_/\_\
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/Global/Linux.gitignore
|
||||
# 4f7062e132d7f88e68ab737e64fef872bd3a491f
|
||||
|
||||
*~
|
||||
|
||||
@@ -63,6 +61,7 @@ wiki
|
||||
# |_| |_| |_| \__,_| \___| \___/ |____/
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
|
||||
# 2bb963b16a1957c865335e53537036c2e97399b5
|
||||
|
||||
# General
|
||||
.DS_Store
|
||||
@@ -99,6 +98,7 @@ Temporary Items
|
||||
# |_|
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/Global/MonoDevelop.gitignore
|
||||
# e8b2e1a9cc7c9ca49bb05c20a4c4491b85feba6d
|
||||
|
||||
#User Specific
|
||||
*.userprefs
|
||||
@@ -116,6 +116,7 @@ test-results/
|
||||
# \_/ |_||___/ \__,_| \__,_||_||____/ \__| \__,_| \__,_||_| \___/
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
# 888439ee893d0097862f1d510585bd0e3cfd500f
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
@@ -513,6 +514,7 @@ FodyWeavers.xsd
|
||||
# \_/\_/ |_||_| |_| \__,_| \___/ \_/\_/ |___/
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
|
||||
# 5808b77453dec299d4daf8557b05a80be832a5b8
|
||||
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
|
||||
@@ -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
|
||||
@@ -90,7 +91,7 @@ csharp_style_prefer_switch_expression = true:warning
|
||||
|
||||
csharp_style_throw_expression = true:warning
|
||||
|
||||
csharp_style_unused_value_assignment_preference = discard_variable :warning
|
||||
csharp_style_unused_value_assignment_preference = discard_variable:warning
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable:warning
|
||||
|
||||
csharp_style_var_elsewhere = false:warning
|
||||
@@ -159,7 +160,7 @@ dotnet_naming_style.t_pascal_case.required_prefix = T
|
||||
|
||||
# Symbol - almost everything
|
||||
dotnet_naming_symbols.almost_everything.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.almost_everything.applicable_kinds = namespace,class,struct,property,method,field,event,delegate
|
||||
dotnet_naming_symbols.almost_everything.applicable_kinds = namespace, class, struct, property, method, field, event, delegate
|
||||
|
||||
# Symbol - enums
|
||||
dotnet_naming_symbols.enums.applicable_accessibilities = *
|
||||
@@ -171,7 +172,7 @@ dotnet_naming_symbols.interfaces.applicable_kinds = interface
|
||||
|
||||
# Symbol - local parameters
|
||||
dotnet_naming_symbols.local_parameters.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.local_parameters.applicable_kinds = parameter,local,local_function
|
||||
dotnet_naming_symbols.local_parameters.applicable_kinds = parameter, local, local_function
|
||||
|
||||
# Symbol - type parameters
|
||||
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
|
||||
@@ -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
|
||||
|
||||
76
.github/ISSUE_TEMPLATE/Bug-report.yml
vendored
76
.github/ISSUE_TEMPLATE/Bug-report.yml
vendored
@@ -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
|
||||
@@ -38,11 +38,15 @@ body:
|
||||
label: ASF variant
|
||||
description: If you're using a different variant, ensure that your bug report is reproducible on one of the below.
|
||||
options:
|
||||
- docker-linux/amd64
|
||||
- docker-linux/arm/v7
|
||||
- docker-linux/arm64
|
||||
- generic (with latest .NET runtime)
|
||||
- generic-netf (with latest Mono runtime)
|
||||
- linux-arm
|
||||
- linux-arm64
|
||||
- linux-x64
|
||||
- osx-arm64
|
||||
- osx-x64
|
||||
- win-x64
|
||||
validations:
|
||||
@@ -52,6 +56,8 @@ body:
|
||||
attributes:
|
||||
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 endpoint, but upon trying the program returned HTTP error: 418 I'm a teapot.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -59,6 +65,8 @@ body:
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: What did you expect to happen?
|
||||
placeholder: |
|
||||
I expected my favourite latte macchiato in a cup put below the machine hosting ASF.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -66,26 +74,37 @@ body:
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: What happened instead?
|
||||
placeholder: |
|
||||
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.
|
||||
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
|
||||
@@ -96,9 +115,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
|
||||
@@ -106,9 +140,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)
|
||||
@@ -118,14 +152,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)
|
||||
@@ -136,6 +178,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
|
||||
|
||||
25
.github/ISSUE_TEMPLATE/Enhancement-idea.yml
vendored
25
.github/ISSUE_TEMPLATE/Enhancement-idea.yml
vendored
@@ -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
|
||||
|
||||
24
.github/ISSUE_TEMPLATE/Wiki-suggestion.yml
vendored
24
.github/ISSUE_TEMPLATE/Wiki-suggestion.yml
vendored
@@ -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
|
||||
|
||||
|
||||
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
## Checklist
|
||||
|
||||
<!-- Put an `x` in all the boxes that apply -->
|
||||
|
||||
- [ ] I read and understood the **[Contributing Guidelines](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/CONTRIBUTING.md)**.
|
||||
- [ ] This is not a **[duplicate](https://github.com/JustArchiNET/ArchiSteamFarm/pulls)** of an existing merge request.
|
||||
- [ ] I believe this falls into the scope of the project and should be part of the built-in functionality.
|
||||
- [ ] My code follows the **[code style](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/CONTRIBUTING.md#code-style)** of this project.
|
||||
- [ ] I have added tests to cover my changes, wherever they are necessary.
|
||||
- [ ] All new and existing tests pass.
|
||||
|
||||
## Changes
|
||||
|
||||
### New functionality
|
||||
|
||||
<!-- Please describe below, what new functionality was added. -->
|
||||
|
||||
### Changed functionality
|
||||
|
||||
<!-- Please describe below, what old functionality was changed. -->
|
||||
|
||||
### Removed functionality
|
||||
|
||||
<!-- Please describe below, what old functionality was removed. Make sure to mention what it was replaced with or how everything that was previously achievable still is. -->
|
||||
|
||||
## Additional info
|
||||
|
||||
<!-- Everything else you consider note-worthy that we didn't ask for. -->
|
||||
2
.github/RELEASE_TEMPLATE.md
vendored
2
.github/RELEASE_TEMPLATE.md
vendored
@@ -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!
|
||||
|
||||
[](https://github.com/sponsors/JustArchi) [](https://www.patreon.com/JustArchi) [](https://commerce.coinbase.com/checkout/0c23b844-c51b-45f4-9135-8db7c6fcf98e) [](https://paypal.me/JustArchi) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HD2P2P3WGS5Y4) [](https://pay.revolut.com/profile/ukaszyxm) [](https://steamcommunity.com/tradeoffer/new/?partner=46697991&token=0ix2Ruv_)
|
||||
|
||||
2
.github/SUPPORT.md
vendored
2
.github/SUPPORT.md
vendored
@@ -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 to GitHub if deemed appropriate. Invalid GitHub issues will be closed immediately and won't be answered.
|
||||
|
||||
276
.github/appveyor.yml
vendored
276
.github/appveyor.yml
vendored
@@ -1,276 +0,0 @@
|
||||
version: '{build}-{branch}'
|
||||
pull_requests:
|
||||
do_not_increment_build_number: true
|
||||
skip_branch_with_pr: true
|
||||
image: Visual Studio 2019
|
||||
configuration: Release
|
||||
clone_depth: 10
|
||||
environment:
|
||||
DOTNET_CHANNEL: 5.0
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_INSTALL_DIR: C:\Program Files\dotnet
|
||||
DOTNET_NOLOGO: true
|
||||
# DOTNET_SDK: 5.0.103
|
||||
NET_CORE_VERSION: net5.0
|
||||
NET_FRAMEWORK_VERSION: net48
|
||||
NODE_JS_VERSION: lts
|
||||
STEAM_TOKEN_DUMPER_NAME: ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
|
||||
STEAM_TOKEN_DUMPER_TOKEN:
|
||||
secure: uttQUE9ZK7BIa9SIbDkpUTMx7Slnl3zAPkRNzE465YgwxLdLEwv6yYR5QXCSZolb5Qq23Z/LmZNGd3M6B0+hbx3waWOeW2AiWvfCcnUmuT+3wfLJsgLbf1g4agFS7zsDgeRPfnNMzOxD8etelnA5YOOUMNB3RLw3fIdznNd+Fs6R0Ou3/1UavDuHKkbh1+A5
|
||||
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!
|
||||
matrix:
|
||||
allow_failures:
|
||||
- image: Visual Studio 2019 Preview
|
||||
fast_finish: true
|
||||
install:
|
||||
- pwsh: >-
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
|
||||
git submodule sync --recursive
|
||||
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
|
||||
git submodule update --init --recursive
|
||||
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
|
||||
if ($env:DOTNET_CHANNEL) {
|
||||
dotnet --info
|
||||
|
||||
try {
|
||||
&([scriptblock]::Create((Invoke-WebRequest 'https://dot.net/v1/dotnet-install.ps1'))) -Channel "$env:DOTNET_CHANNEL" -InstallDir "$env:DOTNET_INSTALL_DIR" -NoPath
|
||||
} catch [System.Net.WebException],[System.IO.IOException] {
|
||||
# Not fatal for the remaining part of the script
|
||||
Write-Host $_
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($env:DOTNET_SDK) {
|
||||
dotnet --info
|
||||
|
||||
try {
|
||||
&([scriptblock]::Create((Invoke-WebRequest 'https://dot.net/v1/dotnet-install.ps1'))) -Channel 'Current' -Version "$env:DOTNET_SDK" -InstallDir "$env:DOTNET_INSTALL_DIR" -NoPath
|
||||
} catch [System.Net.WebException],[System.IO.IOException] {
|
||||
# Not fatal for the remaining part of the script
|
||||
Write-Host $_
|
||||
}
|
||||
}
|
||||
- ps: Install-Product node "$env:NODE_JS_VERSION"
|
||||
before_build:
|
||||
- pwsh: >-
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
|
||||
dotnet --info
|
||||
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
|
||||
node -v
|
||||
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
|
||||
npm -v
|
||||
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
build_script:
|
||||
- pwsh: >-
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
|
||||
npm ci --no-progress --prefix ASF-ui
|
||||
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
|
||||
npm run-script deploy --no-progress --prefix ASF-ui
|
||||
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
|
||||
dotnet build -c "$env:CONFIGURATION" -p:ContinuousIntegrationBuild=true -p:UseAppHost=false --nologo
|
||||
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
test_script:
|
||||
- pwsh: >-
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
|
||||
dotnet test ArchiSteamFarm.Tests -c "$env:CONFIGURATION" -p:ContinuousIntegrationBuild=true -p:UseAppHost=false --nologo
|
||||
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
after_test:
|
||||
- pwsh: >-
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
|
||||
if ((Test-Path env:STEAM_TOKEN_DUMPER_TOKEN) -and (Test-Path "$env:STEAM_TOKEN_DUMPER_NAME\SharedInfo.cs" -PathType Leaf)) {
|
||||
(Get-Content "$env:STEAM_TOKEN_DUMPER_NAME\SharedInfo.cs").Replace('STEAM_TOKEN_DUMPER_TOKEN', "$env:STEAM_TOKEN_DUMPER_TOKEN") | Set-Content "$env:STEAM_TOKEN_DUMPER_NAME\SharedInfo.cs"
|
||||
}
|
||||
|
||||
|
||||
dotnet publish "$env:STEAM_TOKEN_DUMPER_NAME" -c "$env:CONFIGURATION" -f "$env:NET_CORE_VERSION" -o "out/$env:STEAM_TOKEN_DUMPER_NAME/$env:NET_CORE_VERSION" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --nologo
|
||||
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
|
||||
dotnet publish "$env:STEAM_TOKEN_DUMPER_NAME" -c "$env:CONFIGURATION" -f "$env:NET_FRAMEWORK_VERSION" -o "out/$env:STEAM_TOKEN_DUMPER_NAME/$env:NET_FRAMEWORK_VERSION" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --nologo
|
||||
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
|
||||
dotnet restore ArchiSteamFarm -p:ContinuousIntegrationBuild=true
|
||||
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
|
||||
$PublishBlock = {
|
||||
param($variant)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
Set-Location "$env:APPVEYOR_BUILD_FOLDER"
|
||||
|
||||
if ($variant -like '*-netf') {
|
||||
$targetFramework = $env:NET_FRAMEWORK_VERSION
|
||||
} else {
|
||||
$targetFramework = $env:NET_CORE_VERSION
|
||||
}
|
||||
|
||||
if ($variant -like 'generic*') {
|
||||
$variantArgs = '-p:TargetLatestRuntimePatch=false', '-p:UseAppHost=false'
|
||||
} else {
|
||||
$variantArgs = '-p:PublishSingleFile=true', '-p:PublishTrimmed=true', '-r', "$variant"
|
||||
}
|
||||
|
||||
dotnet publish ArchiSteamFarm -c "$env:CONFIGURATION" -f "$targetFramework" -o "out\$variant" "-p:ASFVariant=$variant" -p:ContinuousIntegrationBuild=true --no-restore --nologo $variantArgs
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
# If we're including any overlay for this variant, copy it to output directory
|
||||
if (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)) {
|
||||
New-Item -ItemType Directory -Path "out\$variant\plugins\$env:STEAM_TOKEN_DUMPER_NAME" > $null
|
||||
}
|
||||
|
||||
Copy-Item "out\$env:STEAM_TOKEN_DUMPER_NAME\$targetFramework\*" "out\$variant\plugins\$env:STEAM_TOKEN_DUMPER_NAME" -Recurse
|
||||
}
|
||||
|
||||
# Icon is available only in .NET Framework and .NET Core Windows build, we'll bundle the .ico file for other flavours
|
||||
if (($targetFramework -eq "$env:NET_CORE_VERSION") -and !(Test-Path "out\$variant\ArchiSteamFarm.exe" -PathType Leaf)) {
|
||||
Copy-Item 'resources\ASF.ico' "out\$variant\ArchiSteamFarm.ico"
|
||||
}
|
||||
|
||||
# By default use fastest compression
|
||||
$compressionArgs = '-mx=1'
|
||||
|
||||
# Include extra logic for builds marked for release
|
||||
if ($env:APPVEYOR_REPO_TAG -eq 'true') {
|
||||
# Update link in Changelog.html accordingly
|
||||
if (Test-Path "out\$variant\Changelog.html" -PathType Leaf) {
|
||||
(Get-Content "out\$variant\Changelog.html").Replace('ArchiSteamFarm/commits/main', "ArchiSteamFarm/releases/tag/$env:APPVEYOR_REPO_TAG_NAME") | Set-Content "out\$variant\Changelog.html"
|
||||
}
|
||||
}
|
||||
|
||||
7z a -bd -slp -tzip -mm=Deflate $compressionArgs "out\ASF-$variant.zip" "$env:APPVEYOR_BUILD_FOLDER\out\$variant\*"
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
# TODO: Change me to Push-AppveyorArtifact once https://github.com/appveyor/ci/issues/2183 is fixed
|
||||
appveyor PushArtifact "out\ASF-$variant.zip" -FileName "ASF-$variant.zip" -DeploymentName "ASF-$variant.zip"
|
||||
}
|
||||
|
||||
|
||||
foreach ($variant in $env:VARIANTS.Split([char[]] $null, [System.StringSplitOptions]::RemoveEmptyEntries)) {
|
||||
Start-Job -Name "$variant" $PublishBlock -ArgumentList "$variant"
|
||||
}
|
||||
|
||||
|
||||
Get-Job | Receive-Job -Wait
|
||||
deploy: off
|
||||
notifications:
|
||||
- provider: Webhook
|
||||
url:
|
||||
secure: i/og7KzkpbcWcKoUubrLH+KB6bkfqA55FHUlGxLepLmgZNQeNMiMoAFICOFY795fFrFfUNUKqwk7ApXE6HUyWMoiijLj7G/JBLTPkBiTCu8fZMTMqwQm6FiHB3+/0h0C+ukcrBEqnXYSQUh6znpKqJSTrIfXUQ7ftNuC966kBAw=
|
||||
method: POST
|
||||
body: >-
|
||||
{
|
||||
"avatar_url": "https://www.appveyor.com/assets/img/appveyor-logo-256.png",
|
||||
"username": "AppVeyor",
|
||||
"content": "[{{projectName}}:{{branch}}] {{commitMessage}} by {{commitAuthor}} ({{commitId}}) | **{{status}}** on {{buildUrl}}"
|
||||
}
|
||||
on_build_success: true
|
||||
on_build_failure: true
|
||||
on_build_status_changed: false
|
||||
25
.github/renovate.json
vendored
25
.github/renovate.json
vendored
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
":assignee(JustArchi)",
|
||||
":automergeBranch",
|
||||
":automergeDigest",
|
||||
":automergeMinor",
|
||||
":disableRateLimiting",
|
||||
":label(🤖 Automatic)"
|
||||
],
|
||||
"git-submodules": {
|
||||
"enabled": true
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"allowedVersions": "<= 3.0",
|
||||
"matchPackageNames": [ "Microsoft.Extensions.Configuration.Json", "Microsoft.Extensions.Logging.Configuration" ]
|
||||
},
|
||||
{
|
||||
"allowedVersions": "<= 2.2.4",
|
||||
"groupName": "MSTest packages",
|
||||
"matchPackagePatterns": ["^MSTest\\..+"]
|
||||
}
|
||||
]
|
||||
}
|
||||
24
.github/renovate.json5
vendored
Normal file
24
.github/renovate.json5
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
":assignee(JustArchi)",
|
||||
":automergeBranch",
|
||||
":automergeDigest",
|
||||
":automergeMinor",
|
||||
":disableDependencyDashboard",
|
||||
":disableRateLimiting",
|
||||
":label(🤖 Automatic)"
|
||||
],
|
||||
"git-submodules": {
|
||||
"enabled": true
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
// 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" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -3,44 +3,44 @@ name: ASF-ci
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
CONFIGURATION: Release
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
DOTNET_NOLOGO: 1
|
||||
DOTNET_SDK_VERSION: 5.0.x
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_SDK_VERSION: 6.0.x
|
||||
|
||||
jobs:
|
||||
main:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
configuration: [Debug, Release]
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1.8.1
|
||||
uses: actions/setup-dotnet@v1.9.0
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
|
||||
|
||||
- name: Verify .NET Core
|
||||
run: dotnet --info
|
||||
|
||||
- name: Build ArchiSteamFarm and other projects
|
||||
run: dotnet build -c "${{ env.CONFIGURATION }}" -p:ContinuousIntegrationBuild=true -p:UseAppHost=false --nologo
|
||||
- name: Build ${{ matrix.configuration }} ArchiSteamFarm and other projects
|
||||
run: dotnet build -c "${{ matrix.configuration }}" -p:ContinuousIntegrationBuild=true -p:UseAppHost=false --nologo
|
||||
|
||||
- name: Run ArchiSteamFarm.Tests
|
||||
run: dotnet test ArchiSteamFarm.Tests -c "${{ env.CONFIGURATION }}" -p:ContinuousIntegrationBuild=true -p:UseAppHost=false --nologo
|
||||
- name: Run ${{ matrix.configuration }} ArchiSteamFarm.Tests
|
||||
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' && startsWith(matrix.os, 'ubuntu-') }}
|
||||
uses: crowdin/github-action@1.3.0
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.configuration == 'Release' && startsWith(matrix.os, 'ubuntu-') }}
|
||||
uses: crowdin/github-action@1.4.6
|
||||
with:
|
||||
crowdin_branch_name: main
|
||||
config: '.github/crowdin.yml'
|
||||
|
||||
13
.github/workflows/docker-ci.yml
vendored
13
.github/workflows/docker-ci.yml
vendored
@@ -10,23 +10,26 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
configuration: [Debug, Release]
|
||||
file: [Dockerfile, Dockerfile.Service]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.5.1
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
|
||||
- name: Build Docker image from ${{ matrix.file }}
|
||||
uses: docker/build-push-action@v2.6.1
|
||||
- name: Build ${{ matrix.configuration }} Docker image from ${{ matrix.file }}
|
||||
uses: docker/build-push-action@v2.8.0
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
build-args: STEAM_TOKEN_DUMPER_TOKEN=${{ secrets.STEAM_TOKEN_DUMPER_TOKEN }}
|
||||
build-args: |
|
||||
CONFIGURATION=${{ matrix.configuration }}
|
||||
STEAM_TOKEN_DUMPER_TOKEN=${{ secrets.STEAM_TOKEN_DUMPER_TOKEN }}
|
||||
|
||||
22
.github/workflows/docker-publish-latest.yml
vendored
22
.github/workflows/docker-publish-latest.yml
vendored
@@ -5,6 +5,7 @@ on:
|
||||
types: [released]
|
||||
|
||||
env:
|
||||
ASF_PRIVATE_SNK: ${{ secrets.ASF_PRIVATE_SNK }}
|
||||
PLATFORMS: linux/amd64,linux/arm,linux/arm64
|
||||
TAG: latest
|
||||
|
||||
@@ -14,26 +15,35 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.5.1
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v1.12.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@v1.12.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Prepare private key for signing
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
if [ -n "${ASF_PRIVATE_SNK-}" ]; then
|
||||
echo "$ASF_PRIVATE_SNK" | base64 -d > "resources/ArchiSteamFarm.snk"
|
||||
fi
|
||||
|
||||
- name: Prepare environment outputs
|
||||
shell: sh
|
||||
run: |
|
||||
@@ -45,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.6.1
|
||||
uses: docker/build-push-action@v2.8.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.Service
|
||||
|
||||
22
.github/workflows/docker-publish-main.yml
vendored
22
.github/workflows/docker-publish-main.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- main
|
||||
|
||||
env:
|
||||
ASF_PRIVATE_SNK: ${{ secrets.ASF_PRIVATE_SNK }}
|
||||
PLATFORMS: linux/amd64,linux/arm,linux/arm64
|
||||
TAG: main
|
||||
|
||||
@@ -15,26 +16,35 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.5.1
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v1.12.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@v1.12.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Prepare private key for signing
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
if [ -n "${ASF_PRIVATE_SNK-}" ]; then
|
||||
echo "$ASF_PRIVATE_SNK" | base64 -d > "resources/ArchiSteamFarm.snk"
|
||||
fi
|
||||
|
||||
- name: Prepare environment outputs
|
||||
shell: sh
|
||||
run: |
|
||||
@@ -45,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.6.1
|
||||
uses: docker/build-push-action@v2.8.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
|
||||
22
.github/workflows/docker-publish-released.yml
vendored
22
.github/workflows/docker-publish-released.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
ASF_PRIVATE_SNK: ${{ secrets.ASF_PRIVATE_SNK }}
|
||||
PLATFORMS: linux/amd64,linux/arm,linux/arm64
|
||||
TAG: released
|
||||
|
||||
@@ -15,26 +16,35 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.5.1
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v1.12.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@v1.12.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Prepare private key for signing
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
if [ -n "${ASF_PRIVATE_SNK-}" ]; then
|
||||
echo "$ASF_PRIVATE_SNK" | base64 -d > "resources/ArchiSteamFarm.snk"
|
||||
fi
|
||||
|
||||
- name: Prepare environment outputs
|
||||
shell: sh
|
||||
run: |
|
||||
@@ -46,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.6.1
|
||||
uses: docker/build-push-action@v2.8.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
|
||||
6
.github/workflows/lock-threads.yml
vendored
6
.github/workflows/lock-threads.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Lock inactive threads
|
||||
uses: dessant/lock-threads@v2.1.1
|
||||
uses: dessant/lock-threads@v3.0.0
|
||||
with:
|
||||
issue-lock-inactive-days: 30
|
||||
pr-lock-inactive-days: 30
|
||||
issue-inactive-days: 60
|
||||
pr-inactive-days: 60
|
||||
|
||||
333
.github/workflows/publish.yml
vendored
333
.github/workflows/publish.yml
vendored
@@ -3,18 +3,19 @@ name: ASF-publish
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
ASF_PRIVATE_SNK: ${{ secrets.ASF_PRIVATE_SNK }}
|
||||
CONFIGURATION: Release
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
DOTNET_NOLOGO: 1
|
||||
DOTNET_SDK_VERSION: 5.0.x
|
||||
NET_CORE_VERSION: net5.0
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_SDK_VERSION: 6.0.x
|
||||
NET_CORE_VERSION: net6.0
|
||||
NET_FRAMEWORK_VERSION: net48
|
||||
NODE_JS_VERSION: 'lts/*'
|
||||
STEAM_TOKEN_DUMPER_NAME: ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
|
||||
STEAM_TOKEN_DUMPER_TOKEN: ${{ secrets.STEAM_TOKEN_DUMPER_TOKEN }}
|
||||
|
||||
jobs:
|
||||
main:
|
||||
publish:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -24,12 +25,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1.8.1
|
||||
uses: actions/setup-dotnet@v1.9.0
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
|
||||
|
||||
@@ -37,7 +38,7 @@ jobs:
|
||||
run: dotnet --info
|
||||
|
||||
- name: Setup Node.js with npm
|
||||
uses: actions/setup-node@v2.3.0
|
||||
uses: actions/setup-node@v2.5.1
|
||||
with:
|
||||
check-latest: true
|
||||
node-version: ${{ env.NODE_JS_VERSION }}
|
||||
@@ -54,14 +55,44 @@ jobs:
|
||||
- name: Publish ASF-ui
|
||||
run: npm run-script deploy --no-progress --prefix ASF-ui
|
||||
|
||||
- name: Prepare private key for signing on Unix
|
||||
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
if [ -n "${ASF_PRIVATE_SNK-}" ]; then
|
||||
echo "$ASF_PRIVATE_SNK" | base64 -d > "resources/ArchiSteamFarm.snk"
|
||||
fi
|
||||
|
||||
- name: Prepare private key for signing on Windows
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
if ((Test-Path env:ASF_PRIVATE_SNK) -and ($env:ASF_PRIVATE_SNK)) {
|
||||
echo "$env:ASF_PRIVATE_SNK" > "resources\ArchiSteamFarm.snk"
|
||||
|
||||
certutil -f -decode "resources\ArchiSteamFarm.snk" "resources\ArchiSteamFarm.snk"
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
}
|
||||
|
||||
- name: Prepare ArchiSteamFarm.OfficialPlugins.SteamTokenDumper on Unix
|
||||
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
|
||||
shell: sh
|
||||
run: |
|
||||
if [ -n "${STEAM_TOKEN_DUMPER_TOKEN-}" ] && [ -f "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs" ]; then
|
||||
sed "s/STEAM_TOKEN_DUMPER_TOKEN/${STEAM_TOKEN_DUMPER_TOKEN}/g" "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs" > "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs.new"
|
||||
mv "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs.new" "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs"
|
||||
fi
|
||||
set -eu
|
||||
|
||||
if [ -n "${STEAM_TOKEN_DUMPER_TOKEN-}" ] && [ -f "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs" ]; then
|
||||
sed "s/STEAM_TOKEN_DUMPER_TOKEN/${STEAM_TOKEN_DUMPER_TOKEN}/g" "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs" > "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs.new"
|
||||
mv "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs.new" "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs"
|
||||
fi
|
||||
|
||||
- name: Prepare ArchiSteamFarm.OfficialPlugins.SteamTokenDumper on Windows
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
@@ -71,7 +102,7 @@ jobs:
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
if ((Test-Path env:STEAM_TOKEN_DUMPER_TOKEN) -and (Test-Path "$env:STEAM_TOKEN_DUMPER_NAME\SharedInfo.cs" -PathType Leaf)) {
|
||||
if ((Test-Path env:STEAM_TOKEN_DUMPER_TOKEN) -and ($env:STEAM_TOKEN_DUMPER_TOKEN) -and (Test-Path "$env:STEAM_TOKEN_DUMPER_NAME\SharedInfo.cs" -PathType Leaf)) {
|
||||
(Get-Content "$env:STEAM_TOKEN_DUMPER_NAME\SharedInfo.cs").Replace('STEAM_TOKEN_DUMPER_TOKEN', "$env:STEAM_TOKEN_DUMPER_TOKEN") | Set-Content "$env:STEAM_TOKEN_DUMPER_NAME\SharedInfo.cs"
|
||||
}
|
||||
|
||||
@@ -83,27 +114,33 @@ jobs:
|
||||
run: dotnet publish "${{ env.STEAM_TOKEN_DUMPER_NAME }}" -c "${{ env.CONFIGURATION }}" -f "${{ env.NET_FRAMEWORK_VERSION }}" -o "out/${{ env.STEAM_TOKEN_DUMPER_NAME }}/${{ env.NET_FRAMEWORK_VERSION }}" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --nologo
|
||||
|
||||
- name: Restore packages in preparation for ArchiSteamFarm publishing
|
||||
run: dotnet restore ArchiSteamFarm -p:ContinuousIntegrationBuild=true
|
||||
run: dotnet restore ArchiSteamFarm -p:ContinuousIntegrationBuild=true --nologo
|
||||
|
||||
- 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
|
||||
|
||||
publish() {
|
||||
if [ "$1" = 'generic' ]; then
|
||||
local variantArgs="-p:TargetLatestRuntimePatch=false -p:UseAppHost=false"
|
||||
variantArgs="-p:TargetLatestRuntimePatch=false -p:UseAppHost=false"
|
||||
else
|
||||
local 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
|
||||
if [ -d "ArchiSteamFarm/overlay/${1}" ]; then
|
||||
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
|
||||
|
||||
@@ -116,17 +153,57 @@ jobs:
|
||||
# Include .ico file for all platforms, since only Windows script can bundle it inside the exe
|
||||
cp "resources/ASF.ico" "out/${1}/ArchiSteamFarm.ico"
|
||||
|
||||
if command -v 7z >/dev/null; then
|
||||
7z a -bd -slp -tzip -mm=Deflate -mx=1 "out/ASF-${1}.zip" "${GITHUB_WORKSPACE}/out/${1}/*"
|
||||
elif command -v zip >/dev/null; then
|
||||
(
|
||||
cd "${GITHUB_WORKSPACE}/out/${1}"
|
||||
zip -1 -q -r "../ASF-${1}.zip" .
|
||||
)
|
||||
else
|
||||
echo "ERROR: No supported zip tool!"
|
||||
return 1
|
||||
fi
|
||||
# By default use fastest compression
|
||||
seven_zip_args="-mx=1"
|
||||
zip_args="-1"
|
||||
|
||||
# Include extra logic for builds marked for release
|
||||
case "$GITHUB_REF" in
|
||||
"refs/tags/"*)
|
||||
# Tweak compression args for release publishing
|
||||
seven_zip_args="-mx=9 -mfb=258 -mpass=15"
|
||||
zip_args="-9"
|
||||
|
||||
# Update link in Changelog.html accordingly
|
||||
if [ -f "out/${1}/Changelog.html" ]; then
|
||||
tag="$(echo "$GITHUB_REF" | cut -c 11-)"
|
||||
|
||||
sed "s/ArchiSteamFarm\/commits\/main/ArchiSteamFarm\/releases\/tag\/${tag}/g" "out/${1}/Changelog.html" > "out/${1}/Changelog.html.new"
|
||||
mv "out/${1}/Changelog.html.new" "out/${1}/Changelog.html"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# 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)
|
||||
if command -v zip >/dev/null; then
|
||||
(
|
||||
cd "${GITHUB_WORKSPACE}/out/${1}"
|
||||
zip -q -r $zip_args "../ASF-${1}.zip" .
|
||||
)
|
||||
elif command -v 7z >/dev/null; then
|
||||
7z a -bd -slp -tzip -mm=Deflate $seven_zip_args "out/ASF-${1}.zip" "${GITHUB_WORKSPACE}/out/${1}/*"
|
||||
else
|
||||
echo "ERROR: No supported zip tool!"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if command -v 7z >/dev/null; then
|
||||
7z a -bd -slp -tzip -mm=Deflate $seven_zip_args "out/ASF-${1}.zip" "${GITHUB_WORKSPACE}/out/${1}/*"
|
||||
elif command -v zip >/dev/null; then
|
||||
(
|
||||
cd "${GITHUB_WORKSPACE}/out/${1}"
|
||||
zip -q -r $zip_args "../ASF-${1}.zip" .
|
||||
)
|
||||
else
|
||||
echo "ERROR: No supported zip tool!"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
jobs=""
|
||||
@@ -143,7 +220,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
|
||||
@@ -168,7 +245,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
|
||||
@@ -178,7 +255,13 @@ jobs:
|
||||
}
|
||||
|
||||
# If we're including any overlay for this variant, copy it to output directory
|
||||
if (Test-Path "ArchiSteamFarm\overlay\$variant" -PathType Container) {
|
||||
$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
|
||||
}
|
||||
|
||||
@@ -201,33 +284,63 @@ jobs:
|
||||
|
||||
# Include extra logic for builds marked for release
|
||||
if ($env:GITHUB_REF -like 'refs/tags/*') {
|
||||
$tag = $env:GITHUB_REF.Substring(10)
|
||||
|
||||
# Tweak compression args for release publishing
|
||||
$compressionArgs = '-mx=9', '-mfb=258', '-mpass=15'
|
||||
|
||||
# Update link in Changelog.html accordingly
|
||||
if (Test-Path "out\$variant\Changelog.html" -PathType Leaf) {
|
||||
$tag = $env:GITHUB_REF.Substring(10)
|
||||
|
||||
(Get-Content "out\$variant\Changelog.html").Replace('ArchiSteamFarm/commits/main', "ArchiSteamFarm/releases/tag/$tag") | Set-Content "out\$variant\Changelog.html"
|
||||
}
|
||||
}
|
||||
|
||||
# Create the final zip file
|
||||
7z a -bd -slp -tzip -mm=Deflate $compressionArgs "out\ASF-$variant.zip" "$env:GITHUB_WORKSPACE\out\$variant\*"
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
# We can aid non-windows users by adding chmod +x flag to appropriate executables directly in the zip file
|
||||
# This is ALMOST a hack, but works reliably enough
|
||||
if (Test-Path "tools\zip_exec\zip_exec.exe" -PathType Leaf) {
|
||||
$executableFiles = @()
|
||||
|
||||
if ($variant -like 'generic*') {
|
||||
$executableFiles += 'ArchiSteamFarm.sh', 'ArchiSteamFarm-Service.sh'
|
||||
} elseif (($variant -like 'linux*') -or ($variant -like 'osx*')) {
|
||||
$executableFiles += 'ArchiSteamFarm', 'ArchiSteamFarm-Service.sh'
|
||||
}
|
||||
|
||||
foreach ($executableFile in $executableFiles) {
|
||||
tools\zip_exec\zip_exec.exe "out\ASF-$variant.zip" "$executableFile"
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "Last command failed."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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@v2.3.1
|
||||
with:
|
||||
name: ${{ matrix.os }}_ASF-generic
|
||||
path: out/ASF-generic.zip
|
||||
@@ -235,52 +348,149 @@ jobs:
|
||||
- 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@v2.3.1
|
||||
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@v2.3.1
|
||||
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@v2.3.1
|
||||
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@v2.3.1
|
||||
with:
|
||||
name: ${{ matrix.os }}_ASF-linux-x64
|
||||
path: out/ASF-linux-x64.zip
|
||||
|
||||
- name: Upload ASF-osx-arm64
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v2.3.1
|
||||
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@v2.3.1
|
||||
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@v2.3.1
|
||||
with:
|
||||
name: ${{ matrix.os }}_ASF-win-x64
|
||||
path: out/ASF-win-x64.zip
|
||||
|
||||
release:
|
||||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
|
||||
needs: publish
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.4.0
|
||||
|
||||
# TODO: It'd be perfect if we could match final artifacts to the platform they target, so e.g. linux build comes from the linux machine
|
||||
# However, that is currently impossible due to https://github.com/dotnet/msbuild/issues/3897
|
||||
# Therefore, we'll (sadly) pull artifacts from Windows machine only for now
|
||||
- name: Download ASF-generic artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.1.0
|
||||
with:
|
||||
name: windows-latest_ASF-generic
|
||||
path: out
|
||||
|
||||
- name: Download ASF-generic-netf artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.1.0
|
||||
with:
|
||||
name: windows-latest_ASF-generic-netf
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-arm artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.1.0
|
||||
with:
|
||||
name: windows-latest_ASF-linux-arm
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-arm64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.1.0
|
||||
with:
|
||||
name: windows-latest_ASF-linux-arm64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-x64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.1.0
|
||||
with:
|
||||
name: windows-latest_ASF-linux-x64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-osx-arm64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.1.0
|
||||
with:
|
||||
name: windows-latest_ASF-osx-arm64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-osx-x64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.1.0
|
||||
with:
|
||||
name: windows-latest_ASF-osx-x64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-win-x64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.1.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
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
|
||||
|
||||
- name: Generate SHA-512 checksums and signature
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
(
|
||||
cd "out"
|
||||
|
||||
sha512sum *.zip > SHA512SUMS
|
||||
gpg -a -b -o SHA512SUMS.sign SHA512SUMS
|
||||
)
|
||||
|
||||
- name: Upload SHA512SUMS
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v2.3.1
|
||||
with:
|
||||
name: SHA512SUMS
|
||||
path: out/SHA512SUMS
|
||||
|
||||
- name: Upload SHA512SUMS.sign
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v2.3.1
|
||||
with:
|
||||
name: SHA512SUMS.sign
|
||||
path: out/SHA512SUMS.sign
|
||||
|
||||
- name: Create ArchiSteamFarm GitHub release
|
||||
id: github_release
|
||||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'windows-') }}
|
||||
uses: actions/create-release@v1.1.4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.ARCHIBOT_GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ArchiSteamFarm V${{ github.ref }}
|
||||
@@ -288,7 +498,6 @@ jobs:
|
||||
prerelease: true
|
||||
|
||||
- name: Upload ASF-generic to GitHub release
|
||||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'windows-') }}
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -299,7 +508,6 @@ jobs:
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload ASF-generic-netf to GitHub release
|
||||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'windows-') }}
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -310,7 +518,6 @@ jobs:
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload ASF-linux-arm to GitHub release
|
||||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'windows-') }}
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -321,7 +528,6 @@ jobs:
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload ASF-linux-arm64 to GitHub release
|
||||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'windows-') }}
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -332,7 +538,6 @@ jobs:
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload ASF-linux-x64 to GitHub release
|
||||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'windows-') }}
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -342,8 +547,17 @@ 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
|
||||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'windows-') }}
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -354,7 +568,6 @@ jobs:
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload ASF-win-x64 to GitHub release
|
||||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'windows-') }}
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -363,3 +576,23 @@ jobs:
|
||||
asset_path: out/ASF-win-x64.zip
|
||||
asset_name: ASF-win-x64.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload SHA512SUMS 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/SHA512SUMS
|
||||
asset_name: SHA512SUMS
|
||||
asset_content_type: text/plain
|
||||
|
||||
- name: Upload SHA512SUMS.sign 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/SHA512SUMS.sign
|
||||
asset_name: SHA512SUMS.sign
|
||||
asset_content_type: text/plain
|
||||
|
||||
23
.github/workflows/translations.yml
vendored
23
.github/workflows/translations.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.ARCHIBOT_GITHUB_TOKEN }}
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git reset --hard origin/master
|
||||
|
||||
- name: Download latest translations from Crowdin
|
||||
uses: crowdin/github-action@1.3.0
|
||||
uses: crowdin/github-action@1.4.6
|
||||
with:
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
@@ -37,13 +37,13 @@ jobs:
|
||||
project_id: ${{ secrets.ASF_CROWDIN_PROJECT_ID }}
|
||||
token: ${{ secrets.ASF_CROWDIN_API_TOKEN }}
|
||||
|
||||
- name: Import GPG key for wiki
|
||||
uses: crazy-max/ghaction-import-gpg@v3.1.0
|
||||
- name: Import GPG key for signing
|
||||
uses: crazy-max/ghaction-import-gpg@v4.1.0
|
||||
with:
|
||||
gpg-private-key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
|
||||
git-user-signingkey: true
|
||||
git-commit-gpgsign: true
|
||||
workdir: wiki
|
||||
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
|
||||
git_config_global: true
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: Commit the changes to wiki
|
||||
shell: sh
|
||||
@@ -66,13 +66,6 @@ jobs:
|
||||
directory: wiki
|
||||
repository: ${{ github.repository }}.wiki
|
||||
|
||||
- name: Import GPG key for ASF
|
||||
uses: crazy-max/ghaction-import-gpg@v3.1.0
|
||||
with:
|
||||
gpg-private-key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
|
||||
git-user-signingkey: true
|
||||
git-commit-gpgsign: true
|
||||
|
||||
- name: Commit the changes to ASF
|
||||
shell: sh
|
||||
run: |
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -25,6 +25,7 @@ ArchiSteamFarm/logs
|
||||
# |_____||_||_| |_| \__,_|/_/\_\
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/Global/Linux.gitignore
|
||||
# 4f7062e132d7f88e68ab737e64fef872bd3a491f
|
||||
|
||||
*~
|
||||
|
||||
@@ -47,6 +48,7 @@ ArchiSteamFarm/logs
|
||||
# |_| |_| |_| \__,_| \___| \___/ |____/
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
|
||||
# 2bb963b16a1957c865335e53537036c2e97399b5
|
||||
|
||||
# General
|
||||
.DS_Store
|
||||
@@ -83,6 +85,7 @@ Temporary Items
|
||||
# |_|
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/Global/MonoDevelop.gitignore
|
||||
# e8b2e1a9cc7c9ca49bb05c20a4c4491b85feba6d
|
||||
|
||||
#User Specific
|
||||
*.userprefs
|
||||
@@ -100,6 +103,7 @@ test-results/
|
||||
# \_/ |_||___/ \__,_| \__,_||_||____/ \__| \__,_| \__,_||_| \___/
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
# 888439ee893d0097862f1d510585bd0e3cfd500f
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
@@ -497,6 +501,7 @@ FodyWeavers.xsd
|
||||
# \_/\_/ |_||_| |_| \__,_| \___/ \_/\_/ |___/
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
|
||||
# 5808b77453dec299d4daf8557b05a80be832a5b8
|
||||
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
|
||||
2
ASF-ui
2
ASF-ui
Submodule ASF-ui updated: 0692fb39b5...156992e88d
@@ -5,12 +5,14 @@
|
||||
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||
<PackageReference Include="JustArchiNET.Madness" IncludeAssets="compile" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" IncludeAssets="compile" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,42 +26,40 @@ 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<string?> 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.EscapeUriString(response.Content.Link);
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 = "";
|
||||
[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
|
||||
[JsonConstructor]
|
||||
private MeowResponse() { }
|
||||
}
|
||||
#pragma warning restore CA1812 // False positive, the class is used during json deserialization
|
||||
}
|
||||
|
||||
@@ -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<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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,9 +19,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using ArchiSteamFarm.Compatibility;
|
||||
#endif
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Composition;
|
||||
@@ -31,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
|
||||
string? 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 !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 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
|
||||
<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="Swashbuckle.AspNetCore.Annotations" IncludeAssets="compile" />
|
||||
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||
<PackageReference Include="JustArchiNET.Madness" IncludeAssets="compile" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ArchiSteamFarm\ArchiSteamFarm.csproj" ExcludeAssets="all" Private="false" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,270 +19,288 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using ArchiSteamFarm.Compatibility;
|
||||
using File = System.IO.File;
|
||||
using Path = System.IO.Path;
|
||||
#else
|
||||
using System.IO;
|
||||
#endif
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
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; }
|
||||
|
||||
internal ulong GetAppToken(uint appID) => AppTokens[appID];
|
||||
internal GlobalCache() => FilePath = SharedFilePath;
|
||||
|
||||
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(appToken => appToken.Key, 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(depotKey => depotKey.Key, 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(packageToken => packageToken.Key, packageToken => packageToken.Value);
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeAppChangeNumbers() => !AppChangeNumbers.IsEmpty;
|
||||
|
||||
internal static async Task<GlobalCache?> Load() {
|
||||
if (!File.Exists(SharedFilePath)) {
|
||||
GlobalCache result = new();
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeAppTokens() => !AppTokens.IsEmpty;
|
||||
|
||||
Utilities.InBackground(result.Save);
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeDepotKeys() => !DepotKeys.IsEmpty;
|
||||
|
||||
return result;
|
||||
}
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeLastChangeNumber() => LastChangeNumber > 0;
|
||||
|
||||
GlobalCache? globalCache;
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializePackageTokens() => !PackageTokens.IsEmpty;
|
||||
|
||||
try {
|
||||
string json = await Compatibility.File.ReadAllTextAsync(SharedFilePath).ConfigureAwait(false);
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeSubmittedApps() => !SubmittedApps.IsEmpty;
|
||||
|
||||
if (string.IsNullOrEmpty(json)) {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeSubmittedDepots() => !SubmittedDepots.IsEmpty;
|
||||
|
||||
return null;
|
||||
}
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeSubmittedPackages() => !SubmittedPackages.IsEmpty;
|
||||
|
||||
globalCache = JsonConvert.DeserializeObject<GlobalCache>(json);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
internal ulong GetAppToken(uint appID) => AppTokens[appID];
|
||||
|
||||
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 static async Task<GlobalCache?> Load() {
|
||||
if (!File.Exists(SharedFilePath)) {
|
||||
return new GlobalCache();
|
||||
}
|
||||
|
||||
ASF.ArchiLogger.LogGenericInfo(Strings.LoadingGlobalCache);
|
||||
|
||||
GlobalCache? globalCache;
|
||||
|
||||
try {
|
||||
string json = await File.ReadAllTextAsync(SharedFilePath).ConfigureAwait(false);
|
||||
|
||||
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(nameof(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) || (appData.ChangeNumber <= previousChangeNumber)) {
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() { }
|
||||
}
|
||||
|
||||
@@ -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'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'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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -62,6 +62,50 @@
|
||||
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="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>
|
||||
|
||||
|
||||
|
||||
@@ -70,23 +114,32 @@
|
||||
|
||||
|
||||
|
||||
<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="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>
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -67,46 +67,46 @@
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} ist gemäß Ihrer Konfiguration derzeit deaktiviert. Wenn Sie SteamDB bei der Datenübermittlung helfen möchten, sehen Sie sich bitte unser Wiki an.</value>
|
||||
<value>{0} ist gemäß Ihrer Konfiguration derzeit deaktiviert. Wenn Sie SteamDB bei der Daten-Sammlung helfen möchten, sehen Sie sich bitte unser Wiki an.</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} wurde erfolgreich initialisiert, wir danken Ihnen im Voraus für Ihre Hilfe. Die erste Übermittlung wird in etwa {1} ab jetzt erfolgen.</value>
|
||||
<value>{0} wurde erfolgreich initialisiert. Wir danken Ihnen im Voraus für Ihre Hilfe. Die erste Übermittlung wird in etwa {1} ab jetzt erfolgen.</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} konnte nicht geladen werden. Eine frische Instanz wird initialisiert werden...</value>
|
||||
<value>{0} konnte nicht geladen werden. Eine frische Instanz wird initialisiert...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>Es gibt auf dieser Bot-Instanz keine Applikationen, die einer Auffrischung bedürfen.</value>
|
||||
<value>Es gibt auf dieser Bot-Instanz keine Apps, die einer Aktualisierung bedürfen.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Rufe insgesamt {0} Applikationszugriffstoken ab...</value>
|
||||
<value>Rufe insgesamt {0} App-Zugriffstoken ab...</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>Rufe {0} Applikationszugriffstoken ab...</value>
|
||||
<value>Rufe {0} App-Zugriffstoken ab...</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>Abruf von {0} Applikationszugrifsstoken fertiggestellt.</value>
|
||||
<value>Abruf von {0} App-Zugrifsstoken abgeschlossen.</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>Abruf von insgesamt {0} Applikationszugriffstoken fertiggestellt.</value>
|
||||
<value>Abruf von insgesamt {0} App-Zugriffstoken abgeschlossen.</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>Rufe alle Depots für insgesamt {0} Applikationen ab...</value>
|
||||
<value>Abruf aller Depots für insgesamt {0} Apps...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Rufe {0} Applikationsinfos ab...</value>
|
||||
<value>Rufe {0} App-Infos ab...</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>Abruf von {0} Applikationsinfos fertiggestellt.</value>
|
||||
<value>Abruf von {0} App-Infos abgeschlossen.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
@@ -114,38 +114,38 @@
|
||||
<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>Abruf von {0} Depotschlüsseln fertiggestellt.</value>
|
||||
<value>Abruf von {0} Depotschlüsseln abgeschlossen.</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>Abruf aller Depotschlüssel für insgesamt {0} Applikationen fertiggestellt.</value>
|
||||
<value>Abruf aller Depotschlüssel für insgesamt {0} Apps abgeschlossen.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Es sind keine neuen Daten einzureichen. Alle Daten sind aktuell.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Konnte keine Daten einreichen, da keine SteamID identifiziert wurde, die als Einreichender valide ist. Bitte ziehe in Betracht {0} zu konfigurieren.</value>
|
||||
<value>Konnte keine Daten spenden, da keine SteamID identifiziert wurde, die als Spender valide ist. Bitte ziehe in Betracht {0} zu konfigurieren.</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>Reiche insgesamt folgende Applikationen/Pakete/Depots ein: {0}/{1}/{2}...</value>
|
||||
<value>Übermittle insgesamt folgende Apps/Pakete/Depots: {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>Das Einreichen schlug fehl, weil wir zu viele Einreichungen in zu kurzer Zeit versuchten. Wir versuchen es in ungefähr {0} wieder.</value>
|
||||
<value>Das Übermitteln schlug fehl, weil wir zu viele Spenden in zu kurzer Zeit versuchten. Wir versuchen es in ungefähr {0} wieder.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Die Daten wurden erfolgreich übermittelt. Der Server hat insgesamt folgende Anzahl an neuen Applikationen/Paketen/Depots registriert: {0} ({1} verifiziert)/{2} ({3} verifiziert)/{4} ({5} verifiziert).</value>
|
||||
<value>Die Daten wurden erfolgreich übermittelt. Der Server hat insgesamt folgende Anzahl an neuen Apps/Paketen/Depots registriert: {0} ({1} verifiziert)/{2} ({3} verifiziert)/{4} ({5} verifiziert).</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>Neue Applikationen: {0}</value>
|
||||
<value>Neue Apps: {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>Verifizierte Applikationen: {0}</value>
|
||||
<value>Verifizierte Apps: {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">
|
||||
@@ -165,7 +165,12 @@
|
||||
<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} wurde konfiguriert, das Plugin wird keinen der folgenden Werte auflösen: {1}.</value>
|
||||
<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>
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -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 στην υποβολή δεδομένων, παρακαλώ ελέγξτε το 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 instance.</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>Τα δεδομένα έχουν υποβληθεί με επιτυχία. Ο διακομιστής έχει καταχωρήσει συνολικά νέες εφαρμογές/πακέτα/depots: {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} αρχικοποιήθηκε, το 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -91,6 +91,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -62,31 +62,119 @@
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>{0} è stato disabilitato a causa di un token di generazione mancante</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} è attualmente disabilitato in base alla tua configurazione. Se desideri aiutare SteamDB nell'invio dei dati, controlla la nostra 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} è stato inizializzato con successo, grazie in anticipo per il tuo aiuto. Il primo invio avverrà in circa {1} da ora.</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} non può essere caricato, una nuova richiesta verrà iniziata...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>Non ci sono applicazioni che richiedono un aggiornamento su questa richiesta del bot.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Ricezione di un totale di {0} token di accesso all'app...</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>Ricezione di {0} token di accesso all'app...</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>Hai completato il recupero di {0} token di accesso all'app.</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>Hai completato il recupero di un totale di {0} token di accesso all'app.</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>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>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Non ci sono nuovi dati da inviare, tutto è aggiornato.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Impossibile inviare i dati perché non c'è uno SteamID impostato valido che potremmo classificare come contributore. Considera di impostare delle proprietà {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>Inviando un totale di app/pacchetti/depositi registrati: {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>L'invio non è riuscito a causa di troppe richieste inviate, riproveremo tra circa {0} da ora.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>I dati sono stati inviati con successo. Il server ha registrato un totale di nuove app/pacchetti/depositi: {0} ({1} verificato)/{2} ({3} verificato)/{4} ({5} verificato).</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>Nuove app: {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>App verificate: {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>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>
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -124,4 +124,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -168,4 +168,13 @@
|
||||
<value>{0} inicializado, o plugin não resolverá nenhum desses: {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 STD global...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Validando integridade do cache STD global...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Falha ao verificar a integridade do cache STD global. Isso sugere uma potencial corrupção de arquivo/memória, uma instância nova será inicializada em vez disso.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -82,11 +82,11 @@
|
||||
<value>Нет приложений, которые требуют обновления для этого бота.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Получение в общей сложности {0} маркеров доступа к приложениям...</value>
|
||||
<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>
|
||||
<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">
|
||||
@@ -97,29 +97,59 @@
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
@@ -130,6 +160,21 @@
|
||||
<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>
|
||||
|
||||
@@ -62,30 +62,112 @@
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>{0} bol vypnutý kvôli chýbajúcemu build 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} bol na základe vašej konfigurácie vypnutý. Ak máte záujem pomôcť so zberom údajov pre SteamDB, navštívte našu 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} bol úspešne spustený. Týmto vám ďakujeme za vašu pomoc. Prvý zápis sa udej od teraz 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} sa nedokázal načítať, bude načítaný nanovo...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>Tento bot neobsahuje žiadne aplikácie na opätovné overenie.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Celkovo získaných {0} označení aplikácií...</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>Získaných {0} označení aplikácií...</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>Získalo sa {0} označení aplikácií.</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>Celkovo sa získalo {0} označení aplikácií.</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>Získavanie všetkých {0} položiek pre aplikácie...</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ískavam informácie o {0} aplikáciách...</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>Dokončené získavanie informácií {0} aplikácií.</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ískavam {0} kľúčov položiek...</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>Dokončené získavanie {0} kľúčov položiek.</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>Dokončené získanie všetkých kľúčov položiek {0} aplikácií.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Dáta sú aktuálne, nič nové na potvrdenie.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Nemôžeme prijať žiadne záznamy, pretože ani jedno SteamID nezodpovedá definícii prispievateľa. Zvážte nastavenie {0} podľa pokynov.</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>Prijali sme celkovo registrovaných aplikácií/balíčkov/položiek:{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>Prijímanie bolo neúspešné kvôli veľkému počtu zaslaných požiadaviek. Pokus zopakujeme za približne {0} od teraz.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Dáta boli úspešne prijaté. Na serveri boli celkovo registrovaných nových aplikácií/balíčkov/položiek: {0} ({1} overených)/{2} ({3} overených)/{4} ({5} overených).</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é aplikácie: {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>Overené aplikácie: {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>Overené 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é položky: {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>Overené položky: {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} 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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?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>
|
||||
@@ -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>
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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(PropertyName = "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(PropertyName = "token", Required = Required.Always)]
|
||||
private static string Token => SharedInfo.Token;
|
||||
|
||||
[JsonProperty(PropertyName = "apps", Required = Required.Always)]
|
||||
private readonly ImmutableDictionary<string, string> Apps;
|
||||
[JsonProperty(PropertyName = "v", Required = Required.Always)]
|
||||
private static byte Version => SharedInfo.ApiVersion;
|
||||
|
||||
[JsonProperty(PropertyName = "depots", Required = Required.Always)]
|
||||
private readonly ImmutableDictionary<string, string> Depots;
|
||||
[JsonProperty(PropertyName = "apps", Required = Required.Always)]
|
||||
private readonly ImmutableDictionary<string, string> Apps;
|
||||
|
||||
private readonly ulong SteamID;
|
||||
[JsonProperty(PropertyName = "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(PropertyName = "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(PropertyName = "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(app => app.Key.ToString(CultureInfo.InvariantCulture), app => app.Value.ToString(CultureInfo.InvariantCulture));
|
||||
Subs = accessTokens.ToImmutableDictionary(package => package.Key.ToString(CultureInfo.InvariantCulture), package => package.Value.ToString(CultureInfo.InvariantCulture));
|
||||
Depots = depots.ToImmutableDictionary(depot => depot.Key.ToString(CultureInfo.InvariantCulture), 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(PropertyName = "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(PropertyName = "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(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 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
|
||||
|
||||
@@ -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,17 @@
|
||||
// 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 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
|
||||
|
||||
internal static bool HasValidToken => Token.Length == 128;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal SteamTokenDumperConfig() { }
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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" />
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,9 +19,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using ArchiSteamFarm.Compatibility;
|
||||
#endif
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -29,487 +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(item => item.RealAppID == relevantAppID)
|
||||
.GroupBy(item => (item.RealAppID, item.ContextID, item.ClassID))
|
||||
.ToDictionary(grouping => grouping.Key, grouping => (uint) grouping.Sum(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(asset => (asset.RealAppID, asset.ContextID, asset.ClassID)).ToDictionary(group => group.Key, group => group.Sum(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(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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
52
ArchiSteamFarm.Tests/Utilities.cs
Normal file
52
ArchiSteamFarm.Tests/Utilities.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// 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.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using static ArchiSteamFarm.Core.Utilities;
|
||||
|
||||
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);
|
||||
|
||||
[TestMethod]
|
||||
public void ContextSpecificWordsWeakenPassphrases() => Assert.IsTrue(TestPasswordStrength("archisteamfarmpassword").IsWeak);
|
||||
|
||||
[TestMethod]
|
||||
public void LongPassphraseIsNotWeak() => Assert.IsFalse(TestPasswordStrength("10chars<!>asdf").IsWeak);
|
||||
|
||||
[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);
|
||||
}
|
||||
#pragma warning restore CA1724 // We don't care about the potential conflict, as ASF class name has a priority
|
||||
@@ -16,6 +16,7 @@ EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
DebugFast|Any CPU = DebugFast|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
@@ -24,22 +25,32 @@ Global
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{1501FF07-0114-473F-BDF2-7F8BA34C2948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1501FF07-0114-473F-BDF2-7F8BA34C2948}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1501FF07-0114-473F-BDF2-7F8BA34C2948}.DebugFast|Any CPU.ActiveCfg = DebugFast|Any CPU
|
||||
{1501FF07-0114-473F-BDF2-7F8BA34C2948}.DebugFast|Any CPU.Build.0 = DebugFast|Any CPU
|
||||
{1501FF07-0114-473F-BDF2-7F8BA34C2948}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1501FF07-0114-473F-BDF2-7F8BA34C2948}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D552661C-1356-4B82-A019-B83DC260ECF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D552661C-1356-4B82-A019-B83DC260ECF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D552661C-1356-4B82-A019-B83DC260ECF4}.DebugFast|Any CPU.ActiveCfg = DebugFast|Any CPU
|
||||
{D552661C-1356-4B82-A019-B83DC260ECF4}.DebugFast|Any CPU.Build.0 = DebugFast|Any CPU
|
||||
{D552661C-1356-4B82-A019-B83DC260ECF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D552661C-1356-4B82-A019-B83DC260ECF4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6AE042DA-1152-4F3F-A197-65FD7509E8C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6AE042DA-1152-4F3F-A197-65FD7509E8C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6AE042DA-1152-4F3F-A197-65FD7509E8C6}.DebugFast|Any CPU.ActiveCfg = DebugFast|Any CPU
|
||||
{6AE042DA-1152-4F3F-A197-65FD7509E8C6}.DebugFast|Any CPU.Build.0 = DebugFast|Any CPU
|
||||
{6AE042DA-1152-4F3F-A197-65FD7509E8C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6AE042DA-1152-4F3F-A197-65FD7509E8C6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0B8906EC-DF72-4CB8-9ECE-34961C056680}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0B8906EC-DF72-4CB8-9ECE-34961C056680}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0B8906EC-DF72-4CB8-9ECE-34961C056680}.DebugFast|Any CPU.ActiveCfg = DebugFast|Any CPU
|
||||
{0B8906EC-DF72-4CB8-9ECE-34961C056680}.DebugFast|Any CPU.Build.0 = DebugFast|Any CPU
|
||||
{0B8906EC-DF72-4CB8-9ECE-34961C056680}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0B8906EC-DF72-4CB8-9ECE-34961C056680}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{324E42B1-3C5D-421D-A600-1BEEE7AF401A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{324E42B1-3C5D-421D-A600-1BEEE7AF401A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{324E42B1-3C5D-421D-A600-1BEEE7AF401A}.DebugFast|Any CPU.ActiveCfg = DebugFast|Any CPU
|
||||
{324E42B1-3C5D-421D-A600-1BEEE7AF401A}.DebugFast|Any CPU.Build.0 = DebugFast|Any CPU
|
||||
{324E42B1-3C5D-421D-A600-1BEEE7AF401A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{324E42B1-3C5D-421D-A600-1BEEE7AF401A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,20 +6,15 @@
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp.XPath" />
|
||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
|
||||
<PackageReference Include="CryptSharpStandard" />
|
||||
<PackageReference Include="Humanizer" />
|
||||
<PackageReference Include="JetBrains.Annotations" />
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
|
||||
<PackageReference Include="Markdig.Signed" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="Nito.AsyncEx.Coordination" />
|
||||
<PackageReference Include="NLog" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" />
|
||||
<PackageReference Include="SteamKit2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
@@ -27,18 +22,18 @@
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" />
|
||||
<PackageReference Include="System.Composition" />
|
||||
<PackageReference Include="System.Linq.Async" />
|
||||
<PackageReference Include="zxcvbn-core" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'net48'">
|
||||
<PackageReference Include="System.IO.FileSystem.AccessControl" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||
<PackageReference Include="IndexRange" />
|
||||
<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" />
|
||||
|
||||
@@ -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,5 +23,11 @@ using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: CLSCompliant(false)]
|
||||
|
||||
#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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(nameof(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 _)) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +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.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ArchiSteamFarm.Compatibility {
|
||||
[PublicAPI]
|
||||
public static class File {
|
||||
public static Task AppendAllTextAsync(string path, string contents) {
|
||||
#if NETFRAMEWORK
|
||||
System.IO.File.AppendAllText(path, contents);
|
||||
|
||||
return Task.CompletedTask;
|
||||
#else
|
||||
return System.IO.File.AppendAllTextAsync(path, contents);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void Move(string sourceFileName, string destFileName, bool overwrite) {
|
||||
#if NETFRAMEWORK
|
||||
if (overwrite && System.IO.File.Exists(destFileName)) {
|
||||
System.IO.File.Delete(destFileName);
|
||||
}
|
||||
|
||||
System.IO.File.Move(sourceFileName, destFileName);
|
||||
#else
|
||||
System.IO.File.Move(sourceFileName, destFileName, overwrite);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static Task<byte[]> ReadAllBytesAsync(string path) =>
|
||||
#if NETFRAMEWORK
|
||||
Task.FromResult(System.IO.File.ReadAllBytes(path));
|
||||
#else
|
||||
System.IO.File.ReadAllBytesAsync(path);
|
||||
#endif
|
||||
|
||||
public static Task<string> ReadAllTextAsync(string path) =>
|
||||
#if NETFRAMEWORK
|
||||
Task.FromResult(System.IO.File.ReadAllText(path));
|
||||
#else
|
||||
System.IO.File.ReadAllTextAsync(path);
|
||||
#endif
|
||||
|
||||
public static Task WriteAllTextAsync(string path, string contents) {
|
||||
#if NETFRAMEWORK
|
||||
System.IO.File.WriteAllText(path, contents);
|
||||
|
||||
return Task.CompletedTask;
|
||||
#else
|
||||
return System.IO.File.WriteAllTextAsync(path, contents);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +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.
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
using System.Text;
|
||||
#endif
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ArchiSteamFarm.Compatibility {
|
||||
[PublicAPI]
|
||||
public static class Path {
|
||||
public static string GetRelativePath(string relativeTo, string path) {
|
||||
#if NETFRAMEWORK
|
||||
if (string.IsNullOrEmpty(relativeTo)) {
|
||||
throw new ArgumentNullException(nameof(relativeTo));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path)) {
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
StringComparison comparisonType = PathInternalNetCore.StringComparison;
|
||||
|
||||
relativeTo = System.IO.Path.GetFullPath(relativeTo);
|
||||
path = System.IO.Path.GetFullPath(path);
|
||||
|
||||
// Need to check if the roots are different- if they are we need to return the "to" path.
|
||||
if (!PathInternalNetCore.AreRootsEqual(relativeTo, path, comparisonType)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
int commonLength = PathInternalNetCore.GetCommonPathLength(
|
||||
relativeTo, path,
|
||||
comparisonType == StringComparison.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
// If there is nothing in common they can't share the same root, return the "to" path as is.
|
||||
if (commonLength == 0) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Trailing separators aren't significant for comparison
|
||||
int relativeToLength = relativeTo.Length;
|
||||
|
||||
if (PathInternalNetCore.EndsInDirectorySeparator(relativeTo)) {
|
||||
relativeToLength--;
|
||||
}
|
||||
|
||||
bool pathEndsInSeparator = PathInternalNetCore.EndsInDirectorySeparator(path);
|
||||
int pathLength = path.Length;
|
||||
|
||||
if (pathEndsInSeparator) {
|
||||
pathLength--;
|
||||
}
|
||||
|
||||
// If we have effectively the same path, return "."
|
||||
if ((relativeToLength == pathLength) && (commonLength >= relativeToLength)) {
|
||||
return ".";
|
||||
}
|
||||
|
||||
// We have the same root, we need to calculate the difference now using the
|
||||
// common Length and Segment count past the length.
|
||||
//
|
||||
// Some examples:
|
||||
//
|
||||
// C:\Foo C:\Bar L3, S1 -> ..\Bar
|
||||
// C:\Foo C:\Foo\Bar L6, S0 -> Bar
|
||||
// C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar
|
||||
// C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar
|
||||
|
||||
StringBuilder sb = new(); //StringBuilderCache.Acquire(Math.Max(relativeTo.Length, path.Length));
|
||||
|
||||
// Add parent segments for segments past the common on the "from" path
|
||||
if (commonLength < relativeToLength) {
|
||||
sb.Append("..");
|
||||
|
||||
for (int i = commonLength + 1; i < relativeToLength; i++) {
|
||||
if (PathInternalNetCore.IsDirectorySeparator(relativeTo[i])) {
|
||||
sb.Append(System.IO.Path.DirectorySeparatorChar);
|
||||
sb.Append("..");
|
||||
}
|
||||
}
|
||||
} else if (PathInternalNetCore.IsDirectorySeparator(path[commonLength])) {
|
||||
// No parent segments and we need to eat the initial separator
|
||||
// (C:\Foo C:\Foo\Bar case)
|
||||
commonLength++;
|
||||
}
|
||||
|
||||
// Now add the rest of the "to" path, adding back the trailing separator
|
||||
int differenceLength = pathLength - commonLength;
|
||||
|
||||
if (pathEndsInSeparator) {
|
||||
differenceLength++;
|
||||
}
|
||||
|
||||
if (differenceLength > 0) {
|
||||
if (sb.Length > 0) {
|
||||
sb.Append(System.IO.Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
sb.Append(path, commonLength, differenceLength);
|
||||
}
|
||||
|
||||
return sb.ToString(); //StringBuilderCache.GetStringAndRelease(sb);
|
||||
#else
|
||||
return System.IO.Path.GetRelativePath(relativeTo, path);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,173 +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.
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ArchiSteamFarm.Compatibility {
|
||||
internal static class PathInternalNetCore {
|
||||
private const string ExtendedDevicePathPrefix = @"\\?\";
|
||||
private const string UncExtendedPathPrefix = @"\\?\UNC\";
|
||||
|
||||
internal static StringComparison StringComparison => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the two paths have the same root
|
||||
/// </summary>
|
||||
internal static bool AreRootsEqual(string first, string second, StringComparison comparisonType) {
|
||||
int firstRootLength = GetRootLength(first);
|
||||
int secondRootLength = GetRootLength(second);
|
||||
|
||||
return (firstRootLength == secondRootLength)
|
||||
&& (string.Compare(
|
||||
first,
|
||||
0,
|
||||
second,
|
||||
0,
|
||||
firstRootLength,
|
||||
comparisonType
|
||||
) == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the path ends in a directory separator.
|
||||
/// </summary>
|
||||
internal static bool EndsInDirectorySeparator(string path) => (path.Length > 0) && IsDirectorySeparator(path[^1]);
|
||||
|
||||
/// <summary>
|
||||
/// Get the common path length from the start of the string.
|
||||
/// </summary>
|
||||
internal static int GetCommonPathLength(string first, string second, bool ignoreCase) {
|
||||
int commonChars = EqualStartingCharacterCount(first, second, ignoreCase);
|
||||
|
||||
// If nothing matches
|
||||
if (commonChars == 0) {
|
||||
return commonChars;
|
||||
}
|
||||
|
||||
// Or we're a full string and equal length or match to a separator
|
||||
if ((commonChars == first.Length)
|
||||
&& ((commonChars == second.Length) || IsDirectorySeparator(second[commonChars]))) {
|
||||
return commonChars;
|
||||
}
|
||||
|
||||
if ((commonChars == second.Length) && IsDirectorySeparator(first[commonChars])) {
|
||||
return commonChars;
|
||||
}
|
||||
|
||||
// It's possible we matched somewhere in the middle of a segment e.g. C:\Foodie and C:\Foobar.
|
||||
while ((commonChars > 0) && !IsDirectorySeparator(first[commonChars - 1])) {
|
||||
commonChars--;
|
||||
}
|
||||
|
||||
return commonChars;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the given character is a directory separator.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static bool IsDirectorySeparator(char c) => (c == System.IO.Path.DirectorySeparatorChar) || (c == System.IO.Path.AltDirectorySeparatorChar);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of common characters from the left optionally ignoring case
|
||||
/// </summary>
|
||||
private static unsafe int EqualStartingCharacterCount(string first, string second, bool ignoreCase) {
|
||||
if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int commonChars = 0;
|
||||
|
||||
fixed (char* f = first)
|
||||
fixed (char* s = second) {
|
||||
char* l = f;
|
||||
char* r = s;
|
||||
char* leftEnd = l + first.Length;
|
||||
char* rightEnd = r + second.Length;
|
||||
|
||||
while ((l != leftEnd) && (r != rightEnd)
|
||||
&& ((*l == *r) || (ignoreCase &&
|
||||
(char.ToUpperInvariant(*l) == char.ToUpperInvariant(*r))))) {
|
||||
commonChars++;
|
||||
l++;
|
||||
r++;
|
||||
}
|
||||
}
|
||||
|
||||
return commonChars;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length of the root of the path (drive, share, etc.).
|
||||
/// </summary>
|
||||
private static int GetRootLength(string path) {
|
||||
int i = 0;
|
||||
int volumeSeparatorLength = 2; // Length to the colon "C:"
|
||||
int uncRootLength = 2; // Length to the start of the server name "\\"
|
||||
|
||||
bool extendedSyntax = path.StartsWith(ExtendedDevicePathPrefix, StringComparison.Ordinal);
|
||||
bool extendedUncSyntax = path.StartsWith(UncExtendedPathPrefix, StringComparison.Ordinal);
|
||||
|
||||
if (extendedSyntax) {
|
||||
// Shift the position we look for the root from to account for the extended prefix
|
||||
if (extendedUncSyntax) {
|
||||
// "\\" -> "\\?\UNC\"
|
||||
uncRootLength = UncExtendedPathPrefix.Length;
|
||||
} else {
|
||||
// "C:" -> "\\?\C:"
|
||||
volumeSeparatorLength += ExtendedDevicePathPrefix.Length;
|
||||
}
|
||||
}
|
||||
|
||||
if ((!extendedSyntax || extendedUncSyntax) && (path.Length > 0) && IsDirectorySeparator(path[0])) {
|
||||
// UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")
|
||||
|
||||
i = 1; // Drive rooted (\foo) is one character
|
||||
|
||||
if (extendedUncSyntax || ((path.Length > 1) && IsDirectorySeparator(path[1]))) {
|
||||
// UNC (\\?\UNC\ or \\), scan past the next two directory separators at most
|
||||
// (e.g. to \\?\UNC\Server\Share or \\Server\Share\)
|
||||
i = uncRootLength;
|
||||
int n = 2; // Maximum separators to skip
|
||||
|
||||
while ((i < path.Length) && (!IsDirectorySeparator(path[i]) || (--n > 0))) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
} else if ((path.Length >= volumeSeparatorLength) &&
|
||||
(path[volumeSeparatorLength - 1] == System.IO.Path.VolumeSeparatorChar)) {
|
||||
// Path is at least longer than where we expect a colon, and has a colon (\\?\A:, A:)
|
||||
// If the colon is followed by a directory separator, move past it
|
||||
i = volumeSeparatorLength;
|
||||
|
||||
if ((path.Length >= volumeSeparatorLength + 1) && IsDirectorySeparator(path[volumeSeparatorLength])) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,207 +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.
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.WebSockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
#endif
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ArchiSteamFarm.Compatibility {
|
||||
[PublicAPI]
|
||||
public static class StaticHelpers {
|
||||
#if NETFRAMEWORK
|
||||
private static readonly DateTime SavedProcessStartTime = DateTime.UtcNow;
|
||||
#endif
|
||||
|
||||
#if NETFRAMEWORK
|
||||
public static bool IsRunningOnMono => Type.GetType("Mono.Runtime") != null;
|
||||
#else
|
||||
public static bool IsRunningOnMono => false;
|
||||
#endif
|
||||
|
||||
public static DateTime ProcessStartTime {
|
||||
get {
|
||||
#if NETFRAMEWORK
|
||||
if (IsRunningOnMono) {
|
||||
return SavedProcessStartTime;
|
||||
}
|
||||
#endif
|
||||
|
||||
using Process process = Process.GetCurrentProcess();
|
||||
|
||||
return process.StartTime;
|
||||
}
|
||||
}
|
||||
|
||||
#if NETFRAMEWORK
|
||||
public static Task<byte[]> ComputeHashAsync(this HashAlgorithm hashAlgorithm, Stream inputStream) {
|
||||
if (hashAlgorithm == null) {
|
||||
throw new ArgumentNullException(nameof(hashAlgorithm));
|
||||
}
|
||||
|
||||
return Task.FromResult(hashAlgorithm.ComputeHash(inputStream));
|
||||
}
|
||||
|
||||
public static IAsyncDisposable ConfigureAwait(this IDisposable source, bool _) {
|
||||
if (source == null) {
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
return new AsyncDisposableWrapper(source);
|
||||
}
|
||||
|
||||
public static IWebHostBuilder ConfigureWebHostDefaults(this IWebHostBuilder builder, Action<IWebHostBuilder> configure) {
|
||||
if (configure == null) {
|
||||
throw new ArgumentNullException(nameof(configure));
|
||||
}
|
||||
|
||||
configure(builder);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static bool Contains(this string input, string value, StringComparison comparisonType) {
|
||||
if (input == null) {
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
}
|
||||
|
||||
return input.IndexOf(value, comparisonType) >= 0;
|
||||
}
|
||||
|
||||
// ReSharper disable once UseDeconstructionOnParameter - we actually implement deconstruction here
|
||||
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kv, out TKey key, out TValue value) {
|
||||
key = kv.Key;
|
||||
value = kv.Value;
|
||||
}
|
||||
|
||||
public static ValueTask DisposeAsync(this IDisposable disposable) {
|
||||
if (disposable == null) {
|
||||
throw new ArgumentNullException(nameof(disposable));
|
||||
}
|
||||
|
||||
disposable.Dispose();
|
||||
|
||||
return default(ValueTask);
|
||||
}
|
||||
|
||||
public static int IndexOf(this string source, char value, StringComparison comparisonType) {
|
||||
if (source == null) {
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
return source.IndexOf(value.ToString(), comparisonType);
|
||||
}
|
||||
|
||||
public static async Task<int> ReadAsync(this Stream stream, ReadOnlyMemory<byte> buffer) {
|
||||
if (stream == null) {
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
}
|
||||
|
||||
byte[] byteArray = buffer.ToArray();
|
||||
|
||||
return await stream.ReadAsync(byteArray, 0, byteArray.Length).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task<WebSocketReceiveResult> ReceiveAsync(this WebSocket webSocket, byte[] buffer, CancellationToken cancellationToken) {
|
||||
if (webSocket == null) {
|
||||
throw new ArgumentNullException(nameof(webSocket));
|
||||
}
|
||||
|
||||
return await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static string Replace(this string source, string oldValue, string? newValue, StringComparison comparisonType) {
|
||||
if (source == null) {
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
if (oldValue == null) {
|
||||
throw new ArgumentNullException(nameof(oldValue));
|
||||
}
|
||||
|
||||
if (oldValue.Length == 0) {
|
||||
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(oldValue)), nameof(oldValue));
|
||||
}
|
||||
|
||||
int startIndex = 0;
|
||||
|
||||
while (true) {
|
||||
if (startIndex >= source.Length) {
|
||||
return source;
|
||||
}
|
||||
|
||||
int index = source.IndexOf(oldValue, startIndex, comparisonType);
|
||||
|
||||
if (index < 0) {
|
||||
return source;
|
||||
}
|
||||
|
||||
startIndex = index;
|
||||
|
||||
source = source.Remove(index, oldValue.Length);
|
||||
|
||||
if (!string.IsNullOrEmpty(newValue)) {
|
||||
source = source.Insert(index, newValue!);
|
||||
startIndex += newValue!.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SendAsync(this WebSocket webSocket, byte[] buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) {
|
||||
if (webSocket == null) {
|
||||
throw new ArgumentNullException(nameof(webSocket));
|
||||
}
|
||||
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(buffer), messageType, endOfMessage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static string[] Split(this string text, char separator, StringSplitOptions options = StringSplitOptions.None) {
|
||||
if (text == null) {
|
||||
throw new ArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
return text.Split(new[] { separator }, options);
|
||||
}
|
||||
|
||||
public static void TrimExcess<TKey, TValue>(this Dictionary<TKey, TValue> _) { } // no-op
|
||||
|
||||
public static async Task WriteAsync(this Stream stream, ReadOnlyMemory<byte> buffer) {
|
||||
if (stream == null) {
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
}
|
||||
|
||||
byte[] byteArray = buffer.ToArray();
|
||||
|
||||
await stream.WriteAsync(byteArray, 0, byteArray.Length).ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
77
ArchiSteamFarm/Core/AprilFools.cs
Normal file
77
ArchiSteamFarm/Core/AprilFools.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// 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.Globalization;
|
||||
using System.Threading;
|
||||
|
||||
namespace ArchiSteamFarm.Core;
|
||||
|
||||
internal static class AprilFools {
|
||||
private static readonly object LockObject = new();
|
||||
|
||||
// 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;
|
||||
|
||||
private static readonly Timer Timer = new(Init);
|
||||
|
||||
internal static void Init(object? state = null) {
|
||||
DateTime now = DateTime.Now;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
270
ArchiSteamFarm/Core/ArchiNet.cs
Normal file
270
ArchiSteamFarm/Core/ArchiNet.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// 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.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(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) },
|
||||
{ "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) {
|
||||
if (version == null) {
|
||||
throw new ArgumentNullException(nameof(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 == 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 {
|
||||
#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() { }
|
||||
}
|
||||
|
||||
[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() { }
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(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(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,16 +19,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using ArchiSteamFarm.Compatibility;
|
||||
using File = System.IO.File;
|
||||
#endif
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
@@ -37,189 +35,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 string Version {
|
||||
get {
|
||||
if (!string.IsNullOrEmpty(BackingVersion)) {
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
return BackingVersion!;
|
||||
}
|
||||
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));
|
||||
|
||||
string framework = RuntimeInformation.FrameworkDescription.Trim();
|
||||
internal static DateTime ProcessStartTime {
|
||||
#if NETFRAMEWORK
|
||||
get => RuntimeMadness.ProcessStartTime.ToUniversalTime();
|
||||
#else
|
||||
get {
|
||||
using Process process = Process.GetCurrentProcess();
|
||||
|
||||
if (framework.Length == 0) {
|
||||
framework = "Unknown Framework";
|
||||
}
|
||||
return process.StartTime.ToUniversalTime();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static string Version {
|
||||
get {
|
||||
if (!string.IsNullOrEmpty(BackingVersion)) {
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
return BackingVersion!;
|
||||
}
|
||||
|
||||
string framework = RuntimeInformation.FrameworkDescription.Trim();
|
||||
|
||||
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";
|
||||
}
|
||||
if (runtime.Length == 0) {
|
||||
runtime = "Unknown Runtime";
|
||||
}
|
||||
#endif
|
||||
|
||||
string description = RuntimeInformation.OSDescription.Trim();
|
||||
string description = RuntimeInformation.OSDescription.Trim();
|
||||
|
||||
if (description.Length == 0) {
|
||||
description = "Unknown OS";
|
||||
}
|
||||
if (description.Length == 0) {
|
||||
description = "Unknown OS";
|
||||
}
|
||||
|
||||
BackingVersion = framework + "; " + runtime + "; " + description;
|
||||
BackingVersion = $"{framework}; {runtime}; {description}";
|
||||
|
||||
return BackingVersion;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? BackingVersion;
|
||||
private static Mutex? SingleInstance;
|
||||
|
||||
internal static void CoreInit(bool systemRequired) {
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||
if (systemRequired) {
|
||||
WindowsKeepSystemActive();
|
||||
}
|
||||
|
||||
if (!Console.IsOutputRedirected) {
|
||||
// Normally we should use UTF-8 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
|
||||
// We could in theory conditionally use UTF-8 for Windows 10+ and unicode otherwise, but Windows version detection is simply not worth the hassle in this case
|
||||
// Therefore, until we can drop support for Windows < 10, we'll stick with Unicode for all Windows boxes, unless there will be valid reasoning for conditional switch
|
||||
// See https://github.com/JustArchiNET/ArchiSteamFarm/issues/1289 for more details
|
||||
Console.OutputEncoding = 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));
|
||||
}
|
||||
|
||||
internal static string GetOsResourceName(string objectName) {
|
||||
if (string.IsNullOrEmpty(objectName)) {
|
||||
throw new ArgumentNullException(nameof(objectName));
|
||||
}
|
||||
return $"{SharedInfo.AssemblyName}-{objectName}";
|
||||
}
|
||||
|
||||
return SharedInfo.AssemblyName + "-" + objectName;
|
||||
internal static void Init(GlobalConfig.EOptimizationMode optimizationMode) {
|
||||
if (!Enum.IsDefined(optimizationMode)) {
|
||||
throw new ArgumentNullException(nameof(optimizationMode));
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
internal static async Task<bool> RegisterProcess() {
|
||||
if (SingleInstance != null) {
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
internal static void UnixSetFileAccess(string path, EUnixPermission permission) {
|
||||
if (string.IsNullOrEmpty(path)) {
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
if (SharedInfo.BuildInfo.Variant.EndsWith("-netf", StringComparison.Ordinal)) {
|
||||
#if NETFRAMEWORK
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && !RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
|
||||
#else
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD) && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && !RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
|
||||
#endif
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
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 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 (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || !StaticHelpers.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,
|
||||
@@ -232,96 +256,147 @@ namespace ArchiSteamFarm.Core {
|
||||
};
|
||||
#else
|
||||
|
||||
// This is .NET Core build, we support all scenarios
|
||||
return true;
|
||||
// .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
|
||||
}
|
||||
|
||||
private static void WindowsDisableQuickEditMode() {
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||
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);
|
||||
}
|
||||
if (SharedInfo.BuildInfo.Variant == "generic") {
|
||||
// Generic is supported everywhere
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void WindowsKeepSystemActive() {
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||
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));
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
[Flags]
|
||||
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
|
||||
if (SharedInfo.BuildInfo.Variant.StartsWith("osx-", StringComparison.Ordinal)) {
|
||||
// OS-specific OS X build is supported only on OS X
|
||||
return OperatingSystem.IsMacOS();
|
||||
}
|
||||
|
||||
private static class NativeMethods {
|
||||
internal const EExecutionState AwakeExecutionState = EExecutionState.SystemRequired | EExecutionState.AwayModeRequired | EExecutionState.Continuous;
|
||||
internal const uint EnableQuickEditMode = 0x0040;
|
||||
internal const sbyte StandardInputHandle = -10;
|
||||
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")]
|
||||
internal const EExecutionState AwakeExecutionState = EExecutionState.SystemRequired | EExecutionState.AwayModeRequired | EExecutionState.Continuous;
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal const uint EnableQuickEditMode = 0x0040;
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal const sbyte StandardInputHandle = -10;
|
||||
|
||||
#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)]
|
||||
internal static extern int Chmod(string path, int mode);
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[DllImport("libc", EntryPoint = "chmod", SetLastError = true)]
|
||||
[SupportedOSPlatform("FreeBSD")]
|
||||
[SupportedOSPlatform("Linux")]
|
||||
[SupportedOSPlatform("MacOS")]
|
||||
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
|
||||
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[DllImport("kernel32.dll")]
|
||||
internal static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[DllImport("kernel32.dll")]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
|
||||
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[DllImport("kernel32.dll")]
|
||||
internal static extern IntPtr GetStdHandle(int nStdHandle);
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[DllImport("libc", EntryPoint = "geteuid", SetLastError = true)]
|
||||
[SupportedOSPlatform("FreeBSD")]
|
||||
[SupportedOSPlatform("Linux")]
|
||||
[SupportedOSPlatform("MacOS")]
|
||||
internal static extern uint GetEUID();
|
||||
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[DllImport("kernel32.dll")]
|
||||
internal static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[DllImport("kernel32.dll")]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal static extern IntPtr GetStdHandle(int nStdHandle);
|
||||
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[DllImport("kernel32.dll")]
|
||||
internal static extern EExecutionState SetThreadExecutionState(EExecutionState executionState);
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[DllImport("kernel32.dll")]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
|
||||
|
||||
[Flags]
|
||||
internal enum EExecutionState : uint {
|
||||
None = 0,
|
||||
SystemRequired = 0x00000001,
|
||||
AwayModeRequired = 0x00000040,
|
||||
Continuous = 0x80000000
|
||||
}
|
||||
[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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user