mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2025-12-21 00:38:37 +00:00
Compare commits
848 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3229fa45f | ||
|
|
5c6ca3fee2 | ||
|
|
179affd49c | ||
|
|
79a4638eea | ||
|
|
feede84577 | ||
|
|
f3f71cfb27 | ||
|
|
a785ae3536 | ||
|
|
8663bd1eb4 | ||
|
|
b869df538a | ||
|
|
e8e7d0f1cb | ||
|
|
bea85a3014 | ||
|
|
07972e3714 | ||
|
|
13141f35a7 | ||
|
|
47ace2e526 | ||
|
|
f44556a863 | ||
|
|
fa34c853b7 | ||
|
|
6b6c60de92 | ||
|
|
4d5152a6ae | ||
|
|
d74ae770fe | ||
|
|
b8d7f24d50 | ||
|
|
5063bda7ae | ||
|
|
946308366a | ||
|
|
7e84d26d5c | ||
|
|
7cae68f14a | ||
|
|
f0a2c26a1a | ||
|
|
d146525d9c | ||
|
|
0b2cdb63b2 | ||
|
|
c7a1713066 | ||
|
|
cf23819b48 | ||
|
|
78d5234047 | ||
|
|
a0d7ef5856 | ||
|
|
1dc2b1e06e | ||
|
|
70fe873eaf | ||
|
|
56754053c3 | ||
|
|
cb883cf235 | ||
|
|
661786adf2 | ||
|
|
6c63b6db68 | ||
|
|
dfdb0a22a0 | ||
|
|
41ecfb1d02 | ||
|
|
25b29cd56e | ||
|
|
ab5fb6dd1b | ||
|
|
75e4557da3 | ||
|
|
95c3658197 | ||
|
|
26e7f7deb5 | ||
|
|
f003dcda0b | ||
|
|
296318060f | ||
|
|
f80b114892 | ||
|
|
604652c03d | ||
|
|
2420d34b22 | ||
|
|
21a5793c45 | ||
|
|
888b45c919 | ||
|
|
35bf243f1a | ||
|
|
f4c1dededc | ||
|
|
d5f355a2bc | ||
|
|
eb9b5dd025 | ||
|
|
2cec35f911 | ||
|
|
7d4438b089 | ||
|
|
2782329549 | ||
|
|
14ac124e0c | ||
|
|
ec07d23cc4 | ||
|
|
3299ba8c10 | ||
|
|
4eb09f950d | ||
|
|
144a1d1574 | ||
|
|
05f3aada38 | ||
|
|
9240500e2c | ||
|
|
98768970a7 | ||
|
|
7e41e530e7 | ||
|
|
97198fd435 | ||
|
|
e8b83b8ad4 | ||
|
|
929a1dfd82 | ||
|
|
ea7bad2868 | ||
|
|
71f54a79f3 | ||
|
|
0cd7b10c9a | ||
|
|
42fb71f856 | ||
|
|
7b3ae25d58 | ||
|
|
2961975f05 | ||
|
|
06843ebf9f | ||
|
|
6ca395795c | ||
|
|
0e5490cc3a | ||
|
|
39cc6e6ea6 | ||
|
|
70544d1d76 | ||
|
|
b1e9a53adc | ||
|
|
72e59e7271 | ||
|
|
1ae3517374 | ||
|
|
975b3b89f3 | ||
|
|
a982520844 | ||
|
|
0ed4c7536a | ||
|
|
6e360b7e1a | ||
|
|
17c79904e4 | ||
|
|
0fcbc8c402 | ||
|
|
9e0d44bee2 | ||
|
|
a093f24a9d | ||
|
|
e6a51bae55 | ||
|
|
a992d0c3cd | ||
|
|
a0e5ae8f46 | ||
|
|
c4a46fbdde | ||
|
|
d899dbc18c | ||
|
|
04e14293ef | ||
|
|
18a1b0a883 | ||
|
|
5ca028ef47 | ||
|
|
43ec8f9566 | ||
|
|
32f5b3a1c5 | ||
|
|
196afbf276 | ||
|
|
1f0e4c9058 | ||
|
|
0ded9698b2 | ||
|
|
9825f007c0 | ||
|
|
bc38ba478d | ||
|
|
2f22757fea | ||
|
|
3ff0468926 | ||
|
|
a6a973468c | ||
|
|
4aa1604dfb | ||
|
|
44c7fcd131 | ||
|
|
ce610ab24d | ||
|
|
c76f17c5c7 | ||
|
|
30b4e006dc | ||
|
|
39621ed46e | ||
|
|
e532b57369 | ||
|
|
b117c5164d | ||
|
|
be5a6bc27a | ||
|
|
0ecb04e62c | ||
|
|
0af9f99923 | ||
|
|
cd0078e83e | ||
|
|
10cedad0ee | ||
|
|
693f4edbe5 | ||
|
|
ed44ad030e | ||
|
|
d338477e5c | ||
|
|
053cb5fc03 | ||
|
|
a01ac6641e | ||
|
|
3deb560e5e | ||
|
|
1861add350 | ||
|
|
83fac5b115 | ||
|
|
23e49dafbc | ||
|
|
b71462b151 | ||
|
|
88d3b19196 | ||
|
|
237f23e965 | ||
|
|
776755d3ab | ||
|
|
e1e464b4e7 | ||
|
|
d590a30f20 | ||
|
|
1d520d9071 | ||
|
|
772607b680 | ||
|
|
82750352e2 | ||
|
|
3e5a6a7b32 | ||
|
|
5f803cf725 | ||
|
|
ffdc0e89e8 | ||
|
|
b608083bfc | ||
|
|
ebdb17d71b | ||
|
|
8877468bd4 | ||
|
|
c753ed24cd | ||
|
|
dcebde55a2 | ||
|
|
bd0f1779d6 | ||
|
|
164d9330f0 | ||
|
|
7c1c0d61b4 | ||
|
|
2aab79ec52 | ||
|
|
917df358e8 | ||
|
|
5b62e19a80 | ||
|
|
cb99c916dd | ||
|
|
2596c74d2e | ||
|
|
8147dacae7 | ||
|
|
3ad324ebea | ||
|
|
6e34c14aef | ||
|
|
96c9ab34d6 | ||
|
|
fa6649305e | ||
|
|
c0a3b67ebf | ||
|
|
2a845ab46f | ||
|
|
5ca6e41691 | ||
|
|
6eb9b9b26d | ||
|
|
95a6cef6db | ||
|
|
688e1cea83 | ||
|
|
40a479b1df | ||
|
|
b535886959 | ||
|
|
45d0a8a9c1 | ||
|
|
3ee2ded814 | ||
|
|
a2b5f80f40 | ||
|
|
0fffbdaa52 | ||
|
|
2ec764f8ec | ||
|
|
c67aecacbc | ||
|
|
ae5b9cdc0d | ||
|
|
0eab63bab5 | ||
|
|
1aea4c0550 | ||
|
|
16c4bed95f | ||
|
|
8948817d55 | ||
|
|
6ff943aeaa | ||
|
|
0576bbd3aa | ||
|
|
6e70956ee9 | ||
|
|
77409699f0 | ||
|
|
dae3e93031 | ||
|
|
3041850b92 | ||
|
|
7c00d8d03d | ||
|
|
aedad9d5b3 | ||
|
|
ff7f661197 | ||
|
|
e57cc21b89 | ||
|
|
06bfe01087 | ||
|
|
bcceb0c39c | ||
|
|
996ee66554 | ||
|
|
dad19956aa | ||
|
|
beeda2777d | ||
|
|
53e06a7392 | ||
|
|
9a1d4913a0 | ||
|
|
c65b40b45b | ||
|
|
23647f2e39 | ||
|
|
feb7a72bd1 | ||
|
|
7fe5989f5d | ||
|
|
715ed034df | ||
|
|
d82df0074f | ||
|
|
03c2ba049e | ||
|
|
03bce5dd71 | ||
|
|
023e38d5e0 | ||
|
|
6178b12bb1 | ||
|
|
df95b82b10 | ||
|
|
93ac0b4e4a | ||
|
|
38fc3ba6a3 | ||
|
|
2e87b78b45 | ||
|
|
dae256f069 | ||
|
|
8452c46c47 | ||
|
|
68e30b43c2 | ||
|
|
c9b1e46013 | ||
|
|
fd9770d78e | ||
|
|
a4374389b8 | ||
|
|
082cab42df | ||
|
|
3ff80b37f3 | ||
|
|
07c354f9e7 | ||
|
|
b83f8fc669 | ||
|
|
d6a2f53ab0 | ||
|
|
0261623ea9 | ||
|
|
b5ca484c2b | ||
|
|
a7c30e4878 | ||
|
|
f26a4ae864 | ||
|
|
c2018b53a5 | ||
|
|
55421bb29f | ||
|
|
52eabe4daf | ||
|
|
86fc8a765c | ||
|
|
9162752b99 | ||
|
|
4436e8dc43 | ||
|
|
1f0b996cf5 | ||
|
|
4dc7acb914 | ||
|
|
d570a17532 | ||
|
|
e5184adede | ||
|
|
13a5fa7c02 | ||
|
|
c698fe7b07 | ||
|
|
a08c85e40b | ||
|
|
055af32219 | ||
|
|
080b500ebf | ||
|
|
4a329b0b15 | ||
|
|
e2494960ae | ||
|
|
2e987ccee6 | ||
|
|
99284e22c9 | ||
|
|
37eac5844e | ||
|
|
35f295a860 | ||
|
|
29d047271e | ||
|
|
948a86bfc9 | ||
|
|
f853c61821 | ||
|
|
5b1cb16c98 | ||
|
|
bdac1b2782 | ||
|
|
7532b89fd0 | ||
|
|
7cd351d1cd | ||
|
|
9c8d63318e | ||
|
|
f0c0e07489 | ||
|
|
d2e79ff3a4 | ||
|
|
ab7b998e3b | ||
|
|
9c88d14c8e | ||
|
|
9a3c3bdbaf | ||
|
|
7b3598af20 | ||
|
|
35d2156855 | ||
|
|
d589da7a39 | ||
|
|
c10de94bd0 | ||
|
|
d164296d7e | ||
|
|
b378a76072 | ||
|
|
263a2db476 | ||
|
|
61549fc983 | ||
|
|
19aad04143 | ||
|
|
5a5f3c6786 | ||
|
|
bd68df2fd6 | ||
|
|
2a97644468 | ||
|
|
5304ca3e07 | ||
|
|
f0471ac0eb | ||
|
|
b0e7f1963c | ||
|
|
78990e8aff | ||
|
|
b871970d85 | ||
|
|
d563a20288 | ||
|
|
b2fefa4476 | ||
|
|
840cf25ea4 | ||
|
|
79587f68d7 | ||
|
|
45adf9c1a1 | ||
|
|
1dcf98c849 | ||
|
|
a826b7f9b7 | ||
|
|
85c8397cf7 | ||
|
|
dbf1c1ba51 | ||
|
|
359439e306 | ||
|
|
763766e092 | ||
|
|
a4a347e957 | ||
|
|
844ca93647 | ||
|
|
16fe445ea9 | ||
|
|
34bf8fb84f | ||
|
|
4873cd337a | ||
|
|
8c0249a62d | ||
|
|
220ecf0c38 | ||
|
|
ebd79425f4 | ||
|
|
2eaf934dde | ||
|
|
599cd9bff8 | ||
|
|
339e83a818 | ||
|
|
f083bb2d3b | ||
|
|
9f68d17a28 | ||
|
|
d8413f9633 | ||
|
|
27d9d61309 | ||
|
|
2fc92ce427 | ||
|
|
86d94a7bbe | ||
|
|
9647db8bf7 | ||
|
|
2ce5018d62 | ||
|
|
61768dbeb9 | ||
|
|
37ced5d4e3 | ||
|
|
c1a695de7b | ||
|
|
7040bdabf6 | ||
|
|
14512aec71 | ||
|
|
40d67ac185 | ||
|
|
7de67e84f9 | ||
|
|
6c9df75a1a | ||
|
|
b98d8405e1 | ||
|
|
062b241232 | ||
|
|
b5af510eb9 | ||
|
|
2edcb7a0ce | ||
|
|
58cf93ff48 | ||
|
|
9f0f3339c5 | ||
|
|
6b88d49067 | ||
|
|
b4e102682f | ||
|
|
a580c85234 | ||
|
|
a3cce515ec | ||
|
|
1f21c1f9f6 | ||
|
|
9c7014d5c1 | ||
|
|
0a01dfa22b | ||
|
|
a8bb107e23 | ||
|
|
151f6cfe4a | ||
|
|
d21e398ac0 | ||
|
|
16f7f82dc0 | ||
|
|
8a6a02e034 | ||
|
|
b8bfcd5df3 | ||
|
|
2326196e01 | ||
|
|
380d785388 | ||
|
|
edd82b365c | ||
|
|
d49d106d64 | ||
|
|
78407fbd9c | ||
|
|
1a87149765 | ||
|
|
f260015098 | ||
|
|
5016abe45e | ||
|
|
2238897f37 | ||
|
|
493f40a97c | ||
|
|
5ec7ca050b | ||
|
|
f727403295 | ||
|
|
027d23d894 | ||
|
|
f36798b2c3 | ||
|
|
202a92f66f | ||
|
|
48b2a4c859 | ||
|
|
cfbc3d749f | ||
|
|
a76af71227 | ||
|
|
cc5e5dfcc9 | ||
|
|
a185f2f03d | ||
|
|
3772b303c5 | ||
|
|
d6ed6e81a4 | ||
|
|
0bbc85527a | ||
|
|
1eabe3a5ed | ||
|
|
f95b6bf089 | ||
|
|
ff7b4582c7 | ||
|
|
7e7fb9cd16 | ||
|
|
65bbaf628e | ||
|
|
db2cbde708 | ||
|
|
b701acf72f | ||
|
|
ceb021dbdf | ||
|
|
ce1c77780d | ||
|
|
5b7858c2a0 | ||
|
|
635afa7165 | ||
|
|
c79c314b20 | ||
|
|
ec78ad1ac2 | ||
|
|
9273d73640 | ||
|
|
d598b99a1e | ||
|
|
f12b00bb4c | ||
|
|
ff7116d2ac | ||
|
|
ee435fc628 | ||
|
|
c8f02779b6 | ||
|
|
83667ed7c3 | ||
|
|
387ef3e0dd | ||
|
|
7e8c6b7fb3 | ||
|
|
e97b440225 | ||
|
|
b1db99f328 | ||
|
|
ace0ca4555 | ||
|
|
33767ace78 | ||
|
|
5d5a76de40 | ||
|
|
582680f69d | ||
|
|
e65729ee1a | ||
|
|
83a353dfe0 | ||
|
|
aea7c7640c | ||
|
|
7118185ac5 | ||
|
|
b681b74ee6 | ||
|
|
cc83222c3e | ||
|
|
6c570738ee | ||
|
|
393ddf6f10 | ||
|
|
75a7df2751 | ||
|
|
0c3dfaa4ae | ||
|
|
94e70e5ac2 | ||
|
|
9ce527c938 | ||
|
|
660b05e4c4 | ||
|
|
b39efb2b03 | ||
|
|
53e0b62ced | ||
|
|
d3980962fe | ||
|
|
517787efb8 | ||
|
|
d06afa26d4 | ||
|
|
4562e71e47 | ||
|
|
894471fa82 | ||
|
|
e9f6c15ba1 | ||
|
|
814b93d1cf | ||
|
|
beafbd8f43 | ||
|
|
dbd0e006ed | ||
|
|
4661803836 | ||
|
|
99ecd72660 | ||
|
|
799ec2965f | ||
|
|
c7e9c0c3b0 | ||
|
|
1f3e861612 | ||
|
|
159b0620a7 | ||
|
|
e508602be7 | ||
|
|
6edf62d849 | ||
|
|
021d414143 | ||
|
|
813587508e | ||
|
|
0e3d124663 | ||
|
|
91115b7cb7 | ||
|
|
0f12174564 | ||
|
|
d087aacbfb | ||
|
|
1c0d2d88ed | ||
|
|
6b170c345d | ||
|
|
bc8a4a50d2 | ||
|
|
e025df3d9b | ||
|
|
5a97835531 | ||
|
|
bce0557873 | ||
|
|
4c7cd204ce | ||
|
|
7ba6b230df | ||
|
|
6c4fba5173 | ||
|
|
5cd6477b69 | ||
|
|
1f5fbb5f92 | ||
|
|
d0521ff9ca | ||
|
|
1be15716fc | ||
|
|
e00ee2cc55 | ||
|
|
8893fc8e70 | ||
|
|
86b41f0542 | ||
|
|
a245c091a4 | ||
|
|
abbe0cca22 | ||
|
|
d1c2b103b6 | ||
|
|
9f1734efb7 | ||
|
|
729c2e889c | ||
|
|
a9edc7ad7a | ||
|
|
08a6486c00 | ||
|
|
ca3bc1becd | ||
|
|
fe5028a399 | ||
|
|
c1d9d04071 | ||
|
|
e5ae2abbf0 | ||
|
|
41fa5de5a8 | ||
|
|
697b78aa21 | ||
|
|
1a7be0bac8 | ||
|
|
9d88972ae0 | ||
|
|
aec4130afe | ||
|
|
64228cd3d9 | ||
|
|
3568a0e528 | ||
|
|
38c2b51f2b | ||
|
|
450f365817 | ||
|
|
cf3f6aabdf | ||
|
|
842fb6e304 | ||
|
|
a1169331aa | ||
|
|
2fb7d62e06 | ||
|
|
4e57153e91 | ||
|
|
d2e78b6970 | ||
|
|
ccef6554fe | ||
|
|
97875a87c2 | ||
|
|
2684f99563 | ||
|
|
a50318dc8b | ||
|
|
eeccc36fe4 | ||
|
|
1ead134578 | ||
|
|
f03f8ebe70 | ||
|
|
aa8b360e1d | ||
|
|
8c22f9929c | ||
|
|
3795b2de3a | ||
|
|
f4650fe570 | ||
|
|
fec57e0fff | ||
|
|
8e47a5906f | ||
|
|
f728ddf737 | ||
|
|
173cec5ef7 | ||
|
|
d16c4822eb | ||
|
|
03e3d74e51 | ||
|
|
0a3d011e2e | ||
|
|
f112a05569 | ||
|
|
1c579d96ee | ||
|
|
19a0be1d26 | ||
|
|
a8de495c7c | ||
|
|
ab8ceab055 | ||
|
|
64b72d1e55 | ||
|
|
f807bdb660 | ||
|
|
5b66b70566 | ||
|
|
41c06851a5 | ||
|
|
4dbb964ba9 | ||
|
|
11471c759d | ||
|
|
2aa4ab7fe8 | ||
|
|
dfc055c066 | ||
|
|
1a0ac11f46 | ||
|
|
7266864b3b | ||
|
|
b52f746138 | ||
|
|
a2585ec8c9 | ||
|
|
37781698e0 | ||
|
|
2a8fe7611b | ||
|
|
8fdf14bb10 | ||
|
|
31db72b2d6 | ||
|
|
f28ae15cc9 | ||
|
|
6fcc64dad1 | ||
|
|
e18046084e | ||
|
|
c3c5f33289 | ||
|
|
e03734ef8f | ||
|
|
a7c2ca6bc5 | ||
|
|
171fca42f2 | ||
|
|
e90ac74b16 | ||
|
|
a5ce8bf3d7 | ||
|
|
aad77569a7 | ||
|
|
e74b3e4f78 | ||
|
|
7db44c5835 | ||
|
|
25a88f941d | ||
|
|
2eab00facc | ||
|
|
98e51a4543 | ||
|
|
2ee49db81d | ||
|
|
aab397dd2d | ||
|
|
7426fafcb0 | ||
|
|
270bd7ae26 | ||
|
|
4c3713c19f | ||
|
|
5791b1e552 | ||
|
|
5c59236a09 | ||
|
|
a7119bba89 | ||
|
|
3b64e14489 | ||
|
|
5f36ca91d7 | ||
|
|
5a2cd25fa1 | ||
|
|
20a5d509a7 | ||
|
|
0c457e7f3e | ||
|
|
4e6014d652 | ||
|
|
1436fb6d6a | ||
|
|
e2578c7960 | ||
|
|
8fb1a2e1ea | ||
|
|
3e2951d1d0 | ||
|
|
1dcb103bf7 | ||
|
|
7ca8efb81f | ||
|
|
c08f259806 | ||
|
|
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 |
@@ -22,7 +22,6 @@ ArchiSteamFarm/logs
|
||||
# /_/ \_\____/|_| |____/ \___/ \___|_|\_\___|_|
|
||||
|
||||
# Additional folders that aren't used during image building:
|
||||
|
||||
**/.git*
|
||||
**/[Bb]in/
|
||||
**/[Oo]bj/
|
||||
@@ -31,6 +30,10 @@ ArchiSteamFarm.CustomPlugins.*
|
||||
ASF-ui/dist
|
||||
wiki
|
||||
|
||||
# Add exception for .git used in ASF-ui, it's used for calculating commit hash during build
|
||||
!.git/modules/ASF-ui
|
||||
!ASF-ui/.git
|
||||
|
||||
# _ _
|
||||
# | | (_) _ __ _ _ __ __
|
||||
# | | | || '_ \ | | | |\ \/ /
|
||||
|
||||
@@ -6,6 +6,7 @@ root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
#file_header_template = · _ _ _ ____ _ _____\n / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___\n / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \\n / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |\n/_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|\n\nCopyright 2015-2021 Łukasz "JustArchi" Domeradzki\nContact: JustArchi@JustArchi.net\n\nLicensed under the Apache License, Version 2.0 (the "License")\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an "AS IS" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.
|
||||
indent_style = tab
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
@@ -212,3 +213,16 @@ dotnet_style_qualification_for_property = false:warning
|
||||
|
||||
dotnet_style_readonly_field = true:warning
|
||||
dotnet_style_require_accessibility_modifiers = always:warning
|
||||
|
||||
###############################
|
||||
# JetBrains, IntelliJ/Rider #
|
||||
###############################
|
||||
|
||||
[*.{csproj,props,xml}]
|
||||
ij_xml_keep_blank_lines = 1
|
||||
ij_xml_keep_line_breaks = false
|
||||
ij_xml_keep_line_breaks_in_text = false
|
||||
ij_xml_space_inside_empty_tag = true
|
||||
|
||||
[*.{json,json5}]
|
||||
ij_json_keep_line_breaks = false
|
||||
|
||||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -67,7 +67,7 @@ ASF is open-source project, developed mainly by **[JustArchi](https://github.com
|
||||
|
||||
### License
|
||||
|
||||
ASF is using **[Apache License 2.0](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/LICENSE-2.0.txt)**.
|
||||
ASF is using **[Apache License 2.0](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/LICENSE.txt)**.
|
||||
|
||||
> Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions.
|
||||
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -2,4 +2,4 @@
|
||||
|
||||
github: JustArchi
|
||||
patreon: JustArchi
|
||||
custom: ["https://paypal.me/JustArchi", "https://pay.revolut.com/profile/ukaszyxm", "https://commerce.coinbase.com/checkout/0c23b844-c51b-45f4-9135-8db7c6fcf98e", "https://steamcommunity.com/tradeoffer/new/?partner=46697991&token=0ix2Ruv_"]
|
||||
custom: ["https://paypal.me/JustArchi", "https://pay.revolut.com/justarchi", "https://commerce.coinbase.com/checkout/0c23b844-c51b-45f4-9135-8db7c6fcf98e", "https://steamcommunity.com/tradeoffer/new/?partner=46697991&token=0ix2Ruv_"]
|
||||
|
||||
72
.github/ISSUE_TEMPLATE/Bug-report.yml
vendored
72
.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
|
||||
@@ -46,6 +46,7 @@ body:
|
||||
- linux-arm
|
||||
- linux-arm64
|
||||
- linux-x64
|
||||
- osx-arm64
|
||||
- osx-x64
|
||||
- win-x64
|
||||
validations:
|
||||
@@ -56,7 +57,7 @@ body:
|
||||
label: Bug description
|
||||
description: Short explanation of what you were going to do, what did you want to accomplish?
|
||||
placeholder: |
|
||||
I tried to brew a coffee with ASF using `PUT /Api/Coffee` ASF API, but upon trying the program returned HTTP error: 418 I'm a teapot
|
||||
I tried to brew a coffee with ASF using `PUT /Api/Coffee` ASF API endpoint, but upon trying the program returned HTTP error: 418 I'm a teapot.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -74,27 +75,37 @@ body:
|
||||
label: Actual behavior
|
||||
description: What happened instead?
|
||||
placeholder: |
|
||||
No coffee was brewed, and so I was forced to use a water dispenser instead :/
|
||||
No coffee was brewed, and so I was forced to use a water dispenser instead :/.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Every command or action that happened after launching ASF, which leads to the bug.
|
||||
placeholder: |
|
||||
description: |
|
||||
Every command or action that happened after launching ASF, which leads to the bug.
|
||||
If launching ASF with provided configs (below) is everything that is needed, then this section is not mandatory.
|
||||
Screenshots of the problem and/or steps leading to it could be very useful in particular.
|
||||
placeholder: |
|
||||
1. Put cup below the machine hosting ASF.
|
||||
2. Send `PUT /Api/Coffee` request selecting latte macchiato.
|
||||
3. No coffee was brewed.
|
||||
- type: textarea
|
||||
id: possible-solution
|
||||
attributes:
|
||||
label: Possible reason/solution
|
||||
description: Not mandatory, but you can suggest a fix/reason for the bug, if known to you.
|
||||
placeholder: If you observed something peculiar about your issue that could help us locate and fix the culprit, this is the right place.
|
||||
description: |
|
||||
Not mandatory, but you can suggest a fix/reason for the bug, if known to you.
|
||||
If you observed something peculiar about your issue that could help us locate and fix the culprit, this is the right place.
|
||||
placeholder: |
|
||||
Perhaps no coffee was brewed because I was out of milk?
|
||||
- type: dropdown
|
||||
id: help
|
||||
attributes:
|
||||
label: Can you help us with this bug report?
|
||||
description: ASF is offered for free and our resources are limited. Helping us increases the chance of fixing the problem.
|
||||
description: |
|
||||
ASF is offered for free and our resources are limited.
|
||||
Helping us increases the chance of fixing the problem.
|
||||
options:
|
||||
- Yes, I can code the solution myself and send a pull request
|
||||
- Somehow, I can test and offer feedback, but can't code
|
||||
@@ -105,9 +116,24 @@ body:
|
||||
id: asf-log
|
||||
attributes:
|
||||
label: Full log.txt recorded during reproducing the problem
|
||||
description: You can find `log.txt` file directly in ASF directory. If the bug report doesn't come from the last run of the program, you can find logs from previous runs of the program in the `logs` directory instead.
|
||||
description: |
|
||||
You can find `log.txt` file directly in ASF directory.
|
||||
If the bug report doesn't come from the last run of the program, you can find logs from previous runs of the program in the `logs` directory instead.
|
||||
If no `log.txt` was recorded due to crash at the very early stage, console output should be pasted instead.
|
||||
placeholder: |
|
||||
If no log.txt was recorded due to crash at the very early stage, console output should be pasted instead.
|
||||
2021-12-16 00:20:43|dotnet-282887|INFO|ASF|InitCore() ArchiSteamFarm V5.2.1.2 (generic/6b492ffa-9927-431d-bae7-7360ab9968a9 | .NET 6.0.0-rtm.21522.10; debian-arm64; Linux 5.15.0-1-arm64 #1 SMP Debian 5.15.3-1 (2021-11-18))
|
||||
2021-12-16 00:20:43|dotnet-282887|INFO|ASF|InitCore() Copyright © 2015-2021 JustArchiNET
|
||||
2021-12-16 00:20:47|dotnet-282887|INFO|ASF|InitPlugins() Initializing Plugins...
|
||||
2021-12-16 00:20:47|dotnet-282887|INFO|ASF|InitPlugins() Loading SteamTokenDumperPlugin V5.2.1.2...
|
||||
2021-12-16 00:20:47|dotnet-282887|INFO|ASF|InitPlugins() SteamTokenDumperPlugin has been loaded successfully!
|
||||
2021-12-16 00:20:47|dotnet-282887|INFO|ASF|UpdateAndRestart() ASF will automatically check for new versions every 1 day.
|
||||
2021-12-16 00:20:52|dotnet-282887|INFO|ASF|Update() Checking for new version...
|
||||
2021-12-16 00:20:54|dotnet-282887|INFO|ASF|Update() Local version: 5.2.1.2 | Remote version: 5.2.1.2
|
||||
2021-12-16 00:20:54|dotnet-282887|INFO|ASF|Load() Loading STD global cache...
|
||||
2021-12-16 00:20:56|dotnet-282887|INFO|ASF|Load() Validating STD global cache integrity...
|
||||
2021-12-16 00:20:56|dotnet-282887|INFO|ASF|OnASFInit() SteamTokenDumperPlugin has been initialized successfully, thank you in advance for your help. The first submission will happen in approximately 47 minutes from now.
|
||||
2021-12-16 00:20:57|dotnet-282887|INFO|ASF|Start() Starting IPC server...
|
||||
2021-12-16 00:20:59|dotnet-282887|INFO|ASF|Start() IPC server ready!
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
@@ -115,9 +141,9 @@ body:
|
||||
id: global-config
|
||||
attributes:
|
||||
label: Global ASF.json config file
|
||||
description: The config can be found in `config` directory under `ASF.json` name. You can leave this field empty if not using one.
|
||||
placeholder: |
|
||||
Paste the file content here, no need for triple backtick tags
|
||||
description: |
|
||||
The config can be found in `config` directory under `ASF.json` name.
|
||||
You can leave this field empty if not using one.
|
||||
|
||||
Ensure that your config has redacted (but NOT removed) potentially-sensitive properties, such as:
|
||||
- IPCPassword (recommended)
|
||||
@@ -127,14 +153,22 @@ body:
|
||||
- WebProxyUsername (optionally, if exposing private details)
|
||||
|
||||
Redacting involves replacing sensitive details, for example with stars (***). You should refrain from removing config lines entirely, as their pure existence may be relevant and should be preserved.
|
||||
placeholder: |
|
||||
{
|
||||
"AutoRestart": false,
|
||||
"Headless": true,
|
||||
"IPCPassword": "********",
|
||||
"UpdateChannel": 2,
|
||||
"SteamTokenDumperPluginEnabled": true
|
||||
}
|
||||
render: json
|
||||
- type: textarea
|
||||
id: bot-config
|
||||
attributes:
|
||||
label: BotName.json config of all affected bot instances
|
||||
description: Bot config files can be found in `config` directory, ending with `json` extension. You can leave this field empty if you don't have any defined.
|
||||
placeholder: |
|
||||
Paste the file content here, no need for triple backtick tags
|
||||
description: |
|
||||
Bot config files can be found in `config` directory, ending with `json` extension.
|
||||
You can leave this field empty if you don't have any defined.
|
||||
|
||||
Ensure that your config has redacted (but NOT removed) potentially-sensitive properties, such as:
|
||||
- SteamLogin (mandatory)
|
||||
@@ -145,6 +179,12 @@ body:
|
||||
- SteamUserPermissions (optionally, only SteamIDs)
|
||||
|
||||
Redacting involves replacing sensitive details, for example with stars (***). You should refrain from removing config lines entirely, as their pure existence may be relevant and should be preserved.
|
||||
placeholder: |
|
||||
{
|
||||
"Enabled": true,
|
||||
"SteamLogin": "********",
|
||||
"SteamPassword": "********"
|
||||
}
|
||||
render: json
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
|
||||
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
|
||||
|
||||
|
||||
6
.github/RELEASE_TEMPLATE.md
vendored
6
.github/RELEASE_TEMPLATE.md
vendored
@@ -1,6 +1,6 @@
|
||||
### Notice
|
||||
|
||||
**Pre-releases are experimental versions that often contain unpatched bugs, work-in-progress features or rewritten implementations. If you don't consider yourself advanced user, please download **[latest stable release](https://github.com/JustArchiNET/ArchiSteamFarm/releases/latest)** instead. Pre-release versions are dedicated to users who know how to report bugs, deal with issues and give feedback - no technical support will be given. Check out ASF **[release cycle](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Release-cycle)** if you'd like to learn more.**
|
||||
**Pre-releases are experimental versions that often contain unpatched bugs, work-in-progress features and rewritten implementations. If you don't consider yourself advanced user, please download **[latest stable release](https://github.com/JustArchiNET/ArchiSteamFarm/releases/latest)** instead. Pre-release versions are dedicated to users who know how to report bugs, deal with issues and give feedback - no technical support will be given. Check out ASF **[release cycle](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Release-cycle)** if you'd like to learn more.**
|
||||
|
||||
---
|
||||
|
||||
@@ -12,6 +12,6 @@ This is automated GitHub deployment, human-readable changelog should be availabl
|
||||
|
||||
### Support
|
||||
|
||||
ASF is available for free, this release was made possible thanks to the people that decided to support the project. If you're grateful for what we're doing, please consider donating. Developing ASF requires massive amount of time and knowledge, especially when it comes to Steam (and its problems). Even $1 is highly appreciated and shows that you care. Thank you!
|
||||
ASF is available for free, this release was made possible thanks to the people that decided to support the project. If you're grateful for what we're doing, please consider a donation. Developing ASF requires massive amount of time and knowledge, especially when it comes to Steam (and its problems). Even $1 is highly appreciated and shows that you care. Thank you!
|
||||
|
||||
[](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_)
|
||||
[](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/justarchi) [](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 as GitHub issue if deemed appropriate. Invalid GitHub issues will be closed immediately and won't be answered.
|
||||
|
||||
11
.github/crowdin.yml
vendored
11
.github/crowdin.yml
vendored
@@ -1,3 +1,4 @@
|
||||
"base_path": ".."
|
||||
"preserve_hierarchy": true
|
||||
"files": [
|
||||
{
|
||||
@@ -5,7 +6,10 @@
|
||||
"translation": "/ArchiSteamFarm/Localization/Strings.%locale%.resx",
|
||||
"translation_replace": {
|
||||
".lol-US.resx": ".qps-Ploc.resx",
|
||||
".sr-CS.resx": ".sr-Latn.resx"
|
||||
".sr-CS.resx": ".sr-Latn.resx",
|
||||
".zh-CN.resx": ".zh-Hans.resx",
|
||||
".zh-HK.resx": ".zh-Hant-HK.resx",
|
||||
".zh-TW.resx": ".zh-Hant.resx"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13,7 +17,10 @@
|
||||
"translation": "/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Localization/Strings.%locale%.resx",
|
||||
"translation_replace": {
|
||||
".lol-US.resx": ".qps-Ploc.resx",
|
||||
".sr-CS.resx": ".sr-Latn.resx"
|
||||
".sr-CS.resx": ".sr-Latn.resx",
|
||||
".zh-CN.resx": ".zh-Hans.resx",
|
||||
".zh-HK.resx": ".zh-Hant-HK.resx",
|
||||
".zh-TW.resx": ".zh-Hant.resx"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
7
.github/renovate.json5
vendored
7
.github/renovate.json5
vendored
@@ -17,13 +17,8 @@
|
||||
{
|
||||
// TODO: <= 3.1 for Mono support, last failed version 6.12, https://steamcommunity.com/groups/archiasf/discussions/1/2997673517556002529
|
||||
"allowedVersions": "<= 3.1",
|
||||
"matchManagers": [ "nuget" ],
|
||||
"matchPackageNames": [ "Microsoft.Extensions.Configuration.Json", "Microsoft.Extensions.Logging.Configuration" ]
|
||||
},
|
||||
{
|
||||
// TODO: <= 2.2.4 due to https://github.com/microsoft/testfx/issues/906, should be resolved with v2.2.8+ (?)
|
||||
"allowedVersions": "<= 2.2.4",
|
||||
"groupName": "MSTest packages",
|
||||
"matchPackagePatterns": ["^MSTest\\..+"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -19,12 +19,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1.8.2
|
||||
uses: actions/setup-dotnet@v2.1.0
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
|
||||
|
||||
@@ -38,9 +38,8 @@ jobs:
|
||||
run: dotnet test ArchiSteamFarm.Tests -c "${{ matrix.configuration }}" -p:ContinuousIntegrationBuild=true -p:UseAppHost=false --nologo
|
||||
|
||||
- name: Upload latest strings for translation on Crowdin
|
||||
continue-on-error: true
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.configuration == 'Release' && startsWith(matrix.os, 'ubuntu-') }}
|
||||
uses: crowdin/github-action@1.4.1
|
||||
uses: crowdin/github-action@1.4.11
|
||||
with:
|
||||
crowdin_branch_name: main
|
||||
config: '.github/crowdin.yml'
|
||||
|
||||
6
.github/workflows/docker-ci.yml
vendored
6
.github/workflows/docker-ci.yml
vendored
@@ -17,15 +17,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
|
||||
- name: Build ${{ matrix.configuration }} Docker image from ${{ matrix.file }}
|
||||
uses: docker/build-push-action@v2.7.0
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
|
||||
12
.github/workflows/docker-publish-latest.yml
vendored
12
.github/workflows/docker-publish-latest.yml
vendored
@@ -15,22 +15,22 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v2.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v2.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build and publish Docker image from Dockerfile.Service
|
||||
uses: docker/build-push-action@v2.7.0
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.Service
|
||||
|
||||
15
.github/workflows/docker-publish-main.yml
vendored
15
.github/workflows/docker-publish-main.yml
vendored
@@ -16,22 +16,22 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v2.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v2.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build and publish Docker image from Dockerfile
|
||||
uses: docker/build-push-action@v2.7.0
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
@@ -70,8 +70,7 @@ jobs:
|
||||
push: true
|
||||
|
||||
- name: Update DockerHub repository description
|
||||
continue-on-error: true
|
||||
uses: peter-evans/dockerhub-description@v2.4.3
|
||||
uses: peter-evans/dockerhub-description@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
12
.github/workflows/docker-publish-released.yml
vendored
12
.github/workflows/docker-publish-released.yml
vendored
@@ -16,22 +16,22 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v2.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1.10.0
|
||||
uses: docker/login-action@v2.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build and publish Docker image from Dockerfile
|
||||
uses: docker/build-push-action@v2.7.0
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
|
||||
4
.github/workflows/lock-threads.yml
vendored
4
.github/workflows/lock-threads.yml
vendored
@@ -11,5 +11,5 @@ jobs:
|
||||
- name: Lock inactive threads
|
||||
uses: dessant/lock-threads@v3.0.0
|
||||
with:
|
||||
issue-inactive-days: 30
|
||||
pr-inactive-days: 30
|
||||
issue-inactive-days: 60
|
||||
pr-inactive-days: 60
|
||||
|
||||
114
.github/workflows/publish.yml
vendored
114
.github/workflows/publish.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1.8.2
|
||||
uses: actions/setup-dotnet@v2.1.0
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
run: dotnet --info
|
||||
|
||||
- name: Setup Node.js with npm
|
||||
uses: actions/setup-node@v2.4.1
|
||||
uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
check-latest: true
|
||||
node-version: ${{ env.NODE_JS_VERSION }}
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
- name: Publish ArchiSteamFarm on Unix
|
||||
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
|
||||
env:
|
||||
VARIANTS: generic linux-arm linux-arm64 linux-x64 osx-x64 win-x64 # NOTE: When modifying variants, don't forget to update ASF_VARIANT definitions in SharedInfo.cs!
|
||||
VARIANTS: generic linux-arm linux-arm64 linux-x64 osx-arm64 osx-x64 win-x64 # NOTE: When modifying variants, don't forget to update ASF_VARIANT definitions in SharedInfo.cs!
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
@@ -128,22 +128,11 @@ jobs:
|
||||
if [ "$1" = 'generic' ]; then
|
||||
variantArgs="-p:TargetLatestRuntimePatch=false -p:UseAppHost=false"
|
||||
else
|
||||
variantArgs="-p:PublishSingleFile=true -p:PublishTrimmed=true -r $1"
|
||||
variantArgs="-p:PublishSingleFile=true -p:PublishTrimmed=true -r $1 --self-contained"
|
||||
fi
|
||||
|
||||
dotnet publish ArchiSteamFarm -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/${1}" "-p:ASFVariant=$1" -p:ContinuousIntegrationBuild=true --no-restore --nologo $variantArgs
|
||||
|
||||
# If we're including any overlay for this variant, copy it to output directory
|
||||
variant_os="$(echo "$1" | cut -d '-' -f 1)"
|
||||
|
||||
if [ -d "ArchiSteamFarm/overlay/${variant_os}" ]; then
|
||||
cp -pR "ArchiSteamFarm/overlay/${variant_os}/"* "out/${1}"
|
||||
fi
|
||||
|
||||
if [ "$1" != "$variant_os" ] && [ -d "ArchiSteamFarm/overlay/${1}" ]; then
|
||||
cp -pR "ArchiSteamFarm/overlay/${1}/"* "out/${1}"
|
||||
fi
|
||||
|
||||
# If we're including SteamTokenDumper plugin for this framework, copy it to output directory
|
||||
if [ -d "out/${STEAM_TOKEN_DUMPER_NAME}/${NET_CORE_VERSION}" ]; then
|
||||
mkdir -p "out/${1}/plugins/${STEAM_TOKEN_DUMPER_NAME}"
|
||||
@@ -177,7 +166,7 @@ jobs:
|
||||
# Create the final zip file
|
||||
case "$(uname -s)" in
|
||||
"Darwin")
|
||||
# We prefer to use zip on OS X as 7z implementation on that OS doesn't handle file permissions (chmod +x)
|
||||
# We prefer to use zip on macOS as 7z implementation on that OS doesn't handle file permissions (chmod +x)
|
||||
if command -v zip >/dev/null; then
|
||||
(
|
||||
cd "${GITHUB_WORKSPACE}/out/${1}"
|
||||
@@ -220,7 +209,7 @@ jobs:
|
||||
- name: Publish ArchiSteamFarm on Windows
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
env:
|
||||
VARIANTS: generic generic-netf linux-arm linux-arm64 linux-x64 osx-x64 win-x64 # NOTE: When modifying variants, don't forget to update ASF_VARIANT definitions in SharedInfo.cs!
|
||||
VARIANTS: generic generic-netf linux-arm linux-arm64 linux-x64 osx-arm64 osx-x64 win-x64 # NOTE: When modifying variants, don't forget to update ASF_VARIANT definitions in SharedInfo.cs!
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -245,7 +234,7 @@ jobs:
|
||||
if ($variant -like 'generic*') {
|
||||
$variantArgs = '-p:TargetLatestRuntimePatch=false', '-p:UseAppHost=false'
|
||||
} else {
|
||||
$variantArgs = '-p:PublishSingleFile=true', '-p:PublishTrimmed=true', '-r', "$variant"
|
||||
$variantArgs = '-p:PublishSingleFile=true', '-p:PublishTrimmed=true', '-r', "$variant", '--self-contained'
|
||||
}
|
||||
|
||||
dotnet publish ArchiSteamFarm -c "$env:CONFIGURATION" -f "$targetFramework" -o "out\$variant" "-p:ASFVariant=$variant" -p:ContinuousIntegrationBuild=true --no-restore --nologo $variantArgs
|
||||
@@ -254,17 +243,6 @@ jobs:
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
# If we're including any overlay for this variant, copy it to output directory
|
||||
$variant_os = $variant.Split('-', 2)[0];
|
||||
|
||||
if (Test-Path "ArchiSteamFarm\overlay\$variant_os" -PathType Container) {
|
||||
Copy-Item "ArchiSteamFarm\overlay\$variant_os\*" "out\$variant" -Recurse
|
||||
}
|
||||
|
||||
if (($variant -ne $variant_os) -and (Test-Path "ArchiSteamFarm\overlay\$variant" -PathType Container)) {
|
||||
Copy-Item "ArchiSteamFarm\overlay\$variant\*" "out\$variant" -Recurse
|
||||
}
|
||||
|
||||
# If we're including SteamTokenDumper plugin for this framework, copy it to output directory
|
||||
if (Test-Path "out\$env:STEAM_TOKEN_DUMPER_NAME\$targetFramework" -PathType Container) {
|
||||
if (!(Test-Path "out\$variant\plugins\$env:STEAM_TOKEN_DUMPER_NAME" -PathType Container)) {
|
||||
@@ -325,56 +303,64 @@ jobs:
|
||||
|
||||
foreach ($variant in $env:VARIANTS.Split([char[]] $null, [System.StringSplitOptions]::RemoveEmptyEntries)) {
|
||||
Start-Job -Name "$variant" $PublishBlock -ArgumentList "$variant"
|
||||
|
||||
# Limit active jobs in parallel to help with memory usage
|
||||
$jobs = $(Get-Job -State Running)
|
||||
|
||||
while (@($jobs).Count -ge 5) {
|
||||
Wait-Job -Job $jobs -Any | Out-Null
|
||||
|
||||
$jobs = $(Get-Job -State Running)
|
||||
}
|
||||
}
|
||||
|
||||
Get-Job | Receive-Job -Wait
|
||||
|
||||
- name: Upload ASF-generic
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: ${{ matrix.os }}_ASF-generic
|
||||
path: out/ASF-generic.zip
|
||||
|
||||
- name: Upload ASF-generic-netf
|
||||
continue-on-error: true
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: ${{ matrix.os }}_ASF-generic-netf
|
||||
path: out/ASF-generic-netf.zip
|
||||
|
||||
- name: Upload ASF-linux-arm
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: ${{ matrix.os }}_ASF-linux-arm
|
||||
path: out/ASF-linux-arm.zip
|
||||
|
||||
- name: Upload ASF-linux-arm64
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: ${{ matrix.os }}_ASF-linux-arm64
|
||||
path: out/ASF-linux-arm64.zip
|
||||
|
||||
- name: Upload ASF-linux-x64
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: ${{ matrix.os }}_ASF-linux-x64
|
||||
path: out/ASF-linux-x64.zip
|
||||
|
||||
- name: Upload ASF-osx-arm64
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: ${{ matrix.os }}_ASF-osx-arm64
|
||||
path: out/ASF-osx-arm64.zip
|
||||
|
||||
- name: Upload ASF-osx-x64
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: ${{ matrix.os }}_ASF-osx-x64
|
||||
path: out/ASF-osx-x64.zip
|
||||
|
||||
- name: Upload ASF-win-x64
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: ${{ matrix.os }}_ASF-win-x64
|
||||
path: out/ASF-win-x64.zip
|
||||
@@ -386,55 +372,61 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
|
||||
# TODO: It'd be perfect if we could match final artifacts to the platform they target, so e.g. linux build comes from the linux machine
|
||||
# However, that is currently impossible due to https://github.com/dotnet/msbuild/issues/3897
|
||||
# Therefore, we'll (sadly) pull artifacts from Windows machine only for now
|
||||
- name: Download ASF-generic artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.0.10
|
||||
uses: actions/download-artifact@v3.0.0
|
||||
with:
|
||||
name: windows-latest_ASF-generic
|
||||
path: out
|
||||
|
||||
- name: Download ASF-generic-netf artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.0.10
|
||||
uses: actions/download-artifact@v3.0.0
|
||||
with:
|
||||
name: windows-latest_ASF-generic-netf
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-arm artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.0.10
|
||||
uses: actions/download-artifact@v3.0.0
|
||||
with:
|
||||
name: windows-latest_ASF-linux-arm
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-arm64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.0.10
|
||||
uses: actions/download-artifact@v3.0.0
|
||||
with:
|
||||
name: windows-latest_ASF-linux-arm64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-x64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.0.10
|
||||
uses: actions/download-artifact@v3.0.0
|
||||
with:
|
||||
name: windows-latest_ASF-linux-x64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-osx-arm64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v3.0.0
|
||||
with:
|
||||
name: windows-latest_ASF-osx-arm64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-osx-x64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.0.10
|
||||
uses: actions/download-artifact@v3.0.0
|
||||
with:
|
||||
name: windows-latest_ASF-osx-x64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-win-x64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v2.0.10
|
||||
uses: actions/download-artifact@v3.0.0
|
||||
with:
|
||||
name: windows-latest_ASF-win-x64
|
||||
path: out
|
||||
|
||||
- name: Import GPG key for signing
|
||||
uses: crazy-max/ghaction-import-gpg@v4.1.0
|
||||
uses: crazy-max/ghaction-import-gpg@v5.1.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
|
||||
|
||||
@@ -451,15 +443,13 @@ jobs:
|
||||
)
|
||||
|
||||
- name: Upload SHA512SUMS
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SHA512SUMS
|
||||
path: out/SHA512SUMS
|
||||
|
||||
- name: Upload SHA512SUMS.sign
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: SHA512SUMS.sign
|
||||
path: out/SHA512SUMS.sign
|
||||
@@ -525,6 +515,16 @@ jobs:
|
||||
asset_name: ASF-linux-x64.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload ASF-osx-arm64 to GitHub release
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.github_release.outputs.upload_url }}
|
||||
asset_path: out/ASF-osx-arm64.zip
|
||||
asset_name: ASF-osx-arm64.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload ASF-osx-x64 to GitHub release
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
|
||||
6
.github/workflows/translations.yml
vendored
6
.github/workflows/translations.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.ARCHIBOT_GITHUB_TOKEN }}
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git reset --hard origin/master
|
||||
|
||||
- name: Download latest translations from Crowdin
|
||||
uses: crowdin/github-action@1.4.1
|
||||
uses: crowdin/github-action@1.4.11
|
||||
with:
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
token: ${{ secrets.ASF_CROWDIN_API_TOKEN }}
|
||||
|
||||
- name: Import GPG key for signing
|
||||
uses: crazy-max/ghaction-import-gpg@v4.1.0
|
||||
uses: crazy-max/ghaction-import-gpg@v5.1.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
|
||||
git_config_global: true
|
||||
|
||||
35
.gitignore
vendored
35
.gitignore
vendored
@@ -18,13 +18,16 @@ ArchiSteamFarm/logs
|
||||
# Ignore standard out folders for publishing
|
||||
**/out
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
|
||||
# _ _
|
||||
# | | (_) _ __ _ _ __ __
|
||||
# | | | || '_ \ | | | |\ \/ /
|
||||
# | |___ | || | | || |_| | > <
|
||||
# |_____||_||_| |_| \__,_|/_/\_\
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/Global/Linux.gitignore
|
||||
# https://github.com/github/gitignore/blob/main/Global/Linux.gitignore
|
||||
# 4f7062e132d7f88e68ab737e64fef872bd3a491f
|
||||
|
||||
*~
|
||||
@@ -47,7 +50,7 @@ ArchiSteamFarm/logs
|
||||
# | | | | | || (_| || (__ | |_| | ___) |
|
||||
# |_| |_| |_| \__,_| \___| \___/ |____/
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
|
||||
# https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
|
||||
# 2bb963b16a1957c865335e53537036c2e97399b5
|
||||
|
||||
# General
|
||||
@@ -84,7 +87,7 @@ Temporary Items
|
||||
# |_| |_| \___/ |_| |_| \___/ |____/ \___| \_/ \___||_| \___/ | .__/
|
||||
# |_|
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/Global/MonoDevelop.gitignore
|
||||
# https://github.com/github/gitignore/blob/main/Global/MonoDevelop.gitignore
|
||||
# e8b2e1a9cc7c9ca49bb05c20a4c4491b85feba6d
|
||||
|
||||
#User Specific
|
||||
@@ -102,13 +105,13 @@ test-results/
|
||||
# \ V / | |\__ \| |_| || (_| || | ___) || |_ | |_| || (_| || || (_) |
|
||||
# \_/ |_||___/ \__,_| \__,_||_||____/ \__| \__,_| \__,_||_| \___/
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
# 888439ee893d0097862f1d510585bd0e3cfd500f
|
||||
# https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
# 491040e88a572d300a59484cb78c86c5e944b70a
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
@@ -313,9 +316,6 @@ PublishScripts/
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Nuget personal access tokens and Credentials
|
||||
nuget.config
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
@@ -404,6 +404,17 @@ node_modules/
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
@@ -460,6 +471,9 @@ ASALocalRun/
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
@@ -491,7 +505,6 @@ FodyWeavers.xsd
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# __ __ _ _
|
||||
@@ -500,7 +513,7 @@ FodyWeavers.xsd
|
||||
# \ V V / | || | | || (_| || (_) |\ V V / \__ \
|
||||
# \_/\_/ |_||_| |_| \__,_| \___/ \_/\_/ |___/
|
||||
#
|
||||
# https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
|
||||
# https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
|
||||
# 5808b77453dec299d4daf8557b05a80be832a5b8
|
||||
|
||||
# Windows thumbnail cache files
|
||||
|
||||
2
ASF-ui
2
ASF-ui
Submodule ASF-ui updated: 88fbcc1ac4...f486cd15ab
@@ -5,6 +5,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
|
||||
<PackageReference Include="Newtonsoft.Json" IncludeAssets="compile" />
|
||||
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
|
||||
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
|
||||
|
||||
@@ -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");
|
||||
@@ -20,48 +20,25 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Web;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
|
||||
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";
|
||||
|
||||
internal static async Task<string?> GetRandomCatURL(WebBrowser webBrowser) {
|
||||
if (webBrowser == null) {
|
||||
throw new ArgumentNullException(nameof(webBrowser));
|
||||
}
|
||||
internal static async Task<Uri?> GetRandomCatURL(WebBrowser webBrowser) {
|
||||
ArgumentNullException.ThrowIfNull(webBrowser);
|
||||
|
||||
Uri request = new($"{URL}/meow");
|
||||
|
||||
ObjectResponse<MeowResponse>? response = await webBrowser.UrlGetToJsonObject<MeowResponse>(request).ConfigureAwait(false);
|
||||
|
||||
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 = "";
|
||||
|
||||
[JsonConstructor]
|
||||
private MeowResponse() { }
|
||||
}
|
||||
#pragma warning restore CA1812 // False positive, the class is used during json deserialization
|
||||
return response?.Content?.URL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +27,8 @@ using ArchiSteamFarm.IPC.Controllers.Api;
|
||||
using ArchiSteamFarm.IPC.Responses;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
|
||||
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
|
||||
@@ -37,16 +38,15 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
|
||||
/// Fetches URL of a random cat picture.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(GenericResponse<string>), (int) HttpStatusCode.OK)]
|
||||
[ProducesResponseType(typeof(GenericResponse<Uri>), (int) HttpStatusCode.OK)]
|
||||
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.ServiceUnavailable)]
|
||||
public async Task<ActionResult<GenericResponse>> CatGet() {
|
||||
if (ASF.WebBrowser == null) {
|
||||
throw new InvalidOperationException(nameof(ASF.WebBrowser));
|
||||
}
|
||||
|
||||
string? link = await CatAPI.GetRandomCatURL(ASF.WebBrowser).ConfigureAwait(false);
|
||||
Uri? url = await CatAPI.GetRandomCatURL(ASF.WebBrowser).ConfigureAwait(false);
|
||||
|
||||
return !string.IsNullOrEmpty(link) ? Ok(new GenericResponse<string>(link)) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false));
|
||||
}
|
||||
return url != null ? Ok(new GenericResponse<Uri>(url)) : 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");
|
||||
@@ -28,19 +28,19 @@ 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 {
|
||||
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
|
||||
|
||||
// In order for your plugin to work, it must export generic ASF's IPlugin interface
|
||||
[Export(typeof(IPlugin))]
|
||||
|
||||
// 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 {
|
||||
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);
|
||||
@@ -57,21 +57,25 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
|
||||
// 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) {
|
||||
public Task OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
|
||||
if (additionalConfigProperties == null) {
|
||||
return;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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:
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// This method is called when unknown command is received (starting with CommandPrefix)
|
||||
@@ -81,16 +85,16 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
|
||||
// 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) {
|
||||
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 bot.HasAccess(steamID, BotConfig.EAccess.FamilySharing):
|
||||
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);
|
||||
Uri? randomCatURL = await CatAPI.GetRandomCatURL(bot.ArchiWebHandler.WebBrowser).ConfigureAwait(false);
|
||||
|
||||
return !string.IsNullOrEmpty(randomCatURL) ? randomCatURL : "God damn it, we're out of cats, care to notify my master? Thanks!";
|
||||
return randomCatURL != null ? randomCatURL.ToString() : "God damn it, we're out of cats, care to notify my master? Thanks!";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -100,12 +104,12 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
|
||||
// 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) { }
|
||||
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 void OnBotDisconnected(Bot bot, EResult reason) { }
|
||||
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)
|
||||
@@ -116,10 +120,12 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
|
||||
// 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) {
|
||||
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
|
||||
@@ -127,8 +133,7 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
|
||||
// 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
|
||||
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");
|
||||
@@ -136,7 +141,7 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
|
||||
}
|
||||
|
||||
// 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) { }
|
||||
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
|
||||
@@ -173,9 +178,10 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
|
||||
// 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() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
37
ArchiSteamFarm.CustomPlugins.ExamplePlugin/MeowResponse.cs
Normal file
37
ArchiSteamFarm.CustomPlugins.ExamplePlugin/MeowResponse.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// |
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
|
||||
|
||||
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
internal sealed class MeowResponse {
|
||||
[JsonProperty("file", Required = Required.Always)]
|
||||
internal readonly Uri URL = null!;
|
||||
|
||||
[JsonConstructor]
|
||||
private MeowResponse() { }
|
||||
}
|
||||
#pragma warning restore CA1812 // False positive, the class is used during json deserialization
|
||||
@@ -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,10 +24,12 @@ 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 {
|
||||
namespace ArchiSteamFarm.CustomPlugins.PeriodicGC;
|
||||
|
||||
[Export(typeof(IPlugin))]
|
||||
[SuppressMessage("ReSharper", "UnusedType.Global")]
|
||||
internal sealed class PeriodicGCPlugin : IPlugin {
|
||||
@@ -40,7 +42,7 @@ namespace ArchiSteamFarm.CustomPlugins.PeriodicGC {
|
||||
|
||||
public Version Version => typeof(PeriodicGCPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
|
||||
|
||||
public void OnLoaded() {
|
||||
public Task OnLoaded() {
|
||||
TimeSpan timeSpan = TimeSpan.FromSeconds(GCPeriod);
|
||||
|
||||
ASF.ArchiLogger.LogGenericWarning($"Periodic GC will occur every {timeSpan.ToHumanReadable()}. Please keep in mind that this plugin should be used for debugging tests only.");
|
||||
@@ -48,6 +50,8 @@ namespace ArchiSteamFarm.CustomPlugins.PeriodicGC {
|
||||
lock (LockObject) {
|
||||
PeriodicGCTimer.Change(timeSpan, timeSpan);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void PerformGC(object? state = null) {
|
||||
@@ -61,4 +65,3 @@ namespace ArchiSteamFarm.CustomPlugins.PeriodicGC {
|
||||
ASF.ArchiLogger.LogGenericWarning($"GC finished, current memory: {GC.GetTotalMemory(false) / 1024} KB.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
@@ -28,12 +28,13 @@ 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 {
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
|
||||
internal sealed class GlobalCache : SerializableFile {
|
||||
private static string SharedFilePath => Path.Combine(ArchiSteamFarm.SharedInfo.ConfigDirectory, $"{nameof(SteamTokenDumper)}.cache");
|
||||
|
||||
@@ -95,20 +96,18 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
|
||||
internal static async Task<GlobalCache?> Load() {
|
||||
if (!File.Exists(SharedFilePath)) {
|
||||
GlobalCache result = new();
|
||||
|
||||
Utilities.InBackground(result.Save);
|
||||
|
||||
return result;
|
||||
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, Strings.ErrorIsEmpty, nameof(json)));
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(json)));
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -121,7 +120,15 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
|
||||
if (globalCache == null) {
|
||||
ASF.ArchiLogger.LogNullError(nameof(globalCache));
|
||||
ASF.ArchiLogger.LogNullError(globalCache);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ASF.ArchiLogger.LogGenericInfo(Strings.ValidatingGlobalCacheIntegrity);
|
||||
|
||||
if (globalCache.DepotKeys.Values.Any(static depotKey => !IsValidDepotKey(depotKey))) {
|
||||
ASF.ArchiLogger.LogGenericError(Strings.GlobalCacheIntegrityValidationFailed);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -134,9 +141,7 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
throw new ArgumentOutOfRangeException(nameof(currentChangeNumber));
|
||||
}
|
||||
|
||||
if (appChanges == null) {
|
||||
throw new ArgumentNullException(nameof(appChanges));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(appChanges);
|
||||
|
||||
if (currentChangeNumber <= LastChangeNumber) {
|
||||
return;
|
||||
@@ -145,7 +150,7 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
LastChangeNumber = currentChangeNumber;
|
||||
|
||||
foreach ((uint appID, SteamApps.PICSChangesCallback.PICSChangeData appData) in appChanges) {
|
||||
if (!AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) || (appData.ChangeNumber <= previousChangeNumber)) {
|
||||
if (!AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) || (previousChangeNumber >= appData.ChangeNumber)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -174,14 +179,12 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
internal bool ShouldRefreshDepotKey(uint depotID) => !DepotKeys.ContainsKey(depotID);
|
||||
|
||||
internal void UpdateAppChangeNumbers(IReadOnlyCollection<KeyValuePair<uint, uint>> appChangeNumbers) {
|
||||
if (appChangeNumbers == null) {
|
||||
throw new ArgumentNullException(nameof(appChangeNumbers));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(appChangeNumbers);
|
||||
|
||||
bool save = false;
|
||||
|
||||
foreach ((uint appID, uint changeNumber) in appChangeNumbers) {
|
||||
if (AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) && (previousChangeNumber == changeNumber)) {
|
||||
if (AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) && (previousChangeNumber >= changeNumber)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -195,13 +198,8 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
|
||||
internal void UpdateAppTokens(IReadOnlyCollection<KeyValuePair<uint, ulong>> appTokens, IReadOnlyCollection<uint> publicAppIDs) {
|
||||
if (appTokens == null) {
|
||||
throw new ArgumentNullException(nameof(appTokens));
|
||||
}
|
||||
|
||||
if (publicAppIDs == null) {
|
||||
throw new ArgumentNullException(nameof(publicAppIDs));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(appTokens);
|
||||
ArgumentNullException.ThrowIfNull(publicAppIDs);
|
||||
|
||||
bool save = false;
|
||||
|
||||
@@ -229,9 +227,7 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
|
||||
internal void UpdateDepotKeys(ICollection<SteamApps.DepotKeyCallback> depotKeyResults) {
|
||||
if (depotKeyResults == null) {
|
||||
throw new ArgumentNullException(nameof(depotKeyResults));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(depotKeyResults);
|
||||
|
||||
bool save = false;
|
||||
|
||||
@@ -240,7 +236,13 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
continue;
|
||||
}
|
||||
|
||||
string depotKey = BitConverter.ToString(depotKeyResult.DepotKey).Replace("-", "", StringComparison.Ordinal);
|
||||
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;
|
||||
@@ -256,9 +258,7 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
|
||||
internal void UpdatePackageTokens(IReadOnlyCollection<KeyValuePair<uint, ulong>> packageTokens) {
|
||||
if (packageTokens == null) {
|
||||
throw new ArgumentNullException(nameof(packageTokens));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(packageTokens);
|
||||
|
||||
bool save = false;
|
||||
|
||||
@@ -277,31 +277,49 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
|
||||
internal void UpdateSubmittedData(IReadOnlyDictionary<uint, ulong> apps, IReadOnlyDictionary<uint, ulong> packages, IReadOnlyDictionary<uint, string> depots) {
|
||||
if (apps == null) {
|
||||
throw new ArgumentNullException(nameof(apps));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(apps);
|
||||
ArgumentNullException.ThrowIfNull(packages);
|
||||
ArgumentNullException.ThrowIfNull(depots);
|
||||
|
||||
if (packages == null) {
|
||||
throw new ArgumentNullException(nameof(packages));
|
||||
}
|
||||
|
||||
if (depots == null) {
|
||||
throw new ArgumentNullException(nameof(depots));
|
||||
}
|
||||
bool save = false;
|
||||
|
||||
foreach ((uint appID, ulong token) in apps) {
|
||||
if (SubmittedApps.TryGetValue(appID, out ulong previousToken) && (previousToken == token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SubmittedApps[appID] = token;
|
||||
save = true;
|
||||
}
|
||||
|
||||
foreach ((uint packageID, ulong token) in packages) {
|
||||
if (SubmittedPackages.TryGetValue(packageID, out ulong previousToken) && (previousToken == token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SubmittedPackages[packageID] = token;
|
||||
save = true;
|
||||
}
|
||||
|
||||
foreach ((uint depotID, string key) in depots) {
|
||||
SubmittedDepots[depotID] = key;
|
||||
if (SubmittedDepots.TryGetValue(depotID, out string? previousKey) && (previousKey == key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SubmittedDepots[depotID] = key;
|
||||
save = true;
|
||||
}
|
||||
|
||||
if (save) {
|
||||
Utilities.InBackground(Save);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsValidDepotKey(string depotKey) {
|
||||
if (string.IsNullOrEmpty(depotKey)) {
|
||||
throw new ArgumentNullException(nameof(depotKey));
|
||||
}
|
||||
|
||||
return (depotKey.Length == 64) && Utilities.IsValidHexadecimalText(depotKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +21,8 @@
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
|
||||
public sealed class GlobalConfigExtension {
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
public SteamTokenDumperConfig? SteamTokenDumperPlugin { get; private set; }
|
||||
@@ -32,4 +33,3 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns="" id="root">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||
<xsd:attribute name="type" type="xsd:string"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string"/>
|
||||
<xsd:attribute name="name" type="xsd:string"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>{0} быў адключаны з-за адсутнасці токена зборкі</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} зараз адключаны ў адпаведнасці з вашай канфігурацыяй. Калі вы хочаце дапамагчы SteamDB у адпраўцы даных, калі ласка, зазірніце ў нашу вікі.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} быў паспяхова ініцыялізаваны, загадзя дзякуй за дапамогу. Першая адпраўка адбудзецца прыкладна праз {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>Не атрымалася загрузіць {0}, будзе ініцыялізаваны новы асобнік...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>На гэтым асобніку бота няма праграм, якія патрабуюць абнаўлення.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Атрыманне ў агульнай складанасці {0} токенаў доступу праграмы...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Атрыманне {0} токенаў доступу праграмы...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Скончана атрыманне {0} токенаў доступу праграмы.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Завершана атрыманне {0} токенаў доступу праграмы.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Атрыманне ўсіх сховішч для агульнай колькасці праграм {0}...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Атрыманне інфармацыі пра праграму {0}...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Завершана атрыманне інфармацыі пра праграму {0}.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Атрыманне {0} ключоў сховішча...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Завершана атрыманне {0} ключоў сховішча.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Завершана атрыманне ўсіх ключоў ключоў сховішча для {0} праграм.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Новых дадзеных для адпраўкі няма, усё актуальна.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Памылка адпраўкі дадзеных: карэктны SteamID не быў прадстаўлены. Праверце правільнасць налады {0}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SteamOwnerID") that the user is expected to set</comment>
|
||||
</data>
|
||||
<data name="SubmissionInProgress" xml:space="preserve">
|
||||
<value>Адпраўка агульнай колькасці зарэгістраваных праграм/пакетаў/сховішч: {0}/{1}/{2}...</value>
|
||||
<comment>{0} will be replaced by the number of app access tokens being submitted, {1} will be replaced by the number of package access tokens being submitted, {2} will be replaced by the number of depot keys being submitted</comment>
|
||||
</data>
|
||||
<data name="SubmissionFailedTooManyRequests" xml:space="preserve">
|
||||
<value>Адпраўка не ўдалася з-за занадта вялікай колькасці адпраўленых запытаў. Мы паўтарым спробу прыкладна праз {0}.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Дадзеныя былі паспяхова адпраўлены. Сервер зарэгістраваў агульную колькасць новых праграм/пакетаў/сховішч: {0} ({1} праверана)/{2} ({3} праверана)/{4} ({5} праверана).</value>
|
||||
<comment>{0} will be replaced by the number of new app access tokens that the server has registered, {1} will be replaced by the number of verified app access tokens that the server has registered, {2} will be replaced by the number of new package access tokens that the server has registered, {3} will be replaced by the number of verified package access tokens that the server has registered, {4} will be replaced by the number of new depot keys that the server has registered, {5} will be replaced by the number of verified depot keys that the server has registered</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
<value>Новыя праграмы: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
|
||||
<value>Правераныя праграмы: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
|
||||
<value>Новыя пакеты: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
|
||||
<value>Правераныя пакеты: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewDepots" xml:space="preserve">
|
||||
<value>Новыя сховішчы: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedDepots" xml:space="preserve">
|
||||
<value>Правераныя сховішчы: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="PluginSecretListInitialized" xml:space="preserve">
|
||||
<value>{0} ініцыялізаваны, убудова не працуе з ніводным з наступных: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>Загрузка глабальнага кэша STD...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Праверка цэласнасці глабальнага кэша STD...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Не ўдалося праверыць цэласнасць глабальнага кэша STD. Гэта сведчыць аб магчымым пашкоджанні файла/памяці, замест гэтага будзе ініцыялізаваны новы асобнік.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -62,31 +62,119 @@
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>{0} е била деактивирана поради липсващ токън за изграждане</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} в момента е деактивирана според вашата настройка. Ако искате да помогнете на SteamDB при подаването на данни, моля разгледайте нашата уикипедия.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} е инициализиранa успешно, благодаря Ви предварително за вашата помощ. Първото подаване на данни ще се случи приблизително след {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} не беше успешно заредена, вместо това ще бъде стартирана нова инстанция...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>Няма нови игри или приложения, които да изискват презареждане на бота.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Събиране на общо {0} входящи токени за игри или приложения...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Събиране на {0} входящи токени за игри или приложения...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Приключи събирането на {0} входящи токени за игри или приложения.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Приключи събиране на общо {0} входящи токени за игри или приложения.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Събиране на всички депа за общо {0} игри или проложения...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Събиране на {0} информация за играта или приложението...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Приключи събирането на {0} информация за играта или приложението.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Събиране {0} ключове за депо...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Приключи събирането {0} ключове за депо.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Приключи събирането на всички ключове за депа за общо {0} игри или проложения.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Няма нова информация за подаване, цялата информация е актуална.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Подаването на данните не може да се изпълни, защото няма валиден SteamID зададен като автор или сътрудник. Помислете да настроите {0} правилно.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SteamOwnerID") that the user is expected to set</comment>
|
||||
</data>
|
||||
<data name="SubmissionInProgress" xml:space="preserve">
|
||||
<value>Подаване на общо регистрирани игри/приложения/пакети/депа: {0}/{1}/{2}...</value>
|
||||
<comment>{0} will be replaced by the number of app access tokens being submitted, {1} will be replaced by the number of package access tokens being submitted, {2} will be replaced by the number of depot keys being submitted</comment>
|
||||
</data>
|
||||
<data name="SubmissionFailedTooManyRequests" xml:space="preserve">
|
||||
<value>Подаването е неуспешно поради твърде много изпратени заявки, ще опитаме отново приблизително след {0}.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Данните са изпратени успешно. Сървърът регистрира общо нови игри/приложения/пакети/депа: {0} ({1} потвърдени)/{2} ({3} потвърдени)/{4} ({5} потвърдени).</value>
|
||||
<comment>{0} will be replaced by the number of new app access tokens that the server has registered, {1} will be replaced by the number of verified app access tokens that the server has registered, {2} will be replaced by the number of new package access tokens that the server has registered, {3} will be replaced by the number of verified package access tokens that the server has registered, {4} will be replaced by the number of new depot keys that the server has registered, {5} will be replaced by the number of verified depot keys that the server has registered</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
<value>Нови игри / приложения: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
|
||||
<value>Потвърдени игри / приложения: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
|
||||
<value>Нови пакети: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
|
||||
<value>Потвърдени пакети: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewDepots" xml:space="preserve">
|
||||
<value>Нови депа: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedDepots" xml:space="preserve">
|
||||
<value>Потвърдени депа: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="PluginSecretListInitialized" xml:space="preserve">
|
||||
<value>{0} стартирана, плъгинът няма да разреши нито една от тези: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>Зареждане на STD общ кеш...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Потвърждаване на цялостта на STD общия кеш...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Не успя да провери целостта на глобалния STD кеш. Това предполага потенциална грешка, вместо това ще бъде стартирана нова инстанция.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -62,31 +62,119 @@
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>{0} byl zakázán z důvodu chybějícího tokenu</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} je v současné době v souladu s vaší konfigurací zakázán. Pokud byste chtěli pomoci SteamDB při odesílání dat, podívejte se na naši wiki.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} byl úspěšně inicializován, předem vám děkujeme za vaši pomoc. První příspěvek se od teď stane přibližně za {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} nelze načíst, nová instance bude inicializována...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>Neexistují žádné aplikace, které by vyžadovaly aktualizaci této instance bota.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Načítám celkem {0} přístupových tokenů...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Načítání {0} přístupových tokenů...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Načítání {0} přístupových tokenů bylo dokončeno.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Dokončeno načítání celkem {0} přístupových tokenů.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Načítání všech úložišť, celkem z {0} aplikací...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Získávání {0} informací o aplikaci...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Načítání informací o aplikaci {0} bylo dokončeno.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Získávání {0} tokenů úložišť...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Načítání {0} přístupových tokenů bylo dokončeno.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Načítání všech tokenbů úložišť, celkem z {0} aplikací bylo dokončeno.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Nejsou k dispozici žádné nové údaje k odeslání, vše je aktuální.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Data nelze odeslat, protože neexistuje žádné platné SteamID, které bychom mohli klasifikovat jako přispěvatele. Zvažte nastavení {0} parametrů.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SteamOwnerID") that the user is expected to set</comment>
|
||||
</data>
|
||||
<data name="SubmissionInProgress" xml:space="preserve">
|
||||
<value>Odesílání celkem registrovaných aplikací/balíčků/úložišť: {0}/{1}/{2}...</value>
|
||||
<comment>{0} will be replaced by the number of app access tokens being submitted, {1} will be replaced by the number of package access tokens being submitted, {2} will be replaced by the number of depot keys being submitted</comment>
|
||||
</data>
|
||||
<data name="SubmissionFailedTooManyRequests" xml:space="preserve">
|
||||
<value>Odeslání se nezdařilo z důvodu příliš mnoha odeslaných požadavků. Pokusíme se znovu přibližně za {0}.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Data byla úspěšně odeslána. Server zaregistroval celkem nové aplikace/balíčky/úložiště: {0} ({1} ověřeno)/{2} ({3} ověřeno)/{4} ({5} ověřeno).</value>
|
||||
<comment>{0} will be replaced by the number of new app access tokens that the server has registered, {1} will be replaced by the number of verified app access tokens that the server has registered, {2} will be replaced by the number of new package access tokens that the server has registered, {3} will be replaced by the number of verified package access tokens that the server has registered, {4} will be replaced by the number of new depot keys that the server has registered, {5} will be replaced by the number of verified depot keys that the server has registered</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
<value>Nové aplikace: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
|
||||
<value>Ověřené aplikace: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
|
||||
<value>Nové balíčky: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
|
||||
<value>Ověřené balíčky: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewDepots" xml:space="preserve">
|
||||
<value>Nová úložiště: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedDepots" xml:space="preserve">
|
||||
<value>Ověřená úložiště: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="PluginSecretListInitialized" xml:space="preserve">
|
||||
<value>{0} inicializován, žádný plugin nebude rozpoznávat: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>Načítání globální mezipaměti STD...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Ověřování globální integrity STD keše...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Ověření globální integrity STD keše se nezdařilo. To naznačuje, že může dojít k poškození souboru/paměti, místo toho bude inicializována nová instance.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -168,4 +168,13 @@
|
||||
<value>{0} wurde initialisiert, das Plugin wird keinen der folgenden Werte verarbeiten: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>Globaler STD-Cache wird geladen...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Überprüfe STD globale Cache-Integrität...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Fehler beim Überprüfen der globalen STD-Cache-Integrität. Dies deutet auf eine mögliche Datei-/Speicher-Beschädigung hin; stattdessen wird eine neue Instanz initialisiert.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -168,4 +168,13 @@
|
||||
<value>{0} αρχικοποιήθηκε, το plugin δεν θα επιλύσει κανένα από αυτά: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>Φόρτωση καθολικής μνήμης cache...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Επικύρωση ακεραιότητας καθολικής λανθάνουσας μνήμης STD...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Αποτυχία επαλήθευσης ακεραιότητας καθολικής λανθάνουσας μνήμης STD. Αυτό υποδηλώνει πιθανή διαφθορά αρχείου/μνήμης, αντ' αυτού θα ξεκινήσει μια νέα διεργασία.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -62,31 +62,119 @@
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>{0} on poistettu käytöstä puuttuvan koontitunnuksen vuoksi</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} on tällä hetkellä poistettu käytöstä asetuksistasi. Jos haluat auttaa SteamDB:tä tietojen lähettämisessä, ole hyvä ja tutustu wikimme.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} on alustettu onnistuneesti, kiitos etukäteen avustasi. Ensimmäinen lähetys tapahtuu noin {1} jälkeen.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} ei voitu ladata. Uusi instanssi alustetaan...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>Ei ole sovelluksia, jotka vaatisivat päivitystä tässä botin instanssissa.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Haetaan yhteensä {0} sovelluksen käyttötunnisteita...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Haetaan {0} sovelluksen käyttötunnisteita...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Saatiin haettua {0} sovelluksen käyttötunnisteet.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Saatiin haettua yhteensä {0} sovelluksen käyttötunnisteet.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Haetaan kaikkia depotteja yhteensä {0} sovellukselle...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Haetaan {0} sovelluksen tietoja...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Saatiin haettua {0} sovelluksen tiedot.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Haetaan {0} depot-avainta...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Saatiin haettua {0} depot-avainta.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Saatiin haettua kaikki depot-avaimet yhteensä {0} sovellukselle.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Uutta dataa ei ole lähetettäväksi, kaikki on ajan tasalla.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Tietoja ei voitu lähettää, koska ei ole voimassa olevaa SteamID-ryhmää, jonka voisimme luokitella osallistujaksi. Harkitse ominaisuuden {0} asettamista.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SteamOwnerID") that the user is expected to set</comment>
|
||||
</data>
|
||||
<data name="SubmissionInProgress" xml:space="preserve">
|
||||
<value>Lähetetään yhteensä {0}/{1}/{2} rekisteröityjä sovelluksia/paketteja/varikoita...</value>
|
||||
<comment>{0} will be replaced by the number of app access tokens being submitted, {1} will be replaced by the number of package access tokens being submitted, {2} will be replaced by the number of depot keys being submitted</comment>
|
||||
</data>
|
||||
<data name="SubmissionFailedTooManyRequests" xml:space="preserve">
|
||||
<value>Lähetys epäonnistui liian monen pyynnön vuoksi, yritämme uudelleen noin {0} kuluttua.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Tiedot on lähetetty onnistuneesti. Palvelin on rekisteröinyt yhteensä uusia sovelluksia/paketteja/depoteja: {0} ({1} vahvistettu)/{2} ({3} vahvistettu)/{4} ({5} vahvistettu).</value>
|
||||
<comment>{0} will be replaced by the number of new app access tokens that the server has registered, {1} will be replaced by the number of verified app access tokens that the server has registered, {2} will be replaced by the number of new package access tokens that the server has registered, {3} will be replaced by the number of verified package access tokens that the server has registered, {4} will be replaced by the number of new depot keys that the server has registered, {5} will be replaced by the number of verified depot keys that the server has registered</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
<value>Uudet sovellukset: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
|
||||
<value>Vahvistetut sovellukset: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
|
||||
<value>Uudet paketit: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
|
||||
<value>Vahvistetut paketit: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewDepots" xml:space="preserve">
|
||||
<value>Uudet depotit: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedDepots" xml:space="preserve">
|
||||
<value>Vahvistetut depotit: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="PluginSecretListInitialized" xml:space="preserve">
|
||||
<value>{0} alustettu, laajennus ei käsittele yhtään näistä: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>Ladataan STD:n globaalia välimuistia...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Tarkistetaan STD-välimuistin eheys...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>STD:n globaalin välimuistin eheyden varmistaminen epäonnistui. Tämä viittaa mahdolliseen tiedoston/muistin korruptioon, uusi instanssi käynnistetään sen sijaan.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -82,11 +82,35 @@
|
||||
|
||||
|
||||
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
<value>יישומים חדשים: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
|
||||
<value>יישומים מאומתים: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
|
||||
<value>חבילות חדשות: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
|
||||
<value>חבילות מאומתות: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginSecretListInitialized" xml:space="preserve">
|
||||
<value>{0} אותחל, הפלאגין לא יפתור אף אחד מאלה: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>טוען מטמון עולמי מסוג STD...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>מאמת את שלמות מטמון ה-STD העולמי...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>נכשל אימות שלמות מטמון ה-STD העולמי. זה מרמז על פגיעה פוטנציאלית בקובץ/זיכרון, במקום זה הוא יאותחל מחדש.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -88,6 +88,15 @@
|
||||
|
||||
|
||||
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
<value>Új appok: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
|
||||
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
|
||||
<value>Új csomagok: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -101,13 +101,22 @@
|
||||
<value>Ricezione di tutti i depositi per un totale di {0} app...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Recuperate {0} informazioni app...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Hai completato il recupero di {0} informazioni app.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
|
||||
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Recupero {0} chiavi...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Completato il recupero di {0} chiavi.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Finito il recupero di tutte le chiavi del deposito per un totale di {0} applicazioni.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
@@ -143,8 +152,29 @@
|
||||
<value>Nuovi pacchetti: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
|
||||
<value>Pacchetti verificati: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewDepots" xml:space="preserve">
|
||||
<value>Nuove app: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedDepots" xml:space="preserve">
|
||||
<value>App verificate: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="PluginSecretListInitialized" xml:space="preserve">
|
||||
<value>{0} inizializzato, il plugin non risolverà nessuno dei seguenti: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>Caricamento cache globale STD...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Convalida integrità cache globale STD...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Impossibile verificare l'integrità globale della cache STD. Questo suggerisce un potenziale danneggiamento di file/memoria, una nuova istanza verrà inizializzata.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -63,42 +63,42 @@
|
||||
</value>
|
||||
</resheader>
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>{0} foi desativado devido à falta de um token de compilação</value>
|
||||
<value>O {0} foi desativado devido a um token de compilação ausente</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} está desativado de acordo com sua configuração. Se você gostaria de ajudar o SteamDB no envio de dados, por favor, confira nosso wiki.</value>
|
||||
<value>O {0} está desativado de acordo com a sua configuração. Caso deseje ajudar o SteamDB com o envio de informações, dê uma olhada na nossa wiki.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} foi inicializado com sucesso, obrigado antecipadamente pela sua ajuda. O primeiro envio ocorrerá em aproximadamente {1} a partir de agora.</value>
|
||||
<value>O {0} foi inicializado com sucesso, agradecemos a sua ajuda. O primeiro envio ocorrerá em aproximadamente {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} não pôde ser carregado, uma instância nova será inicializada...</value>
|
||||
<value>O {0} não pôde ser carregado, uma instância nova será inicializada...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>Não há aplicativos que necessitem de ser atualizados nesta instância de bot.</value>
|
||||
<value>Não há aplicativos que exijam atualizações na instância atual.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Recuperando um total de {0} tokens de acesso a aplicativos...</value>
|
||||
<value>Recuperando um total de {0} tokens de acesso para aplicativos...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Recuperando {0} tokens de acesso a aplicativos...</value>
|
||||
<value>Recuperando {0} tokens...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Concluímos a recuperação de {0} tokens de acesso ao aplicativo.</value>
|
||||
<value>Recuperamos {0} tokens.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Obtivemos um total de {0} tokens de acesso aos aplicativos.</value>
|
||||
<value>Recuperamos um total de {0} tokens de acesso.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Recuperando todos os depósitos por um total de {0} apps...</value>
|
||||
<value>Recuperando depots para todos os {0} aplicativos...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
@@ -106,38 +106,38 @@
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Concluímos a recuperação de {0} informações de aplicativo.</value>
|
||||
<value>Recuperamos um total de {0} informações de aplicativos.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Recuperando {0} chaves de depósito...</value>
|
||||
<value>Recuperando {0} códigos de depots...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Terminamos de recuperar {0} chaves de depósito.</value>
|
||||
<value>Recuperamos códigos para {0} depots.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Terminamos de recuperar todas as chaves de depósito para um total de {0} apps.</value>
|
||||
<value>Recuperamos códigos de acesso para {0} aplicativos.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Não há novos dados para enviar, tudo está atualizado.</value>
|
||||
<value>Não há novos dados a serem enviados. Tudo está atualizado.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Não foi possível enviar os dados porque não há um conjunto SteamID válido que possamos classificar como colaborador. Considere configurar a propriedade {0}.</value>
|
||||
<value>Não foi possível enviar os dados porque não conseguimos identificar um ID Steam válido para definir como colaborador. Considere configurar a propriedade "{0}".</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SteamOwnerID") that the user is expected to set</comment>
|
||||
</data>
|
||||
<data name="SubmissionInProgress" xml:space="preserve">
|
||||
<value>Enviando um total de apps/pacotes/pacotes registrados: {0}/{1}/{2}...</value>
|
||||
<value>Enviando um total de {0} aplicativos, {1} pacotes e {2} depots registrados...</value>
|
||||
<comment>{0} will be replaced by the number of app access tokens being submitted, {1} will be replaced by the number of package access tokens being submitted, {2} will be replaced by the number of depot keys being submitted</comment>
|
||||
</data>
|
||||
<data name="SubmissionFailedTooManyRequests" xml:space="preserve">
|
||||
<value>O envio falhou devido a muitas solicitações enviadas, tentaremos novamente em aproximadamente {0} a partir de agora.</value>
|
||||
<value>O envio falhou porque muitas solicitações foram enviadas pelo cliente. Tentaremos novamente em aproximadamente {0}.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Os dados foram enviados com sucesso. O servidor registrou um total de novos aplicativos/pacotes/depósitos: {0} ({1} verificado)/{2} ({3} verificado)/{4} ({5} verificado).</value>
|
||||
<value>Os dados foram enviados com sucesso. O servidor registrou um total de {0} aplicativos ({1} verificados), {2} pacotes ({3} verificados) e {4} depots ({5} verificados).</value>
|
||||
<comment>{0} will be replaced by the number of new app access tokens that the server has registered, {1} will be replaced by the number of verified app access tokens that the server has registered, {2} will be replaced by the number of new package access tokens that the server has registered, {3} will be replaced by the number of verified package access tokens that the server has registered, {4} will be replaced by the number of new depot keys that the server has registered, {5} will be replaced by the number of verified depot keys that the server has registered</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
@@ -157,15 +157,24 @@
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewDepots" xml:space="preserve">
|
||||
<value>Novos depósitos: {0}</value>
|
||||
<value>Novos depots: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedDepots" xml:space="preserve">
|
||||
<value>Depósitos verificados: {0}</value>
|
||||
<value>Depots verificados: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="PluginSecretListInitialized" xml:space="preserve">
|
||||
<value>{0} inicializado, o plugin não resolverá nenhum desses: {1}.</value>
|
||||
<value>{0} inicializado, o plugin ignorará os seguintes pacotes: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>Carregando cache global do STD...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Validando integridade do cache global do STD...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Falha ao verificar a integridade do cache global do STD. Isto pode indicar uma possível corrupção de arquivo/memória, uma nova instância será inicializada.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -164,5 +164,17 @@
|
||||
<value>Количество подтвержденных хранилищ: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
|
||||
<data name="PluginSecretListInitialized" xml:space="preserve">
|
||||
<value>{0} инициализирован, плагин не взаимодействует ни с одним из их: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>Загрузка глобального кэша STD...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Проверка целостности глобального кэша STD...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Не удалось проверить целостность глобального кэша STD. Это говорит о потенциальном повреждении файла/памяти, вместо этого будет инициализирован новый экземпляр.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -168,4 +168,7 @@
|
||||
<value>{0} inicializovaných, modul nebude pracovať so žiadnym: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns="" id="root">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||
<xsd:attribute name="type" type="xsd:string"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string"/>
|
||||
<xsd:attribute name="name" type="xsd:string"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>由於 {0} 缺少組建權杖而被停用</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>目前 {0} 已根據您的設定被停用。如果您想幫助 SteamDB 提交資料,請查看我們的 Wiki。</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>已成功初始化 {0},先感謝您的幫助。第一次提交將大約在 {1} 後進行。</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>無法載入 {0},將初始化一個新實例…</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>此 Bot 中沒有需要再刷新的應用程式。</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>正在檢索共 {0} 個應用程式存取權杖…</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>正在檢索 {0} 個應用程式存取權杖…</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>已完成檢索 {0} 個應用程式存取權杖。</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>已完成檢索共 {0} 個應用程式存取權杖。</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>正在檢索共 {0} 個應用程式的 Depot…</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>正在檢索 {0} 個應用程式資料…</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
|
||||
<value>已完成檢索 {0} 個應用程式資料。</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>正在檢索 {0} 個應用程式的 Depot 金鑰…</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>已完成檢索 {0} 個應用程式的 Depot 金鑰。</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>已完成檢索共 {0} 個應用程式的 Depot 金鑰。</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>沒有要提交的新資料,一切都是最新狀態。</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>無法提交資料,因為沒有可以讓我們歸類為貢獻者的有效 SteamID 集。 考慮設定 {0} 屬性。</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SteamOwnerID") that the user is expected to set</comment>
|
||||
</data>
|
||||
<data name="SubmissionInProgress" xml:space="preserve">
|
||||
<value>正在提交註冊的應用程式/程式包/Depot 共:{0}/{1}/{2}…</value>
|
||||
<comment>{0} will be replaced by the number of app access tokens being submitted, {1} will be replaced by the number of package access tokens being submitted, {2} will be replaced by the number of depot keys being submitted</comment>
|
||||
</data>
|
||||
<data name="SubmissionFailedTooManyRequests" xml:space="preserve">
|
||||
<value>由於發送的請求過多導致提交失敗,我們將在約 {0} 後重試。</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>已成功提交資料。伺服器共已註冊了新的應用程式/程式包/Depot 共:{0} ({1} 個已驗證)/{2} ({3} 個已驗證)/{4} ({5} 個已驗證)。</value>
|
||||
<comment>{0} will be replaced by the number of new app access tokens that the server has registered, {1} will be replaced by the number of verified app access tokens that the server has registered, {2} will be replaced by the number of new package access tokens that the server has registered, {3} will be replaced by the number of verified package access tokens that the server has registered, {4} will be replaced by the number of new depot keys that the server has registered, {5} will be replaced by the number of verified depot keys that the server has registered</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
<value>新的應用程式:{0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
|
||||
<value>已驗證的應用程式:{0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
|
||||
<value>新的程式包:{0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
|
||||
<value>已驗證的程式包:{0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewDepots" xml:space="preserve">
|
||||
<value>新的 Depot:{0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedDepots" xml:space="preserve">
|
||||
<value>已被驗證的 Depot:{0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="PluginSecretListInitialized" xml:space="preserve">
|
||||
<value>{0} 已被初始化,外掛程式將無法解析其中任何一個:{1}。</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>正在載入 STD 全域快取…</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>正在驗證 STD 全域快取完整性…</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>無法驗證 STD 全域快取完整性。這表示可能有檔案/記憶損壞,將初始化一個新實例。</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,92 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns="" id="root">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||
<xsd:attribute name="type" type="xsd:string"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string"/>
|
||||
<xsd:attribute name="name" type="xsd:string"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
@@ -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,29 +27,30 @@ using ArchiSteamFarm.Core;
|
||||
using Newtonsoft.Json;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
|
||||
internal sealed class RequestData {
|
||||
[JsonProperty(PropertyName = "guid", Required = Required.Always)]
|
||||
[JsonProperty("guid", Required = Required.Always)]
|
||||
private static string Guid => ASF.GlobalDatabase?.Identifier.ToString("N") ?? throw new InvalidOperationException(nameof(ASF.GlobalDatabase.Identifier));
|
||||
|
||||
[JsonProperty(PropertyName = "token", Required = Required.Always)]
|
||||
[JsonProperty("token", Required = Required.Always)]
|
||||
private static string Token => SharedInfo.Token;
|
||||
|
||||
[JsonProperty(PropertyName = "v", Required = Required.Always)]
|
||||
[JsonProperty("v", Required = Required.Always)]
|
||||
private static byte Version => SharedInfo.ApiVersion;
|
||||
|
||||
[JsonProperty(PropertyName = "apps", Required = Required.Always)]
|
||||
[JsonProperty("apps", Required = Required.Always)]
|
||||
private readonly ImmutableDictionary<string, string> Apps;
|
||||
|
||||
[JsonProperty(PropertyName = "depots", Required = Required.Always)]
|
||||
[JsonProperty("depots", Required = Required.Always)]
|
||||
private readonly ImmutableDictionary<string, string> Depots;
|
||||
|
||||
private readonly ulong SteamID;
|
||||
|
||||
[JsonProperty(PropertyName = "subs", Required = Required.Always)]
|
||||
[JsonProperty("subs", Required = Required.Always)]
|
||||
private readonly ImmutableDictionary<string, string> Subs;
|
||||
|
||||
[JsonProperty(PropertyName = "steamid", Required = Required.Always)]
|
||||
[JsonProperty("steamid", Required = Required.Always)]
|
||||
private string SteamIDText => new SteamID(SteamID).Render();
|
||||
|
||||
internal RequestData(ulong steamID, IReadOnlyCollection<KeyValuePair<uint, ulong>> apps, IReadOnlyCollection<KeyValuePair<uint, ulong>> accessTokens, IReadOnlyCollection<KeyValuePair<uint, string>> depots) {
|
||||
@@ -57,17 +58,9 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
throw new ArgumentOutOfRangeException(nameof(steamID));
|
||||
}
|
||||
|
||||
if (apps == null) {
|
||||
throw new ArgumentNullException(nameof(apps));
|
||||
}
|
||||
|
||||
if (accessTokens == null) {
|
||||
throw new ArgumentNullException(nameof(accessTokens));
|
||||
}
|
||||
|
||||
if (depots == null) {
|
||||
throw new ArgumentNullException(nameof(depots));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(apps);
|
||||
ArgumentNullException.ThrowIfNull(accessTokens);
|
||||
ArgumentNullException.ThrowIfNull(depots);
|
||||
|
||||
SteamID = steamID;
|
||||
|
||||
@@ -76,4 +69,3 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
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,17 +23,18 @@ 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 {
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty(PropertyName = "data", Required = Required.DisallowNull)]
|
||||
[JsonProperty("data", Required = Required.DisallowNull)]
|
||||
internal readonly InternalData? Data;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty(PropertyName = "success", Required = Required.Always)]
|
||||
[JsonProperty("success", Required = Required.Always)]
|
||||
internal readonly bool Success;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
|
||||
@@ -41,22 +42,22 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
private ResponseData() { }
|
||||
|
||||
internal sealed class InternalData {
|
||||
[JsonProperty(PropertyName = "new_apps", Required = Required.Always)]
|
||||
[JsonProperty("new_apps", Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<uint> NewApps = ImmutableHashSet<uint>.Empty;
|
||||
|
||||
[JsonProperty(PropertyName = "new_depots", Required = Required.Always)]
|
||||
[JsonProperty("new_depots", Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<uint> NewDepots = ImmutableHashSet<uint>.Empty;
|
||||
|
||||
[JsonProperty(PropertyName = "new_subs", Required = Required.Always)]
|
||||
[JsonProperty("new_subs", Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<uint> NewPackages = ImmutableHashSet<uint>.Empty;
|
||||
|
||||
[JsonProperty(PropertyName = "verified_apps", Required = Required.Always)]
|
||||
[JsonProperty("verified_apps", Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<uint> VerifiedApps = ImmutableHashSet<uint>.Empty;
|
||||
|
||||
[JsonProperty(PropertyName = "verified_depots", Required = Required.Always)]
|
||||
[JsonProperty("verified_depots", Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<uint> VerifiedDepots = ImmutableHashSet<uint>.Empty;
|
||||
|
||||
[JsonProperty(PropertyName = "verified_subs", Required = Required.Always)]
|
||||
[JsonProperty("verified_subs", Required = Required.Always)]
|
||||
internal readonly ImmutableHashSet<uint> VerifiedPackages = ImmutableHashSet<uint>.Empty;
|
||||
|
||||
[JsonConstructor]
|
||||
@@ -64,4 +65,3 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
}
|
||||
#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,18 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
|
||||
internal static class SharedInfo {
|
||||
internal const byte ApiVersion = 2;
|
||||
internal const byte AppInfosPerSingleRequest = byte.MaxValue;
|
||||
internal const byte HoursBetweenUploads = 24;
|
||||
internal const byte MaximumHoursBetweenRefresh = 8; // Per single bot account, makes sense to be 2 or 3 times less than MinimumHoursBetweenUploads
|
||||
internal const byte MaximumMinutesBeforeFirstUpload = 60; // Must be greater or equal to MinimumMinutesBeforeFirstUpload
|
||||
internal const byte MinimumHoursBetweenUploads = 24;
|
||||
internal const byte MinimumMinutesBeforeFirstUpload = 10; // Must be less or equal to MaximumMinutesBeforeFirstUpload
|
||||
internal const byte MinimumMinutesBetweenUploads = 5; // Rate limiting for the server
|
||||
internal const string ServerURL = "https://asf-token-dumper.xpaw.me";
|
||||
internal const string Token = "STEAM_TOKEN_DUMPER_TOKEN"; // This is filled automatically during our CI build with API key provided by xPaw for ASF project
|
||||
|
||||
internal static bool HasValidToken => Token.Length == 128;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +24,8 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using ArchiSteamFarm.IPC.Integration;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
public sealed class SteamTokenDumperConfig {
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
@@ -43,9 +44,8 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
public ImmutableHashSet<uint> SecretPackageIDs { get; private set; } = ImmutableHashSet<uint>.Empty;
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
public bool SkipAutoGrantPackages { get; private set; }
|
||||
public bool SkipAutoGrantPackages { get; private set; } = true;
|
||||
|
||||
[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,7 +24,8 @@ using ArchiSteamFarm.IPC.Controllers.Api;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
|
||||
[Route("Api/SteamTokenDumperPlugin")]
|
||||
public sealed class SteamTokenDumperController : ArchiController {
|
||||
[HttpGet(nameof(GlobalConfigExtension))]
|
||||
@@ -32,4 +33,3 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
[SwaggerOperation(Tags = new[] { nameof(GlobalConfigExtension) })]
|
||||
public ActionResult<GlobalConfigExtension> Get() => Ok(new GlobalConfigExtension());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +23,7 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Composition;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@@ -40,9 +41,10 @@ using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
|
||||
[Export(typeof(IPlugin))]
|
||||
internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotSteamClient, ISteamPICSChanges {
|
||||
internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotCommand2, IBotSteamClient, ISteamPICSChanges {
|
||||
[JsonProperty]
|
||||
internal static SteamTokenDumperConfig? Config { get; private set; }
|
||||
|
||||
@@ -52,6 +54,7 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
private static readonly Timer SubmissionTimer = new(SubmitData);
|
||||
|
||||
private static GlobalCache? GlobalCache;
|
||||
private static DateTimeOffset LastUploadAt = DateTimeOffset.MinValue;
|
||||
|
||||
[JsonProperty]
|
||||
public override string Name => nameof(SteamTokenDumperPlugin);
|
||||
@@ -61,7 +64,7 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
|
||||
public Task<uint> GetPreferredChangeNumberToStartFrom() => Task.FromResult(Config?.Enabled == true ? GlobalCache?.LastChangeNumber ?? 0 : 0);
|
||||
|
||||
public async void OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
|
||||
public async Task OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
|
||||
if (!SharedInfo.HasValidToken) {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledMissingBuildToken, nameof(SteamTokenDumperPlugin)));
|
||||
|
||||
@@ -99,8 +102,6 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
config.Enabled = true;
|
||||
}
|
||||
|
||||
Config = config;
|
||||
|
||||
if (!config.Enabled) {
|
||||
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledInConfig, nameof(SteamTokenDumperPlugin)));
|
||||
|
||||
@@ -131,21 +132,59 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
}
|
||||
|
||||
TimeSpan startIn = TimeSpan.FromMinutes(Utilities.RandomNext(SharedInfo.MinimumMinutesBeforeFirstUpload, SharedInfo.MaximumMinutesBeforeFirstUpload));
|
||||
Config = config;
|
||||
|
||||
#pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
|
||||
TimeSpan startIn = TimeSpan.FromMinutes(Random.Shared.Next(SharedInfo.MinimumMinutesBeforeFirstUpload, SharedInfo.MaximumMinutesBeforeFirstUpload));
|
||||
#pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
|
||||
|
||||
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
|
||||
lock (SubmissionSemaphore) {
|
||||
SubmissionTimer.Change(startIn, TimeSpan.FromHours(SharedInfo.MinimumHoursBetweenUploads));
|
||||
SubmissionTimer.Change(startIn, TimeSpan.FromHours(SharedInfo.HoursBetweenUploads));
|
||||
}
|
||||
|
||||
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginInitializedAndEnabled, nameof(SteamTokenDumperPlugin), startIn.ToHumanReadable()));
|
||||
}
|
||||
|
||||
public async void OnBotDestroy(Bot bot) {
|
||||
if (bot == null) {
|
||||
throw new ArgumentNullException(nameof(bot));
|
||||
public Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
if (!Enum.IsDefined(access)) {
|
||||
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
|
||||
}
|
||||
|
||||
if ((args == null) || (args.Length == 0)) {
|
||||
throw new ArgumentNullException(nameof(args));
|
||||
}
|
||||
|
||||
switch (args[0].ToUpperInvariant()) {
|
||||
case "STD" when access >= EAccess.Owner:
|
||||
if (Config is not { Enabled: true }) {
|
||||
return Task.FromResult((string?) string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(Config)));
|
||||
}
|
||||
|
||||
TimeSpan minimumTimeBetweenUpload = TimeSpan.FromMinutes(SharedInfo.MinimumMinutesBetweenUploads);
|
||||
|
||||
if (LastUploadAt + minimumTimeBetweenUpload > DateTimeOffset.UtcNow) {
|
||||
return Task.FromResult((string?) string.Format(CultureInfo.CurrentCulture, Strings.SubmissionFailedTooManyRequests, minimumTimeBetweenUpload.ToHumanReadable()));
|
||||
}
|
||||
|
||||
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
|
||||
lock (SubmissionSemaphore) {
|
||||
SubmissionTimer.Change(TimeSpan.Zero, TimeSpan.FromHours(SharedInfo.HoursBetweenUploads));
|
||||
}
|
||||
|
||||
return Task.FromResult((string?) ArchiSteamFarm.Localization.Strings.Done);
|
||||
case "STD" when access > EAccess.None:
|
||||
return Task.FromResult((string?) ArchiSteamFarm.Localization.Strings.ErrorAccessDenied);
|
||||
default:
|
||||
return Task.FromResult((string?) null);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnBotDestroy(Bot bot) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
if (BotSubscriptions.TryRemove(bot, out IDisposable? subscription)) {
|
||||
subscription.Dispose();
|
||||
}
|
||||
@@ -157,10 +196,8 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
}
|
||||
|
||||
public async void OnBotInit(Bot bot) {
|
||||
if (bot == null) {
|
||||
throw new ArgumentNullException(nameof(bot));
|
||||
}
|
||||
public async Task OnBotInit(Bot bot) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
if (Config is not { Enabled: true }) {
|
||||
return;
|
||||
@@ -176,21 +213,16 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
}
|
||||
|
||||
public void OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackManager) {
|
||||
if (bot == null) {
|
||||
throw new ArgumentNullException(nameof(bot));
|
||||
}
|
||||
|
||||
if (callbackManager == null) {
|
||||
throw new ArgumentNullException(nameof(callbackManager));
|
||||
}
|
||||
public Task OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackManager) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
ArgumentNullException.ThrowIfNull(callbackManager);
|
||||
|
||||
if (BotSubscriptions.TryRemove(bot, out IDisposable? subscription)) {
|
||||
subscription.Dispose();
|
||||
}
|
||||
|
||||
if (Config is not { Enabled: true }) {
|
||||
return;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
subscription = callbackManager.Subscribe<SteamApps.LicenseListCallback>(callback => OnLicenseList(bot, callback));
|
||||
@@ -198,27 +230,28 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
if (!BotSubscriptions.TryAdd(bot, subscription)) {
|
||||
subscription.Dispose();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<ClientMsgHandler>? OnBotSteamHandlersInit(Bot bot) => null;
|
||||
public Task<IReadOnlyCollection<ClientMsgHandler>?> OnBotSteamHandlersInit(Bot bot) => Task.FromResult((IReadOnlyCollection<ClientMsgHandler>?) null);
|
||||
|
||||
public override void OnLoaded() => Utilities.WarnAboutIncompleteTranslation(Strings.ResourceManager);
|
||||
public override Task OnLoaded() {
|
||||
Utilities.WarnAboutIncompleteTranslation(Strings.ResourceManager);
|
||||
|
||||
public void OnPICSChanges(uint currentChangeNumber, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> appChanges, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> packageChanges) {
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnPICSChanges(uint currentChangeNumber, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> appChanges, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> packageChanges) {
|
||||
if (currentChangeNumber == 0) {
|
||||
throw new ArgumentOutOfRangeException(nameof(currentChangeNumber));
|
||||
}
|
||||
|
||||
if (appChanges == null) {
|
||||
throw new ArgumentNullException(nameof(appChanges));
|
||||
}
|
||||
|
||||
if (packageChanges == null) {
|
||||
throw new ArgumentNullException(nameof(packageChanges));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(appChanges);
|
||||
ArgumentNullException.ThrowIfNull(packageChanges);
|
||||
|
||||
if (Config is not { Enabled: true }) {
|
||||
return;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (GlobalCache == null) {
|
||||
@@ -226,15 +259,17 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
|
||||
GlobalCache.OnPICSChanges(currentChangeNumber, appChanges);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void OnPICSChangesRestart(uint currentChangeNumber) {
|
||||
public Task OnPICSChangesRestart(uint currentChangeNumber) {
|
||||
if (currentChangeNumber == 0) {
|
||||
throw new ArgumentOutOfRangeException(nameof(currentChangeNumber));
|
||||
}
|
||||
|
||||
if (Config is not { Enabled: true }) {
|
||||
return;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (GlobalCache == null) {
|
||||
@@ -242,6 +277,8 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
|
||||
GlobalCache.OnPICSChangesRestart(currentChangeNumber);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static async void OnBotRefreshTimer(object? state) {
|
||||
@@ -253,13 +290,8 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
|
||||
private static async void OnLicenseList(Bot bot, SteamApps.LicenseListCallback callback) {
|
||||
if (bot == null) {
|
||||
throw new ArgumentNullException(nameof(bot));
|
||||
}
|
||||
|
||||
if (callback == null) {
|
||||
throw new ArgumentNullException(nameof(callback));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
ArgumentNullException.ThrowIfNull(callback);
|
||||
|
||||
if (Config is not { Enabled: true }) {
|
||||
return;
|
||||
@@ -277,9 +309,7 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
|
||||
private static async Task Refresh(Bot bot, IReadOnlyCollection<uint>? packageIDs = null) {
|
||||
if (bot == null) {
|
||||
throw new ArgumentNullException(nameof(bot));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
if (Config is not { Enabled: true }) {
|
||||
return;
|
||||
@@ -420,8 +450,6 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
}
|
||||
|
||||
GlobalCache.UpdateAppChangeNumbers(appChangeNumbers);
|
||||
|
||||
if (depotTasks.Count > 0) {
|
||||
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotRetrievingDepotKeys, depotTasks.Count));
|
||||
|
||||
@@ -439,6 +467,8 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
|
||||
GlobalCache.UpdateDepotKeys(results);
|
||||
}
|
||||
|
||||
GlobalCache.UpdateAppChangeNumbers(appChangeNumbers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,14 +494,14 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
throw new InvalidOperationException(nameof(GlobalCache));
|
||||
}
|
||||
|
||||
if (ASF.GlobalConfig == null) {
|
||||
throw new InvalidOperationException(nameof(ASF.GlobalConfig));
|
||||
}
|
||||
|
||||
if (ASF.WebBrowser == null) {
|
||||
throw new InvalidOperationException(nameof(ASF.WebBrowser));
|
||||
}
|
||||
|
||||
if (LastUploadAt + TimeSpan.FromMinutes(SharedInfo.MinimumMinutesBetweenUploads) > DateTimeOffset.UtcNow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await SubmissionSemaphore.WaitAsync(0).ConfigureAwait(false)) {
|
||||
return;
|
||||
}
|
||||
@@ -487,7 +517,7 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
return;
|
||||
}
|
||||
|
||||
ulong contributorSteamID = (ASF.GlobalConfig.SteamOwnerID > 0) && new SteamID(ASF.GlobalConfig.SteamOwnerID).IsIndividualAccount ? ASF.GlobalConfig.SteamOwnerID : Bot.Bots.Values.Where(static bot => bot.SteamID > 0).OrderByDescending(static bot => bot.OwnedPackageIDs.Count).FirstOrDefault()?.SteamID ?? 0;
|
||||
ulong contributorSteamID = ASF.GlobalConfig is { SteamOwnerID: > 0 } && new SteamID(ASF.GlobalConfig.SteamOwnerID).IsIndividualAccount ? ASF.GlobalConfig.SteamOwnerID : Bot.Bots.Values.Where(static bot => bot.SteamID > 0).MaxBy(static bot => bot.OwnedPackageIDs.Count)?.SteamID ?? 0;
|
||||
|
||||
if (contributorSteamID == 0) {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionNoContributorSet, nameof(ASF.GlobalConfig.SteamOwnerID)));
|
||||
@@ -500,7 +530,7 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
|
||||
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionInProgress, appTokens.Count, packageTokens.Count, depotKeys.Count));
|
||||
|
||||
ObjectResponse<ResponseData>? response = await ASF.WebBrowser.UrlPostToJsonObject<ResponseData, RequestData>(request, data: requestData, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
|
||||
ObjectResponse<ResponseData>? response = await ASF.WebBrowser.UrlPostToJsonObject<ResponseData, RequestData>(request, data: requestData, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
ASF.ArchiLogger.LogGenericWarning(ArchiSteamFarm.Localization.Strings.WarningFailed);
|
||||
@@ -508,6 +538,9 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
return;
|
||||
}
|
||||
|
||||
// We've communicated with the server and didn't timeout, regardless of the success, this was the last upload attempt
|
||||
LastUploadAt = DateTimeOffset.UtcNow;
|
||||
|
||||
if (response.StatusCode.IsClientErrorCode()) {
|
||||
ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, response.StatusCode));
|
||||
|
||||
@@ -516,11 +549,13 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
#else
|
||||
if (response.StatusCode == HttpStatusCode.TooManyRequests) {
|
||||
#endif
|
||||
TimeSpan startIn = TimeSpan.FromMinutes(Utilities.RandomNext(SharedInfo.MinimumMinutesBeforeFirstUpload, SharedInfo.MaximumMinutesBeforeFirstUpload));
|
||||
#pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
|
||||
TimeSpan startIn = TimeSpan.FromMinutes(Random.Shared.Next(SharedInfo.MinimumMinutesBeforeFirstUpload, SharedInfo.MaximumMinutesBeforeFirstUpload));
|
||||
#pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
|
||||
|
||||
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
|
||||
lock (SubmissionSemaphore) {
|
||||
SubmissionTimer.Change(startIn, TimeSpan.FromHours(SharedInfo.MinimumHoursBetweenUploads));
|
||||
SubmissionTimer.Change(startIn, TimeSpan.FromHours(SharedInfo.HoursBetweenUploads));
|
||||
}
|
||||
|
||||
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionFailedTooManyRequests, startIn.ToHumanReadable()));
|
||||
@@ -529,7 +564,7 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.Content.Success) {
|
||||
if (response.Content is not { Success: true }) {
|
||||
ASF.ArchiLogger.LogGenericError(ArchiSteamFarm.Localization.Strings.WarningFailed);
|
||||
|
||||
return;
|
||||
@@ -573,4 +608,3 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
@@ -26,7 +26,8 @@ using ArchiSteamFarm.Steam.Data;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using static ArchiSteamFarm.Steam.Bot;
|
||||
|
||||
namespace ArchiSteamFarm.Tests {
|
||||
namespace ArchiSteamFarm.Tests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class Bot {
|
||||
[TestMethod]
|
||||
@@ -484,13 +485,8 @@ namespace ArchiSteamFarm.Tests {
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
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);
|
||||
@@ -507,4 +503,3 @@ namespace ArchiSteamFarm.Tests {
|
||||
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,7 +27,8 @@ using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using static ArchiSteamFarm.Steam.Integration.SteamChatMessage;
|
||||
|
||||
namespace ArchiSteamFarm.Tests {
|
||||
namespace ArchiSteamFarm.Tests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class SteamChatMessage {
|
||||
[TestMethod]
|
||||
@@ -35,16 +36,16 @@ namespace ArchiSteamFarm.Tests {
|
||||
string prefix = new('x', MaxMessagePrefixBytes);
|
||||
|
||||
const string emoji = "😎";
|
||||
const string message = emoji + emoji + emoji + emoji;
|
||||
const string message = $"{emoji}{emoji}{emoji}{emoji}";
|
||||
|
||||
List<string> output = await GetMessageParts(message, prefix, true).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(4, output.Count);
|
||||
|
||||
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($"{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]
|
||||
@@ -70,14 +71,14 @@ namespace ArchiSteamFarm.Tests {
|
||||
const string emoji = "😎";
|
||||
|
||||
string longSequence = new('a', longLineLength - 1);
|
||||
string message = longSequence + emoji;
|
||||
string message = $"{longSequence}{emoji}";
|
||||
|
||||
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(2, output.Count);
|
||||
|
||||
Assert.AreEqual(longSequence + ContinuationCharacter, output[0]);
|
||||
Assert.AreEqual(ContinuationCharacter + emoji, output[1]);
|
||||
Assert.AreEqual($"{longSequence}{ContinuationCharacter}", output[0]);
|
||||
Assert.AreEqual($"{ContinuationCharacter}{emoji}", output[1]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -121,7 +122,7 @@ namespace ArchiSteamFarm.Tests {
|
||||
|
||||
Assert.AreEqual(2, output.Count);
|
||||
|
||||
Assert.AreEqual(longLine + ContinuationCharacter, output[0]);
|
||||
Assert.AreEqual($"{longLine}{ContinuationCharacter}", output[0]);
|
||||
Assert.AreEqual($@"{ContinuationCharacter}\[", output[1]);
|
||||
}
|
||||
|
||||
@@ -169,7 +170,7 @@ namespace ArchiSteamFarm.Tests {
|
||||
List<string> output = await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(1, output.Count);
|
||||
Assert.AreEqual(escapedPrefix + message, output.First());
|
||||
Assert.AreEqual($"{escapedPrefix}{message}", output.First());
|
||||
}
|
||||
|
||||
[DataRow(false)]
|
||||
@@ -180,16 +181,16 @@ namespace ArchiSteamFarm.Tests {
|
||||
int longLineLength = maxMessageBytes - ReservedContinuationMessageBytes;
|
||||
|
||||
string longLine = new('a', longLineLength);
|
||||
string message = longLine + longLine + longLine + longLine;
|
||||
string message = $"{longLine}{longLine}{longLine}{longLine}";
|
||||
|
||||
List<string> output = await GetMessageParts(message, isAccountLimited: isAccountLimited).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(4, output.Count);
|
||||
|
||||
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($"{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]
|
||||
@@ -296,15 +297,15 @@ namespace ArchiSteamFarm.Tests {
|
||||
}
|
||||
|
||||
string newlinePart = newlinePartBuilder.ToString();
|
||||
string message = newlinePart + Environment.NewLine + newlinePart + Environment.NewLine + newlinePart + Environment.NewLine + newlinePart;
|
||||
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}{ParagraphCharacter}", output[0]);
|
||||
Assert.AreEqual($"{newlinePart}{ParagraphCharacter}", output[1]);
|
||||
Assert.AreEqual($"{newlinePart}{ParagraphCharacter}", output[2]);
|
||||
Assert.AreEqual(newlinePart, output[3]);
|
||||
}
|
||||
|
||||
@@ -332,4 +333,3 @@ namespace ArchiSteamFarm.Tests {
|
||||
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,7 +24,8 @@ using ArchiSteamFarm.Steam.Data;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using static ArchiSteamFarm.Steam.Exchange.Trading;
|
||||
|
||||
namespace ArchiSteamFarm.Tests {
|
||||
namespace ArchiSteamFarm.Tests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class Trading {
|
||||
[TestMethod]
|
||||
@@ -389,4 +390,3 @@ namespace ArchiSteamFarm.Tests {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,11 +19,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using static ArchiSteamFarm.Core.Utilities;
|
||||
|
||||
namespace ArchiSteamFarm.Tests {
|
||||
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 {
|
||||
@@ -33,9 +35,28 @@ namespace ArchiSteamFarm.Tests {
|
||||
[TestMethod]
|
||||
public void ContextSpecificWordsWeakenPassphrases() => Assert.IsTrue(TestPasswordStrength("archisteamfarmpassword").IsWeak);
|
||||
|
||||
[TestMethod]
|
||||
public void EasyPasswordsHaveMeaningfulReason() {
|
||||
(bool isWeak, string? reason) = TestPasswordStrength("CorrectHorse");
|
||||
|
||||
Assert.IsTrue(isWeak);
|
||||
Assert.IsTrue(reason?.Contains("Capitalization doesn't help very much", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LongPassphraseIsNotWeak() => Assert.IsFalse(TestPasswordStrength("10chars<!>asdf").IsWeak);
|
||||
|
||||
[TestMethod]
|
||||
public void MemePasswordIsNotWeak() => Assert.IsFalse(TestPasswordStrength("correcthorsebatterystaple").IsWeak);
|
||||
|
||||
[TestMethod]
|
||||
public void RepeatedPasswordsHaveMeaningfulReason() {
|
||||
(bool isWeak, string? reason) = TestPasswordStrength("abcabcabc");
|
||||
|
||||
Assert.IsTrue(isWeak);
|
||||
Assert.IsTrue(reason?.Contains("Avoid repeated words and characters", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RepetitiveCharactersWeakenPassphrases() => Assert.IsTrue(TestPasswordStrength("testaaaatest").IsWeak);
|
||||
|
||||
@@ -47,6 +68,13 @@ namespace ArchiSteamFarm.Tests {
|
||||
|
||||
[TestMethod]
|
||||
public void ShortPassphraseIsWeak() => Assert.IsTrue(TestPasswordStrength("four").IsWeak);
|
||||
|
||||
[TestMethod]
|
||||
public void StraightRowsPasswordsHaveMeaningfulReason() {
|
||||
(bool isWeak, string? reason) = TestPasswordStrength("`1234567890-=");
|
||||
|
||||
Assert.IsTrue(isWeak);
|
||||
Assert.IsTrue(reason?.Contains("Straight rows of keys are easy to guess", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
#pragma warning restore CA1724 // We don't care about the potential conflict, as ASF class name has a priority
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeLocalFunctionBody/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeMethodOrOperatorBody/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeMissingParentheses/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeNamespaceBody/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeObjectCreationWhenTypeNotEvident/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeRedundantParentheses/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeStaticMemberQualifier/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
@@ -238,6 +239,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=OverriddenWithSameValue/@EntryIndexedValue">HINT</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ParameterTypeCanBeEnumerable_002EGlobal/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ParameterTypeCanBeEnumerable_002ELocal/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PassStringInterpolation/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PatternAlwaysMatches/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PatternAlwaysOfType/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PropertyNotResolved/@EntryIndexedValue">WARNING</s:String>
|
||||
@@ -252,6 +254,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantCommaInInitializer/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantDeclarationSemicolon/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantEmptyObjectCreationArgumentList/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantEnumCaseLabelForDefaultSection/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantIfElseBlock/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantLambdaSignatureParentheses/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantLinebreak/@EntryIndexedValue">WARNING</s:String>
|
||||
@@ -289,6 +292,10 @@
|
||||
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=TailRecursiveCall/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=TryStatementsCanBeMerged/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=Unity_002EPerformanceCriticalCodeCameraMain/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=Unity_002EPerformanceCriticalCodeInvocation/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=Unity_002EPerformanceCriticalCodeNullComparison/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=Unity_002EPreferGuidReference/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnnecessaryWhitespace/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseConfigureAwaitFalseForAsyncDisposable/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseDeconstruction/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
@@ -299,6 +306,8 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNullPropagation/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNullPropagationWhenPossible/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UsePositionalDeconstructionPattern/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseStringInterpolationWhenPossible/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseThrowIfNullMethod/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseVerbatimString/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=WrongIndentSize/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=WrongMetadataUse/@EntryIndexedValue">WARNING</s:String>
|
||||
@@ -389,6 +398,7 @@
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_OWNER_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGNMENT_TAB_FILL_STYLE/@EntryValue">USE_TABS_ONLY</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_BINARY_EXPRESSIONS_CHAIN/@EntryValue">False</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_STATEMENT_CONDITIONS/@EntryValue">False</s:Boolean>
|
||||
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ANONYMOUS_METHOD_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
|
||||
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AFTER_CONTROL_TRANSFER_STATEMENTS/@EntryValue">1</s:Int64>
|
||||
@@ -402,6 +412,7 @@
|
||||
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_INSIDE_REGION/@EntryValue">0</s:Int64>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/CASE_BLOCK_BRACES/@EntryValue">END_OF_LINE</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/EMPTY_BLOCK_STYLE/@EntryValue">TOGETHER_SAME_LINE</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_PREPROCESSOR_REGION/@EntryValue">NO_INDENT</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_STYLE/@EntryValue">Tab</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INITIALIZER_BRACES/@EntryValue">END_OF_LINE</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INVOCABLE_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
|
||||
@@ -417,6 +428,7 @@
|
||||
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_INITIALIZER_ELEMENTS_ON_LINE/@EntryValue">1</s:Int64>
|
||||
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/OTHER_BRACES/@EntryValue">END_OF_LINE</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/OUTDENT_STATEMENT_LABELS/@EntryValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSOR_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSORHOLDER_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_CATCH_ON_NEW_LINE/@EntryValue">False</s:Boolean>
|
||||
@@ -431,6 +443,7 @@
|
||||
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_WITHING_EMPTY_BRACES/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_WITHIN_SINGLE_LINE_ARRAY_INITIALIZER_BRACES/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/STICK_COMMENT/@EntryValue">False</s:Boolean>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/TYPE_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/USE_INDENT_FROM_VS/@EntryValue">False</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_AFTER_DECLARATION_LPAR/@EntryValue">True</s:Boolean>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<PackageReference Include="zxcvbn-core" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'net48' AND ('$(TargetGeneric)' == 'true' OR '$(TargetWindows)' == 'true')">
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'net48'">
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cors" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Localization" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.ResponseCaching" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||
@@ -64,7 +63,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\LICENSE-2.0.txt">
|
||||
<Content Include="..\LICENSE.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
@@ -79,4 +78,20 @@
|
||||
<Link>www\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="Exists($([System.IO.Path]::Combine('overlay', 'variant-base', $(ASFVariant.Split('-')[0]))))">
|
||||
<Content Include="overlay/variant-base/$(ASFVariant.Split('-')[0])/**/*.*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="Exists($([System.IO.Path]::Combine('overlay', 'variant-specific', $(ASFVariant))))">
|
||||
<Content Include="overlay/variant-specific/$(ASFVariant)/**/*.*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -24,10 +24,10 @@ using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: CLSCompliant(false)]
|
||||
|
||||
#if DEBUG
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.Tests")]
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper")]
|
||||
#else
|
||||
#if ASF_SIGNED_BUILD
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.Tests, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")]
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")]
|
||||
#else
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.Tests")]
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper")]
|
||||
#endif
|
||||
|
||||
@@ -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,7 +23,8 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ArchiSteamFarm.Collections {
|
||||
namespace ArchiSteamFarm.Collections;
|
||||
|
||||
internal sealed class ConcurrentEnumerator<T> : IEnumerator<T> {
|
||||
public T Current => Enumerator.Current;
|
||||
|
||||
@@ -33,9 +34,7 @@ namespace ArchiSteamFarm.Collections {
|
||||
object? IEnumerator.Current => Current;
|
||||
|
||||
internal ConcurrentEnumerator(IReadOnlyCollection<T> collection, IDisposable lockObject) {
|
||||
if (collection == null) {
|
||||
throw new ArgumentNullException(nameof(collection));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(collection);
|
||||
|
||||
LockObject = lockObject ?? throw new ArgumentNullException(nameof(lockObject));
|
||||
Enumerator = collection.GetEnumerator();
|
||||
@@ -49,4 +48,3 @@ namespace ArchiSteamFarm.Collections {
|
||||
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,7 +26,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ArchiSteamFarm.Collections {
|
||||
namespace ArchiSteamFarm.Collections;
|
||||
|
||||
public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where T : notnull {
|
||||
public event EventHandler? OnModified;
|
||||
|
||||
@@ -38,9 +39,7 @@ namespace ArchiSteamFarm.Collections {
|
||||
public ConcurrentHashSet() => BackingCollection = new ConcurrentDictionary<T, bool>();
|
||||
|
||||
public ConcurrentHashSet(IEqualityComparer<T> comparer) {
|
||||
if (comparer == null) {
|
||||
throw new ArgumentNullException(nameof(comparer));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(comparer);
|
||||
|
||||
BackingCollection = new ConcurrentDictionary<T, bool>(comparer);
|
||||
}
|
||||
@@ -70,9 +69,7 @@ namespace ArchiSteamFarm.Collections {
|
||||
public void CopyTo(T[] array, int arrayIndex) => BackingCollection.Keys.CopyTo(array, arrayIndex);
|
||||
|
||||
public void ExceptWith(IEnumerable<T> other) {
|
||||
if (other == null) {
|
||||
throw new ArgumentNullException(nameof(other));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
foreach (T item in other) {
|
||||
Remove(item);
|
||||
@@ -150,9 +147,7 @@ namespace ArchiSteamFarm.Collections {
|
||||
}
|
||||
|
||||
public void UnionWith(IEnumerable<T> other) {
|
||||
if (other == null) {
|
||||
throw new ArgumentNullException(nameof(other));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
foreach (T otherElement in other) {
|
||||
Add(otherElement);
|
||||
@@ -202,4 +197,3 @@ namespace ArchiSteamFarm.Collections {
|
||||
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,7 +23,8 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
namespace ArchiSteamFarm.Collections {
|
||||
namespace ArchiSteamFarm.Collections;
|
||||
|
||||
internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
@@ -114,4 +115,3 @@ namespace ArchiSteamFarm.Collections {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +25,8 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using ArchiSteamFarm.Core;
|
||||
|
||||
namespace ArchiSteamFarm.Collections {
|
||||
namespace ArchiSteamFarm.Collections;
|
||||
|
||||
internal sealed class FixedSizeConcurrentQueue<T> : IEnumerable<T> {
|
||||
private readonly ConcurrentQueue<T> BackingQueue = new();
|
||||
|
||||
@@ -34,7 +35,7 @@ namespace ArchiSteamFarm.Collections {
|
||||
|
||||
set {
|
||||
if (value == 0) {
|
||||
ASF.ArchiLogger.LogNullError(nameof(value));
|
||||
ASF.ArchiLogger.LogNullError(value);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -74,4 +75,3 @@ namespace ArchiSteamFarm.Collections {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
126
ArchiSteamFarm/Collections/ObservableConcurrentDictionary.cs
Normal file
126
ArchiSteamFarm/Collections/ObservableConcurrentDictionary.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// |
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.Collections;
|
||||
|
||||
public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue> where TKey : notnull {
|
||||
public event EventHandler? OnModified;
|
||||
|
||||
[PublicAPI]
|
||||
public int Count => BackingDictionary.Count;
|
||||
|
||||
[PublicAPI]
|
||||
public bool IsEmpty => BackingDictionary.IsEmpty;
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly ConcurrentDictionary<TKey, TValue> BackingDictionary = new();
|
||||
|
||||
int ICollection<KeyValuePair<TKey, TValue>>.Count => BackingDictionary.Count;
|
||||
int IReadOnlyCollection<KeyValuePair<TKey, TValue>>.Count => BackingDictionary.Count;
|
||||
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => BackingDictionary.Keys;
|
||||
ICollection<TKey> IDictionary<TKey, TValue>.Keys => BackingDictionary.Keys;
|
||||
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => BackingDictionary.Values;
|
||||
ICollection<TValue> IDictionary<TKey, TValue>.Values => BackingDictionary.Values;
|
||||
|
||||
public TValue this[TKey key] {
|
||||
get => BackingDictionary[key];
|
||||
set {
|
||||
if (BackingDictionary.TryGetValue(key, out TValue? savedValue) && EqualityComparer<TValue>.Default.Equals(savedValue, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
BackingDictionary[key] = value;
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(KeyValuePair<TKey, TValue> item) {
|
||||
(TKey key, TValue value) = item;
|
||||
|
||||
Add(key, value);
|
||||
}
|
||||
|
||||
public void Add(TKey key, TValue value) => TryAdd(key, value);
|
||||
|
||||
public void Clear() {
|
||||
if (BackingDictionary.IsEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
BackingDictionary.Clear();
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public bool Contains(KeyValuePair<TKey, TValue> item) => ((ICollection<KeyValuePair<TKey, TValue>>) BackingDictionary).Contains(item);
|
||||
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) => ((ICollection<KeyValuePair<TKey, TValue>>) BackingDictionary).CopyTo(array, arrayIndex);
|
||||
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => BackingDictionary.GetEnumerator();
|
||||
|
||||
public bool Remove(KeyValuePair<TKey, TValue> item) {
|
||||
ICollection<KeyValuePair<TKey, TValue>> collection = BackingDictionary;
|
||||
|
||||
if (!collection.Remove(item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Remove(TKey key) {
|
||||
if (!BackingDictionary.TryRemove(key, out _)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IDictionary<TKey, TValue>.ContainsKey(TKey key) => BackingDictionary.ContainsKey(key);
|
||||
bool IReadOnlyDictionary<TKey, TValue>.ContainsKey(TKey key) => BackingDictionary.ContainsKey(key);
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
bool IReadOnlyDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) => BackingDictionary.TryGetValue(key, out value!);
|
||||
bool IDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) => BackingDictionary.TryGetValue(key, out value!);
|
||||
|
||||
[PublicAPI]
|
||||
public bool TryAdd(TKey key, TValue value) {
|
||||
if (!BackingDictionary.TryAdd(key, value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OnModified?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public bool TryGetValue(TKey key, out TValue? value) => BackingDictionary.TryGetValue(key, out value);
|
||||
}
|
||||
@@ -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,13 +24,13 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Helpers;
|
||||
@@ -47,7 +47,8 @@ using JetBrains.Annotations;
|
||||
using SteamKit2;
|
||||
using SteamKit2.Discovery;
|
||||
|
||||
namespace ArchiSteamFarm.Core {
|
||||
namespace ArchiSteamFarm.Core;
|
||||
|
||||
public static class ASF {
|
||||
// This is based on internal Valve guidelines, we're not using it as a hard limit
|
||||
private const byte MaximumRecommendedBotsCount = 10;
|
||||
@@ -67,6 +68,8 @@ namespace ArchiSteamFarm.Core {
|
||||
[PublicAPI]
|
||||
public static WebBrowser? WebBrowser { get; private set; }
|
||||
|
||||
internal static readonly SemaphoreSlim OpenConnectionsSemaphore = new(WebBrowser.MaxConnections, WebBrowser.MaxConnections);
|
||||
|
||||
internal static ICrossProcessSemaphore? ConfirmationsSemaphore { get; private set; }
|
||||
internal static ICrossProcessSemaphore? GiftsSemaphore { get; private set; }
|
||||
internal static ICrossProcessSemaphore? InventorySemaphore { get; private set; }
|
||||
@@ -75,6 +78,7 @@ namespace ArchiSteamFarm.Core {
|
||||
internal static ICrossProcessSemaphore? RateLimitingSemaphore { get; private set; }
|
||||
internal static ImmutableDictionary<Uri, (ICrossProcessSemaphore RateLimitingSemaphore, SemaphoreSlim OpenConnectionsSemaphore)>? WebLimitingSemaphores { get; private set; }
|
||||
|
||||
private static readonly ImmutableHashSet<string> AssembliesNeededBeforeUpdate = ImmutableHashSet.Create("System.IO.Pipes");
|
||||
private static readonly SemaphoreSlim UpdateSemaphore = new(1, 1);
|
||||
|
||||
private static Timer? AutoUpdatesTimer;
|
||||
@@ -83,7 +87,7 @@ namespace ArchiSteamFarm.Core {
|
||||
|
||||
[PublicAPI]
|
||||
public static bool IsOwner(ulong steamID) {
|
||||
if (steamID == 0) {
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
throw new ArgumentOutOfRangeException(nameof(steamID));
|
||||
}
|
||||
|
||||
@@ -91,7 +95,7 @@ namespace ArchiSteamFarm.Core {
|
||||
}
|
||||
|
||||
internal static string GetFilePath(EFileType fileType) {
|
||||
if (!Enum.IsDefined(typeof(EFileType), fileType)) {
|
||||
if (!Enum.IsDefined(fileType)) {
|
||||
throw new InvalidEnumArgumentException(nameof(fileType), (int) fileType, typeof(EFileType));
|
||||
}
|
||||
|
||||
@@ -119,8 +123,9 @@ namespace ArchiSteamFarm.Core {
|
||||
await InitRateLimiters().ConfigureAwait(false);
|
||||
|
||||
StringComparer botsComparer = await PluginsCore.GetBotsComparer().ConfigureAwait(false);
|
||||
IMachineInfoProvider? customMachineInfoProvider = await PluginsCore.GetCustomMachineInfoProvider().ConfigureAwait(false);
|
||||
|
||||
InitBotsComparer(botsComparer);
|
||||
Bot.Init(botsComparer, customMachineInfoProvider);
|
||||
|
||||
if (!Program.Service && !GlobalConfig.Headless && !Console.IsInputRedirected) {
|
||||
Logging.StartInteractiveConsole();
|
||||
@@ -257,11 +262,26 @@ namespace ArchiSteamFarm.Core {
|
||||
}
|
||||
|
||||
if (binaryAsset.DownloadURL == null) {
|
||||
ArchiLogger.LogNullError(nameof(binaryAsset.DownloadURL));
|
||||
ArchiLogger.LogNullError(binaryAsset.DownloadURL);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.FetchingChecksumFromRemoteServer);
|
||||
|
||||
string? remoteChecksum = await ArchiNet.FetchBuildChecksum(newVersion, SharedInfo.BuildInfo.Variant).ConfigureAwait(false);
|
||||
|
||||
switch (remoteChecksum) {
|
||||
case null:
|
||||
// Timeout or error, refuse to update as a security measure
|
||||
return null;
|
||||
case "":
|
||||
// Unknown checksum, release too new or actual malicious build published, no need to scare the user as it's 99.99% the first
|
||||
ArchiLogger.LogGenericWarning(Strings.ChecksumMissing);
|
||||
|
||||
return SharedInfo.Version;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(releaseResponse.ChangelogPlainText)) {
|
||||
ArchiLogger.LogGenericInfo(releaseResponse.ChangelogPlainText!);
|
||||
}
|
||||
@@ -280,19 +300,39 @@ namespace ArchiSteamFarm.Core {
|
||||
progressReporter.ProgressChanged -= OnProgressChanged;
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
if (response?.Content == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
ArchiLogger.LogGenericInfo(Strings.VerifyingChecksumWithRemoteServer);
|
||||
|
||||
byte[] responseBytes = response.Content as byte[] ?? response.Content.ToArray();
|
||||
|
||||
string checksum = Convert.ToHexString(SHA512.HashData(responseBytes));
|
||||
|
||||
if (!checksum.Equals(remoteChecksum, StringComparison.OrdinalIgnoreCase)) {
|
||||
ArchiLogger.LogGenericError(Strings.ChecksumWrong);
|
||||
|
||||
return SharedInfo.Version;
|
||||
}
|
||||
|
||||
await PluginsCore.OnUpdateProceeding(newVersion).ConfigureAwait(false);
|
||||
|
||||
bool kestrelWasRunning = ArchiKestrel.IsRunning;
|
||||
|
||||
if (kestrelWasRunning) {
|
||||
// We disable ArchiKestrel here as the update process moves the core files and might result in IPC crash
|
||||
// TODO: It might fail if the update was triggered from the API, this should be something to improve in the future, by changing the structure into request -> return response -> finish update
|
||||
try {
|
||||
await ArchiKestrel.Stop().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
MemoryStream ms = new(response.Content as byte[] ?? response.Content.ToArray());
|
||||
ArchiLogger.LogGenericInfo(Strings.PatchingFiles);
|
||||
|
||||
MemoryStream ms = new(responseBytes);
|
||||
|
||||
try {
|
||||
await using (ms.ConfigureAwait(false)) {
|
||||
@@ -305,21 +345,23 @@ namespace ArchiSteamFarm.Core {
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericException(e);
|
||||
|
||||
if (kestrelWasRunning) {
|
||||
// We've temporarily disabled ArchiKestrel but the update has failed, let's bring it back up
|
||||
// We can't even be sure if it's possible to bring it back up in this state, but it's worth trying anyway
|
||||
try {
|
||||
await ArchiKestrel.Start().ConfigureAwait(false);
|
||||
} catch (Exception ex) {
|
||||
ArchiLogger.LogGenericWarningException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#if TARGET_GENERIC || !TARGET_WINDOWS
|
||||
if (OperatingSystem.IsFreeBSD() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) {
|
||||
string executable = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.AssemblyName);
|
||||
|
||||
if (File.Exists(executable)) {
|
||||
OS.UnixSetFileAccess(executable, OS.EUnixPermission.Combined755);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateFinished);
|
||||
|
||||
await PluginsCore.OnUpdateFinished(newVersion).ConfigureAwait(false);
|
||||
|
||||
return newVersion;
|
||||
} finally {
|
||||
UpdateSemaphore.Release();
|
||||
@@ -346,16 +388,11 @@ namespace ArchiSteamFarm.Core {
|
||||
return LastWriteEvents.TryGetValue(filePath, out object? savedWriteEvent) && (currentWriteEvent == savedWriteEvent) && LastWriteEvents.TryRemove(filePath, out _);
|
||||
}
|
||||
|
||||
private static void InitBotsComparer(StringComparer botsComparer) {
|
||||
if (botsComparer == null) {
|
||||
throw new ArgumentNullException(nameof(botsComparer));
|
||||
}
|
||||
private static HashSet<string> GetLoadedAssembliesNames() {
|
||||
Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
if (Bot.Bots != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bot.Init(botsComparer);
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
return loadedAssemblies.Select(static loadedAssembly => loadedAssembly.FullName).Where(static name => !string.IsNullOrEmpty(name)).ToHashSet(StringComparer.Ordinal)!;
|
||||
}
|
||||
|
||||
private static void InitConfigWatchEvents() {
|
||||
@@ -380,70 +417,73 @@ namespace ArchiSteamFarm.Core {
|
||||
}
|
||||
|
||||
private static async Task InitRateLimiters() {
|
||||
if (GlobalConfig == null) {
|
||||
throw new InvalidOperationException(nameof(GlobalConfig));
|
||||
}
|
||||
|
||||
// The only purpose of using hashingAlgorithm below 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
|
||||
string networkGroupText = "";
|
||||
|
||||
if (!string.IsNullOrEmpty(Program.NetworkGroup)) {
|
||||
using SHA256 hashingAlgorithm = SHA256.Create();
|
||||
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
networkGroupText = $"-{BitConverter.ToString(hashingAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(Program.NetworkGroup!))).Replace("-", "", StringComparison.Ordinal)}";
|
||||
} else if (!string.IsNullOrEmpty(GlobalConfig.WebProxyText)) {
|
||||
using SHA256 hashingAlgorithm = SHA256.Create();
|
||||
|
||||
networkGroupText = $"-{BitConverter.ToString(hashingAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(GlobalConfig.WebProxyText!))).Replace("-", "", StringComparison.Ordinal)}";
|
||||
}
|
||||
|
||||
ConfirmationsSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(ConfirmationsSemaphore) + networkGroupText).ConfigureAwait(false);
|
||||
GiftsSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(GiftsSemaphore) + networkGroupText).ConfigureAwait(false);
|
||||
InventorySemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(InventorySemaphore) + networkGroupText).ConfigureAwait(false);
|
||||
LoginRateLimitingSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(LoginRateLimitingSemaphore) + networkGroupText).ConfigureAwait(false);
|
||||
LoginSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(LoginSemaphore) + networkGroupText).ConfigureAwait(false);
|
||||
RateLimitingSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(RateLimitingSemaphore) + networkGroupText).ConfigureAwait(false);
|
||||
ConfirmationsSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(ConfirmationsSemaphore)).ConfigureAwait(false);
|
||||
GiftsSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(GiftsSemaphore)).ConfigureAwait(false);
|
||||
InventorySemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(InventorySemaphore)).ConfigureAwait(false);
|
||||
LoginRateLimitingSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(LoginRateLimitingSemaphore)).ConfigureAwait(false);
|
||||
LoginSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(LoginSemaphore)).ConfigureAwait(false);
|
||||
RateLimitingSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(RateLimitingSemaphore)).ConfigureAwait(false);
|
||||
|
||||
WebLimitingSemaphores ??= new Dictionary<Uri, (ICrossProcessSemaphore RateLimitingSemaphore, SemaphoreSlim OpenConnectionsSemaphore)>(4) {
|
||||
{ ArchiWebHandler.SteamCommunityURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}{networkGroupText}-{nameof(ArchiWebHandler.SteamCommunityURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ ArchiWebHandler.SteamHelpURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}{networkGroupText}-{nameof(ArchiWebHandler.SteamHelpURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ ArchiWebHandler.SteamStoreURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}{networkGroupText}-{nameof(ArchiWebHandler.SteamStoreURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ WebAPI.DefaultBaseAddress, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}{networkGroupText}-{nameof(WebAPI)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }
|
||||
{ ArchiWebHandler.SteamCommunityURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(ArchiWebHandler.SteamCommunityURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ ArchiWebHandler.SteamHelpURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(ArchiWebHandler.SteamHelpURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ ArchiWebHandler.SteamStoreURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(ArchiWebHandler.SteamStoreURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ WebAPI.DefaultBaseAddress, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(WebAPI)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }
|
||||
}.ToImmutableDictionary();
|
||||
}
|
||||
|
||||
private static void LoadAssembliesRecursively(Assembly assembly, HashSet<string>? loadedAssembliesNames = null) {
|
||||
if (assembly == null) {
|
||||
throw new ArgumentNullException(nameof(assembly));
|
||||
private static void LoadAllAssemblies() {
|
||||
HashSet<string> loadedAssembliesNames = GetLoadedAssembliesNames();
|
||||
|
||||
LoadAssembliesRecursively(Assembly.GetExecutingAssembly(), loadedAssembliesNames);
|
||||
}
|
||||
|
||||
if (loadedAssembliesNames == null) {
|
||||
Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
private static void LoadAssembliesNeededBeforeUpdate() {
|
||||
HashSet<string> loadedAssembliesNames = GetLoadedAssembliesNames();
|
||||
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
loadedAssembliesNames = loadedAssemblies.Select(static loadedAssembly => loadedAssembly.FullName).Where(static name => !string.IsNullOrEmpty(name)).ToHashSet()!;
|
||||
foreach (string assemblyName in AssembliesNeededBeforeUpdate.Where(loadedAssembliesNames.Add)) {
|
||||
Assembly assembly;
|
||||
|
||||
try {
|
||||
assembly = Assembly.Load(assemblyName);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (AssemblyName assemblyName in assembly.GetReferencedAssemblies().Where(assemblyName => !loadedAssembliesNames.Contains(assemblyName.FullName))) {
|
||||
loadedAssembliesNames.Add(assemblyName.FullName);
|
||||
LoadAssembliesRecursively(assembly, loadedAssembliesNames);
|
||||
}
|
||||
}
|
||||
|
||||
LoadAssembliesRecursively(Assembly.Load(assemblyName), loadedAssembliesNames);
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
private static void LoadAssembliesRecursively(Assembly assembly, ISet<string> loadedAssembliesNames) {
|
||||
ArgumentNullException.ThrowIfNull(assembly);
|
||||
|
||||
if ((loadedAssembliesNames == null) || (loadedAssembliesNames.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(loadedAssembliesNames));
|
||||
}
|
||||
|
||||
foreach (AssemblyName assemblyName in assembly.GetReferencedAssemblies().Where(assemblyName => loadedAssembliesNames.Add(assemblyName.FullName))) {
|
||||
Assembly loadedAssembly;
|
||||
|
||||
try {
|
||||
loadedAssembly = Assembly.Load(assemblyName);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
LoadAssembliesRecursively(loadedAssembly, loadedAssembliesNames);
|
||||
}
|
||||
}
|
||||
|
||||
private static async void OnAutoUpdatesTimer(object? state = null) => await UpdateAndRestart().ConfigureAwait(false);
|
||||
|
||||
private static async void OnChanged(object sender, FileSystemEventArgs e) {
|
||||
if (sender == null) {
|
||||
throw new ArgumentNullException(nameof(sender));
|
||||
}
|
||||
|
||||
if (e == null) {
|
||||
throw new ArgumentNullException(nameof(e));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(sender);
|
||||
ArgumentNullException.ThrowIfNull(e);
|
||||
|
||||
if (string.IsNullOrEmpty(e.Name)) {
|
||||
throw new InvalidOperationException(nameof(e.Name));
|
||||
@@ -523,13 +563,8 @@ namespace ArchiSteamFarm.Core {
|
||||
}
|
||||
|
||||
private static async void OnCreated(object sender, FileSystemEventArgs e) {
|
||||
if (sender == null) {
|
||||
throw new ArgumentNullException(nameof(sender));
|
||||
}
|
||||
|
||||
if (e == null) {
|
||||
throw new ArgumentNullException(nameof(e));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(sender);
|
||||
ArgumentNullException.ThrowIfNull(e);
|
||||
|
||||
if (string.IsNullOrEmpty(e.Name)) {
|
||||
throw new InvalidOperationException(nameof(e.Name));
|
||||
@@ -665,13 +700,8 @@ namespace ArchiSteamFarm.Core {
|
||||
}
|
||||
|
||||
private static async void OnDeleted(object sender, FileSystemEventArgs e) {
|
||||
if (sender == null) {
|
||||
throw new ArgumentNullException(nameof(sender));
|
||||
}
|
||||
|
||||
if (e == null) {
|
||||
throw new ArgumentNullException(nameof(e));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(sender);
|
||||
ArgumentNullException.ThrowIfNull(e);
|
||||
|
||||
if (string.IsNullOrEmpty(e.Name)) {
|
||||
throw new InvalidOperationException(nameof(e.Name));
|
||||
@@ -789,13 +819,8 @@ namespace ArchiSteamFarm.Core {
|
||||
}
|
||||
|
||||
private static async void OnRenamed(object sender, RenamedEventArgs e) {
|
||||
if (sender == null) {
|
||||
throw new ArgumentNullException(nameof(sender));
|
||||
}
|
||||
|
||||
if (e == null) {
|
||||
throw new ArgumentNullException(nameof(e));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(sender);
|
||||
ArgumentNullException.ThrowIfNull(e);
|
||||
|
||||
if (string.IsNullOrEmpty(e.OldName)) {
|
||||
throw new InvalidOperationException(nameof(e.OldName));
|
||||
@@ -819,7 +844,7 @@ namespace ArchiSteamFarm.Core {
|
||||
|
||||
private static async Task RegisterBots() {
|
||||
if ((GlobalConfig == null) || (GlobalDatabase == null) || (WebBrowser == null)) {
|
||||
throw new ArgumentNullException($"{nameof(GlobalConfig)} || {nameof(GlobalDatabase)} || {nameof(WebBrowser)}");
|
||||
throw new InvalidOperationException($"{nameof(GlobalConfig)} || {nameof(GlobalDatabase)} || {nameof(WebBrowser)}");
|
||||
}
|
||||
|
||||
// Ensure that we ask for a list of servers if we don't have any saved servers available
|
||||
@@ -866,7 +891,7 @@ namespace ArchiSteamFarm.Core {
|
||||
|
||||
private static async Task UpdateAndRestart() {
|
||||
if (GlobalConfig == null) {
|
||||
throw new ArgumentNullException(nameof(GlobalConfig));
|
||||
throw new InvalidOperationException(nameof(GlobalConfig));
|
||||
}
|
||||
|
||||
if (!SharedInfo.BuildInfo.CanUpdate || (GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.None)) {
|
||||
@@ -905,9 +930,7 @@ namespace ArchiSteamFarm.Core {
|
||||
}
|
||||
|
||||
private static bool UpdateFromArchive(ZipArchive archive, string targetDirectory) {
|
||||
if (archive == null) {
|
||||
throw new ArgumentNullException(nameof(archive));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(archive);
|
||||
|
||||
if (string.IsNullOrEmpty(targetDirectory)) {
|
||||
throw new ArgumentNullException(nameof(targetDirectory));
|
||||
@@ -916,7 +939,12 @@ namespace ArchiSteamFarm.Core {
|
||||
if (SharedInfo.HomeDirectory == AppContext.BaseDirectory) {
|
||||
// We're running a build that includes our dependencies in ASF's home
|
||||
// Before actually moving files in update procedure, let's minimize the risk of some assembly not being loaded that we may need in the process
|
||||
LoadAssembliesRecursively(Assembly.GetExecutingAssembly());
|
||||
LoadAllAssemblies();
|
||||
} else {
|
||||
// This is a tricky one, for some reason we might need to preload some selected assemblies even in OS-specific builds that normally should be self-contained...
|
||||
// It's as if the executable file was directly mapped to memory and moving it out of the original path caused the whole thing to crash
|
||||
// TODO: This is a total hack, I wish we could get to the bottom of this hole and find out what is really going on there in regards to the above
|
||||
LoadAssembliesNeededBeforeUpdate();
|
||||
}
|
||||
|
||||
// Firstly we'll move all our existing files to a backup directory
|
||||
@@ -926,7 +954,7 @@ namespace ArchiSteamFarm.Core {
|
||||
string fileName = Path.GetFileName(file);
|
||||
|
||||
if (string.IsNullOrEmpty(fileName)) {
|
||||
ArchiLogger.LogNullError(nameof(fileName));
|
||||
ArchiLogger.LogNullError(fileName);
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -934,7 +962,7 @@ namespace ArchiSteamFarm.Core {
|
||||
string relativeFilePath = Path.GetRelativePath(targetDirectory, file);
|
||||
|
||||
if (string.IsNullOrEmpty(relativeFilePath)) {
|
||||
ArchiLogger.LogNullError(nameof(relativeFilePath));
|
||||
ArchiLogger.LogNullError(relativeFilePath);
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -943,7 +971,7 @@ namespace ArchiSteamFarm.Core {
|
||||
|
||||
switch (relativeDirectoryName) {
|
||||
case null:
|
||||
ArchiLogger.LogNullError(nameof(relativeDirectoryName));
|
||||
ArchiLogger.LogNullError(relativeDirectoryName);
|
||||
|
||||
return false;
|
||||
case "":
|
||||
@@ -1007,7 +1035,7 @@ namespace ArchiSteamFarm.Core {
|
||||
string? directory = Path.GetDirectoryName(file);
|
||||
|
||||
if (string.IsNullOrEmpty(directory)) {
|
||||
ArchiLogger.LogNullError(nameof(directory));
|
||||
ArchiLogger.LogNullError(directory);
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1045,4 +1073,3 @@ namespace ArchiSteamFarm.Core {
|
||||
Database
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +23,8 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
|
||||
namespace ArchiSteamFarm.Core {
|
||||
namespace ArchiSteamFarm.Core;
|
||||
|
||||
internal static class AprilFools {
|
||||
private static readonly object LockObject = new();
|
||||
|
||||
@@ -74,4 +75,3 @@ namespace ArchiSteamFarm.Core {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
269
ArchiSteamFarm/Core/ArchiNet.cs
Normal file
269
ArchiSteamFarm/Core/ArchiNet.cs
Normal file
@@ -0,0 +1,269 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// |
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.Steam;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using ArchiSteamFarm.Steam.Storage;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using ArchiSteamFarm.Web;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.Core;
|
||||
|
||||
internal static class ArchiNet {
|
||||
private static Uri URL => new("https://asf.JustArchi.net");
|
||||
|
||||
internal static async Task<HttpStatusCode?> AnnounceForListing(Bot bot, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, string tradeToken, string? nickname = null, string? avatarHash = null) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
if ((inventory == null) || (inventory.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(inventory));
|
||||
}
|
||||
|
||||
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(tradeToken)) {
|
||||
throw new ArgumentNullException(nameof(tradeToken));
|
||||
}
|
||||
|
||||
if (tradeToken.Length != BotConfig.SteamTradeTokenLength) {
|
||||
throw new ArgumentOutOfRangeException(nameof(tradeToken));
|
||||
}
|
||||
|
||||
Uri request = new(URL, "/Api/Announce");
|
||||
|
||||
Dictionary<string, string> data = new(10, StringComparer.Ordinal) {
|
||||
{ "AvatarHash", avatarHash ?? "" },
|
||||
{ "GamesCount", inventory.Select(static item => item.RealAppID).Distinct().Count().ToString(CultureInfo.InvariantCulture) },
|
||||
{ "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") },
|
||||
{ "ItemsCount", inventory.Count.ToString(CultureInfo.InvariantCulture) },
|
||||
{ "MatchableTypes", JsonConvert.SerializeObject(acceptedMatchableTypes) },
|
||||
{ "MatchEverything", bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) ? "1" : "0" },
|
||||
{ "MaxTradeHoldDuration", (ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration).ToString(CultureInfo.InvariantCulture) },
|
||||
{ "Nickname", nickname ?? "" },
|
||||
{ "SteamID", bot.SteamID.ToString(CultureInfo.InvariantCulture) },
|
||||
{ "TradeToken", tradeToken }
|
||||
};
|
||||
|
||||
BasicResponse? response = await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
|
||||
|
||||
return response?.StatusCode;
|
||||
}
|
||||
|
||||
internal static async Task<string?> FetchBuildChecksum(Version version, string variant) {
|
||||
ArgumentNullException.ThrowIfNull(version);
|
||||
|
||||
if (string.IsNullOrEmpty(variant)) {
|
||||
throw new ArgumentNullException(nameof(variant));
|
||||
}
|
||||
|
||||
if (ASF.WebBrowser == null) {
|
||||
throw new InvalidOperationException(nameof(ASF.WebBrowser));
|
||||
}
|
||||
|
||||
Uri request = new(URL, $"/Api/Checksum/{version}/{variant}");
|
||||
|
||||
ObjectResponse<ChecksumResponse>? response = await ASF.WebBrowser.UrlGetToJsonObject<ChecksumResponse>(request).ConfigureAwait(false);
|
||||
|
||||
if (response?.Content == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.Content.Checksum ?? "";
|
||||
}
|
||||
|
||||
internal static async Task<ImmutableHashSet<ListedUser>?> GetListedUsers(Bot bot) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
Uri request = new(URL, "/Api/Bots");
|
||||
|
||||
ObjectResponse<ImmutableHashSet<ListedUser>>? response = await bot.ArchiWebHandler.WebBrowser.UrlGetToJsonObject<ImmutableHashSet<ListedUser>>(request).ConfigureAwait(false);
|
||||
|
||||
return response?.Content;
|
||||
}
|
||||
|
||||
internal static async Task<HttpStatusCode?> HeartBeatForListing(Bot bot) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
Uri request = new(URL, "/Api/HeartBeat");
|
||||
|
||||
Dictionary<string, string> data = new(2, StringComparer.Ordinal) {
|
||||
{ "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") },
|
||||
{ "SteamID", bot.SteamID.ToString(CultureInfo.InvariantCulture) }
|
||||
};
|
||||
|
||||
BasicResponse? response = await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
|
||||
|
||||
return response?.StatusCode;
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
internal sealed class ListedUser {
|
||||
[JsonProperty("items_count", Required = Required.Always)]
|
||||
internal readonly ushort ItemsCount;
|
||||
|
||||
internal readonly HashSet<Asset.EType> MatchableTypes = new();
|
||||
|
||||
[JsonProperty("max_trade_hold_duration", Required = Required.Always)]
|
||||
internal readonly byte MaxTradeHoldDuration;
|
||||
|
||||
[JsonProperty("steam_id", Required = Required.Always)]
|
||||
internal readonly ulong SteamID;
|
||||
|
||||
[JsonProperty("trade_token", Required = Required.Always)]
|
||||
internal readonly string TradeToken = "";
|
||||
|
||||
internal float Score => GamesCount / (float) ItemsCount;
|
||||
|
||||
#pragma warning disable CS0649 // False positive, it's a field set during json deserialization
|
||||
[JsonProperty("games_count", Required = Required.Always)]
|
||||
private readonly ushort GamesCount;
|
||||
#pragma warning restore CS0649 // False positive, it's a field set during json deserialization
|
||||
|
||||
internal bool MatchEverything { get; private set; }
|
||||
|
||||
[JsonProperty("matchable_backgrounds", Required = Required.Always)]
|
||||
private byte MatchableBackgroundsNumber {
|
||||
set {
|
||||
switch (value) {
|
||||
case 0:
|
||||
MatchableTypes.Remove(Asset.EType.ProfileBackground);
|
||||
|
||||
break;
|
||||
case 1:
|
||||
MatchableTypes.Add(Asset.EType.ProfileBackground);
|
||||
|
||||
break;
|
||||
default:
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty("matchable_cards", Required = Required.Always)]
|
||||
private byte MatchableCardsNumber {
|
||||
set {
|
||||
switch (value) {
|
||||
case 0:
|
||||
MatchableTypes.Remove(Asset.EType.TradingCard);
|
||||
|
||||
break;
|
||||
case 1:
|
||||
MatchableTypes.Add(Asset.EType.TradingCard);
|
||||
|
||||
break;
|
||||
default:
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty("matchable_emoticons", Required = Required.Always)]
|
||||
private byte MatchableEmoticonsNumber {
|
||||
set {
|
||||
switch (value) {
|
||||
case 0:
|
||||
MatchableTypes.Remove(Asset.EType.Emoticon);
|
||||
|
||||
break;
|
||||
case 1:
|
||||
MatchableTypes.Add(Asset.EType.Emoticon);
|
||||
|
||||
break;
|
||||
default:
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty("matchable_foil_cards", Required = Required.Always)]
|
||||
private byte MatchableFoilCardsNumber {
|
||||
set {
|
||||
switch (value) {
|
||||
case 0:
|
||||
MatchableTypes.Remove(Asset.EType.FoilTradingCard);
|
||||
|
||||
break;
|
||||
case 1:
|
||||
MatchableTypes.Add(Asset.EType.FoilTradingCard);
|
||||
|
||||
break;
|
||||
default:
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty("match_everything", Required = Required.Always)]
|
||||
private byte MatchEverythingNumber {
|
||||
set {
|
||||
switch (value) {
|
||||
case 0:
|
||||
MatchEverything = false;
|
||||
|
||||
break;
|
||||
case 1:
|
||||
MatchEverything = true;
|
||||
|
||||
break;
|
||||
default:
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
private ListedUser() { }
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
private sealed class ChecksumResponse {
|
||||
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
|
||||
[JsonProperty("checksum", Required = Required.AllowNull)]
|
||||
internal readonly string? Checksum;
|
||||
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
|
||||
|
||||
[JsonConstructor]
|
||||
private ChecksumResponse() { }
|
||||
}
|
||||
}
|
||||
@@ -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,9 +20,11 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.Core {
|
||||
namespace ArchiSteamFarm.Core;
|
||||
|
||||
internal static class Debugging {
|
||||
#if DEBUG
|
||||
internal static bool IsDebugBuild => true;
|
||||
@@ -30,7 +32,7 @@ namespace ArchiSteamFarm.Core {
|
||||
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;
|
||||
|
||||
@@ -44,4 +46,3 @@ namespace ArchiSteamFarm.Core {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +24,8 @@ using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.Steam;
|
||||
|
||||
namespace ArchiSteamFarm.Core {
|
||||
namespace ArchiSteamFarm.Core;
|
||||
|
||||
internal static class Events {
|
||||
internal static async Task OnBotShutdown() {
|
||||
if (Program.ProcessRequired || ((Bot.Bots != null) && Bot.Bots.Values.Any(static bot => bot.KeepRunning))) {
|
||||
@@ -43,4 +44,3 @@ namespace ArchiSteamFarm.Core {
|
||||
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");
|
||||
@@ -20,6 +20,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@@ -35,10 +36,11 @@ using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using ArchiSteamFarm.Web;
|
||||
|
||||
namespace ArchiSteamFarm.Core {
|
||||
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));
|
||||
internal static readonly string ProcessFileName = Environment.ProcessPath ?? throw new InvalidOperationException(nameof(ProcessFileName));
|
||||
|
||||
internal static DateTime ProcessStartTime {
|
||||
#if NETFRAMEWORK
|
||||
@@ -90,7 +92,6 @@ namespace ArchiSteamFarm.Core {
|
||||
private static string? BackingVersion;
|
||||
private static Mutex? SingleInstance;
|
||||
|
||||
#if TARGET_GENERIC || TARGET_WINDOWS
|
||||
internal static void CoreInit(bool systemRequired) {
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
if (systemRequired) {
|
||||
@@ -111,9 +112,6 @@ namespace ArchiSteamFarm.Core {
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
internal static void CoreInit(bool _) { }
|
||||
#endif
|
||||
|
||||
internal static string GetOsResourceName(string objectName) {
|
||||
if (string.IsNullOrEmpty(objectName)) {
|
||||
@@ -124,8 +122,8 @@ namespace ArchiSteamFarm.Core {
|
||||
}
|
||||
|
||||
internal static void Init(GlobalConfig.EOptimizationMode optimizationMode) {
|
||||
if (!Enum.IsDefined(typeof(GlobalConfig.EOptimizationMode), optimizationMode)) {
|
||||
throw new ArgumentNullException(nameof(optimizationMode));
|
||||
if (!Enum.IsDefined(optimizationMode)) {
|
||||
throw new InvalidEnumArgumentException(nameof(optimizationMode), (int) optimizationMode, typeof(GlobalConfig.EOptimizationMode));
|
||||
}
|
||||
|
||||
switch (optimizationMode) {
|
||||
@@ -143,19 +141,15 @@ namespace ArchiSteamFarm.Core {
|
||||
}
|
||||
|
||||
internal static bool IsRunningAsRoot() {
|
||||
#if TARGET_GENERIC || TARGET_WINDOWS
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
using WindowsIdentity identity = WindowsIdentity.GetCurrent();
|
||||
|
||||
return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
return identity.IsSystem || new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if TARGET_GENERIC || !TARGET_WINDOWS
|
||||
if (OperatingSystem.IsFreeBSD() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) {
|
||||
return NativeMethods.GetEUID() == 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
// We can't determine whether user is running as root or not, so fallback to that not happening
|
||||
return false;
|
||||
@@ -166,14 +160,10 @@ namespace ArchiSteamFarm.Core {
|
||||
return false;
|
||||
}
|
||||
|
||||
string uniqueName;
|
||||
|
||||
// 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
|
||||
// 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 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)}";
|
||||
}
|
||||
// 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;
|
||||
|
||||
@@ -201,7 +191,6 @@ namespace ArchiSteamFarm.Core {
|
||||
return true;
|
||||
}
|
||||
|
||||
#if TARGET_GENERIC || !TARGET_WINDOWS
|
||||
[SupportedOSPlatform("FreeBSD")]
|
||||
[SupportedOSPlatform("Linux")]
|
||||
[SupportedOSPlatform("MacOS")]
|
||||
@@ -225,7 +214,6 @@ namespace ArchiSteamFarm.Core {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, Marshal.GetLastWin32Error()));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
internal static void UnregisterProcess() {
|
||||
if (SingleInstance == null) {
|
||||
@@ -239,19 +227,24 @@ namespace ArchiSteamFarm.Core {
|
||||
}
|
||||
|
||||
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 (OperatingSystem.IsWindows() || !RuntimeMadness.IsRunningOnMono) {
|
||||
if (SharedInfo.BuildInfo.Variant.EndsWith("-netf", StringComparison.Ordinal)) {
|
||||
#if NETFRAMEWORK
|
||||
// All Windows variants (7+) have valid .NET Core build
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Non-Windows variants of generic-netf are supported only in Mono
|
||||
if (!RuntimeMadness.IsRunningOnMono) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Platforms not supported by .NET Core
|
||||
return RuntimeInformation.OSArchitecture switch {
|
||||
// Sadly we can't tell a difference between ARMv6 and ARMv7 reliably, we'll believe that this linux-arm user knows what he's doing and he's indeed in need of generic-netf on ARMv6
|
||||
Architecture.Arm => true,
|
||||
@@ -264,12 +257,39 @@ 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
|
||||
}
|
||||
|
||||
#if TARGET_GENERIC || TARGET_WINDOWS
|
||||
if (SharedInfo.BuildInfo.Variant == "generic") {
|
||||
// Generic is supported everywhere
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((SharedInfo.BuildInfo.Variant == "docker") || SharedInfo.BuildInfo.Variant.StartsWith("linux-", StringComparison.Ordinal)) {
|
||||
// OS-specific Linux and Docker builds are supported only on Linux
|
||||
return OperatingSystem.IsLinux();
|
||||
}
|
||||
|
||||
if (SharedInfo.BuildInfo.Variant.StartsWith("osx-", StringComparison.Ordinal)) {
|
||||
// OS-specific macOS build is supported only on macOS
|
||||
return OperatingSystem.IsMacOS();
|
||||
}
|
||||
|
||||
if (SharedInfo.BuildInfo.Variant.StartsWith("win-", StringComparison.Ordinal)) {
|
||||
// OS-specific Windows build is supported only on Windows
|
||||
return OperatingSystem.IsWindows();
|
||||
}
|
||||
|
||||
// Unknown combination, we intend to cover all of the available ones above, so this results in an error
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(SharedInfo.BuildInfo.Variant), SharedInfo.BuildInfo.Variant));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
private static void WindowsDisableQuickEditMode() {
|
||||
if (!OperatingSystem.IsWindows()) {
|
||||
@@ -307,9 +327,7 @@ namespace ArchiSteamFarm.Core {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if TARGET_GENERIC || !TARGET_WINDOWS
|
||||
[Flags]
|
||||
[SupportedOSPlatform("FreeBSD")]
|
||||
[SupportedOSPlatform("Linux")]
|
||||
@@ -324,13 +342,10 @@ namespace ArchiSteamFarm.Core {
|
||||
UserExecute = 0x40,
|
||||
UserWrite = 0x80,
|
||||
UserRead = 0x100,
|
||||
Combined755 = UserRead | UserWrite | UserExecute | GroupRead | GroupExecute | OtherRead | OtherExecute,
|
||||
Combined777 = UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute
|
||||
}
|
||||
#endif
|
||||
|
||||
private static class NativeMethods {
|
||||
#if TARGET_GENERIC || TARGET_WINDOWS
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal const EExecutionState AwakeExecutionState = EExecutionState.SystemRequired | EExecutionState.AwayModeRequired | EExecutionState.Continuous;
|
||||
|
||||
@@ -339,9 +354,7 @@ namespace ArchiSteamFarm.Core {
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal const sbyte StandardInputHandle = -10;
|
||||
#endif
|
||||
|
||||
#if TARGET_GENERIC || !TARGET_WINDOWS
|
||||
#pragma warning disable CA2101 // False positive, we can't use unicode charset on Unix, and it uses UTF-8 by default anyway
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[DllImport("libc", EntryPoint = "chmod", SetLastError = true)]
|
||||
@@ -350,25 +363,19 @@ namespace ArchiSteamFarm.Core {
|
||||
[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
|
||||
#endif
|
||||
|
||||
#if TARGET_GENERIC || TARGET_WINDOWS
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[DllImport("kernel32.dll")]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
|
||||
#endif
|
||||
|
||||
#if TARGET_GENERIC || !TARGET_WINDOWS
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[DllImport("libc", EntryPoint = "geteuid", SetLastError = true)]
|
||||
[SupportedOSPlatform("FreeBSD")]
|
||||
[SupportedOSPlatform("Linux")]
|
||||
[SupportedOSPlatform("MacOS")]
|
||||
internal static extern uint GetEUID();
|
||||
#endif
|
||||
|
||||
#if TARGET_GENERIC || TARGET_WINDOWS
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[DllImport("kernel32.dll")]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
@@ -392,7 +399,5 @@ namespace ArchiSteamFarm.Core {
|
||||
AwayModeRequired = 0x00000040,
|
||||
Continuous = 0x80000000
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
707
ArchiSteamFarm/Core/RemoteCommunication.cs
Normal file
707
ArchiSteamFarm/Core/RemoteCommunication.cs
Normal file
@@ -0,0 +1,707 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// |
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.Steam;
|
||||
using ArchiSteamFarm.Steam.Cards;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using ArchiSteamFarm.Steam.Exchange;
|
||||
using ArchiSteamFarm.Steam.Integration;
|
||||
using ArchiSteamFarm.Steam.Security;
|
||||
using ArchiSteamFarm.Steam.Storage;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using ArchiSteamFarm.Web;
|
||||
|
||||
namespace ArchiSteamFarm.Core;
|
||||
|
||||
internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
private const ushort MaxItemsForFairBots = ArchiWebHandler.MaxItemsInSingleInventoryRequest * WebBrowser.MaxTries; // Determines which fair bots we'll deprioritize when matching due to excessive number of inventory requests they need to make, which are likely to fail in the process or cause excessive delays
|
||||
private const byte MaxMatchedBotsHard = 40; // Determines how many bots we can attempt to match in total, where match attempt is equal to analyzing bot's inventory
|
||||
private const byte MaxMatchingRounds = 10; // Determines maximum amount of matching rounds we're going to consider before leaving the rest of work for the next batch
|
||||
private const byte MinAnnouncementCheckTTL = 6; // Minimum amount of hours we must wait before checking eligibility for Announcement, should be lower than MinPersonaStateTTL
|
||||
private const byte MinHeartBeatTTL = 10; // Minimum amount of minutes we must wait before sending next HeartBeat
|
||||
private const byte MinItemsCount = 100; // Minimum amount of items to be eligible for public listing
|
||||
private const byte MinPersonaStateTTL = 8; // Minimum amount of hours we must wait before requesting persona state update
|
||||
|
||||
private static readonly ImmutableHashSet<Asset.EType> AcceptedMatchableTypes = ImmutableHashSet.Create(
|
||||
Asset.EType.Emoticon,
|
||||
Asset.EType.FoilTradingCard,
|
||||
Asset.EType.ProfileBackground,
|
||||
Asset.EType.TradingCard
|
||||
);
|
||||
|
||||
private readonly Bot Bot;
|
||||
private readonly SemaphoreSlim MatchActivelySemaphore = new(1, 1);
|
||||
private readonly Timer? MatchActivelyTimer;
|
||||
private readonly SemaphoreSlim RequestsSemaphore = new(1, 1);
|
||||
|
||||
private DateTime LastAnnouncementCheck;
|
||||
private DateTime LastHeartBeat;
|
||||
private DateTime LastPersonaStateRequest;
|
||||
private bool ShouldSendHeartBeats;
|
||||
|
||||
internal RemoteCommunication(Bot bot) {
|
||||
Bot = bot ?? throw new ArgumentNullException(nameof(bot));
|
||||
|
||||
if (Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively)) {
|
||||
MatchActivelyTimer = new Timer(
|
||||
MatchActively,
|
||||
null,
|
||||
TimeSpan.FromHours(1) + TimeSpan.FromSeconds(ASF.LoadBalancingDelay * Bot.Bots?.Count ?? 0), // Delay
|
||||
TimeSpan.FromHours(8) // Period
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
// Those are objects that are always being created if constructor doesn't throw exception
|
||||
MatchActivelySemaphore.Dispose();
|
||||
RequestsSemaphore.Dispose();
|
||||
|
||||
// Those are objects that might be null and the check should be in-place
|
||||
MatchActivelyTimer?.Dispose();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync() {
|
||||
// Those are objects that are always being created if constructor doesn't throw exception
|
||||
MatchActivelySemaphore.Dispose();
|
||||
RequestsSemaphore.Dispose();
|
||||
|
||||
// Those are objects that might be null and the check should be in-place
|
||||
if (MatchActivelyTimer != null) {
|
||||
await MatchActivelyTimer.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task OnHeartBeat() {
|
||||
if (!Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.PublicListing)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Request persona update if needed
|
||||
if ((DateTime.UtcNow > LastPersonaStateRequest.AddHours(MinPersonaStateTTL)) && (DateTime.UtcNow > LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL))) {
|
||||
LastPersonaStateRequest = DateTime.UtcNow;
|
||||
Bot.RequestPersonaStateUpdate();
|
||||
}
|
||||
|
||||
if (!ShouldSendHeartBeats || (DateTime.UtcNow < LastHeartBeat.AddMinutes(MinHeartBeatTTL))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await RequestsSemaphore.WaitAsync(0).ConfigureAwait(false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
HttpStatusCode? response = await ArchiNet.HeartBeatForListing(Bot).ConfigureAwait(false);
|
||||
|
||||
if (!response.HasValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.Value.IsClientErrorCode()) {
|
||||
LastHeartBeat = DateTime.MinValue;
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LastHeartBeat = DateTime.UtcNow;
|
||||
} finally {
|
||||
RequestsSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task OnLoggedOn() {
|
||||
if (!Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.SteamGroup)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await Bot.ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID).ConfigureAwait(false)) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ArchiWebHandler.JoinGroup)));
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task OnPersonaState(string? nickname = null, string? avatarHash = null) {
|
||||
if (!Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.PublicListing)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((DateTime.UtcNow < LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL)) && (ShouldSendHeartBeats || (LastHeartBeat == DateTime.MinValue))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await RequestsSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
if ((DateTime.UtcNow < LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL)) && (ShouldSendHeartBeats || (LastHeartBeat == DateTime.MinValue))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't announce if we don't meet conditions
|
||||
bool? eligible = await IsEligibleForListing().ConfigureAwait(false);
|
||||
|
||||
if (!eligible.HasValue) {
|
||||
// This is actually network failure, so we'll stop sending heartbeats but not record it as valid check
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eligible.Value) {
|
||||
LastAnnouncementCheck = DateTime.UtcNow;
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
string? tradeToken = await Bot.ArchiHandler.GetTradeToken().ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(tradeToken)) {
|
||||
// This is actually network failure, so we'll stop sending heartbeats but not record it as valid check
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<Asset.EType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
|
||||
if (acceptedMatchableTypes.Count == 0) {
|
||||
Bot.ArchiLogger.LogNullError(acceptedMatchableTypes);
|
||||
LastAnnouncementCheck = DateTime.UtcNow;
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<Asset> inventory;
|
||||
|
||||
try {
|
||||
inventory = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => item.Tradable && acceptedMatchableTypes.Contains(item.Type)).ToHashSetAsync().ConfigureAwait(false);
|
||||
} catch (HttpRequestException e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
// This is actually inventory failure, so we'll stop sending heartbeats but not record it as valid check
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericException(e);
|
||||
|
||||
// This is actually inventory failure, so we'll stop sending heartbeats but not record it as valid check
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LastAnnouncementCheck = DateTime.UtcNow;
|
||||
|
||||
// This is actual inventory
|
||||
if (inventory.Count < MinItemsCount) {
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
HttpStatusCode? response = await ArchiNet.AnnounceForListing(Bot, inventory, acceptedMatchableTypes, tradeToken!, nickname, avatarHash).ConfigureAwait(false);
|
||||
|
||||
if (!response.HasValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.Value.IsClientErrorCode()) {
|
||||
LastHeartBeat = DateTime.MinValue;
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LastHeartBeat = DateTime.UtcNow;
|
||||
ShouldSendHeartBeats = true;
|
||||
} finally {
|
||||
RequestsSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool?> IsEligibleForListing() {
|
||||
// Bot must be eligible for matching first
|
||||
bool? isEligibleForMatching = await IsEligibleForMatching().ConfigureAwait(false);
|
||||
|
||||
if (isEligibleForMatching != true) {
|
||||
return isEligibleForMatching;
|
||||
}
|
||||
|
||||
// Bot must have STM enabled in TradingPreferences
|
||||
if (!Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.SteamTradeMatcher)) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.BotConfig.TradingPreferences)}: {Bot.BotConfig.TradingPreferences}"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bot must have public inventory
|
||||
bool? hasPublicInventory = await Bot.HasPublicInventory().ConfigureAwait(false);
|
||||
|
||||
if (hasPublicInventory != true) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.HasPublicInventory)}: {hasPublicInventory?.ToString() ?? "null"}"));
|
||||
|
||||
return hasPublicInventory;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool?> IsEligibleForMatching() {
|
||||
// Bot must have ASF 2FA
|
||||
if (!Bot.HasMobileAuthenticator) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.HasMobileAuthenticator)}: {Bot.HasMobileAuthenticator}"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bot must have at least one accepted matchable type set
|
||||
if ((Bot.BotConfig.MatchableTypes.Count == 0) || Bot.BotConfig.MatchableTypes.All(static type => !AcceptedMatchableTypes.Contains(type))) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.BotConfig.MatchableTypes)}: {string.Join(", ", Bot.BotConfig.MatchableTypes)}"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bot must have valid API key (e.g. not being restricted account)
|
||||
bool? hasValidApiKey = await Bot.ArchiWebHandler.HasValidApiKey().ConfigureAwait(false);
|
||||
|
||||
if (hasValidApiKey != true) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.ArchiWebHandler.HasValidApiKey)}: {hasValidApiKey?.ToString() ?? "null"}"));
|
||||
|
||||
return hasValidApiKey;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async void MatchActively(object? state = null) {
|
||||
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) || !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively) || (await IsEligibleForMatching().ConfigureAwait(false) != true)) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<Asset.EType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
|
||||
if (acceptedMatchableTypes.Count == 0) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await MatchActivelySemaphore.WaitAsync(0).ConfigureAwait(false)) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.Starting);
|
||||
|
||||
Dictionary<ulong, (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs)> triedSteamIDs = new();
|
||||
|
||||
bool shouldContinueMatching = true;
|
||||
bool tradedSomething = false;
|
||||
|
||||
for (byte i = 0; (i < MaxMatchingRounds) && shouldContinueMatching; i++) {
|
||||
if ((i > 0) && tradedSomething) {
|
||||
// After each round we wait at least 5 minutes for all bots to react
|
||||
await Task.Delay(5 * 60 * 1000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) || !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively) || (await IsEligibleForMatching().ConfigureAwait(false) != true)) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
#pragma warning disable CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
|
||||
using (await Bot.Actions.GetTradingLock().ConfigureAwait(false)) {
|
||||
#pragma warning restore CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ActivelyMatchingItems, i));
|
||||
(shouldContinueMatching, tradedSomething) = await MatchActivelyRound(acceptedMatchableTypes, triedSteamIDs).ConfigureAwait(false);
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.DoneActivelyMatchingItems, i));
|
||||
}
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.Done);
|
||||
} finally {
|
||||
MatchActivelySemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldContinueMatching, bool TradedSomething)> MatchActivelyRound(IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, IDictionary<ulong, (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs)> triedSteamIDs) {
|
||||
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(triedSteamIDs);
|
||||
|
||||
HashSet<Asset> ourInventory;
|
||||
|
||||
try {
|
||||
ourInventory = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => acceptedMatchableTypes.Contains(item.Type) && !Bot.BotDatabase.MatchActivelyBlacklistAppIDs.Contains(item.RealAppID)).ToHashSetAsync().ConfigureAwait(false);
|
||||
} catch (HttpRequestException e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
return (false, false);
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericException(e);
|
||||
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
if (ourInventory.Count == 0) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(ourInventory)));
|
||||
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
(Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourFullState, Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourTradableState) = Trading.GetDividedInventoryState(ourInventory);
|
||||
|
||||
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
|
||||
// User doesn't have any more dupes in the inventory
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, $"{nameof(ourFullState)} || {nameof(ourTradableState)}"));
|
||||
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
ImmutableHashSet<ArchiNet.ListedUser>? listedUsers = await ArchiNet.GetListedUsers(Bot).ConfigureAwait(false);
|
||||
|
||||
if ((listedUsers == null) || (listedUsers.Count == 0)) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(listedUsers)));
|
||||
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
byte maxTradeHoldDuration = ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration;
|
||||
byte totalMatches = 0;
|
||||
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisRound = new();
|
||||
|
||||
foreach (ArchiNet.ListedUser? listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && (!triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) attempt) || (attempt.Tries < byte.MaxValue)) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) attempt) ? attempt.Tries : 0).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenByDescending(static listedUser => listedUser.MatchEverything || (listedUser.ItemsCount < MaxItemsForFairBots)).ThenByDescending(static listedUser => listedUser.Score)) {
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> wantedSets = ourTradableState.Keys.Where(set => !skippedSetsThisRound.Contains(set) && listedUser.MatchableTypes.Contains(set.Type)).ToHashSet();
|
||||
|
||||
if (wantedSets.Count == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (++totalMatches > MaxMatchedBotsHard) {
|
||||
break;
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericTrace($"{listedUser.SteamID}...");
|
||||
|
||||
byte? tradeHoldDuration = await Bot.ArchiWebHandler.GetCombinedTradeHoldDurationAgainstUser(listedUser.SteamID, listedUser.TradeToken).ConfigureAwait(false);
|
||||
|
||||
switch (tradeHoldDuration) {
|
||||
case null:
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(tradeHoldDuration)));
|
||||
|
||||
continue;
|
||||
case > 0 when (tradeHoldDuration.Value > maxTradeHoldDuration) || (tradeHoldDuration.Value > listedUser.MaxTradeHoldDuration):
|
||||
Bot.ArchiLogger.LogGenericTrace($"{tradeHoldDuration.Value} > {maxTradeHoldDuration} || {listedUser.MaxTradeHoldDuration}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSet<Asset> theirInventory;
|
||||
|
||||
try {
|
||||
theirInventory = await Bot.ArchiWebHandler.GetInventoryAsync(listedUser.SteamID).Where(item => (!listedUser.MatchEverything || item.Tradable) && wantedSets.Contains((item.RealAppID, item.Type, item.Rarity)) && ((tradeHoldDuration.Value == 0) || !(item.Type is Asset.EType.FoilTradingCard or Asset.EType.TradingCard && CardsFarmer.SalesBlacklist.Contains(item.RealAppID)))).ToHashSetAsync().ConfigureAwait(false);
|
||||
} catch (HttpRequestException e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
continue;
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericException(e);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (theirInventory.Count == 0) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(theirInventory)));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisUser = new();
|
||||
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> theirTradableState = Trading.GetTradableInventoryState(theirInventory);
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> inventoryStateChanges = new();
|
||||
|
||||
for (byte i = 0; i < Trading.MaxTradesPerAccount; i++) {
|
||||
byte itemsInTrade = 0;
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisTrade = new();
|
||||
|
||||
Dictionary<ulong, uint> classIDsToGive = new();
|
||||
Dictionary<ulong, uint> classIDsToReceive = new();
|
||||
Dictionary<ulong, uint> fairClassIDsToGive = new();
|
||||
Dictionary<ulong, uint> fairClassIDsToReceive = new();
|
||||
|
||||
foreach (((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) set, Dictionary<ulong, uint> ourFullItems) in ourFullState.Where(set => !skippedSetsThisUser.Contains(set.Key) && listedUser.MatchableTypes.Contains(set.Key.Type) && set.Value.Values.Any(static count => count > 1))) {
|
||||
if (!ourTradableState.TryGetValue(set, out Dictionary<ulong, uint>? ourTradableItems) || (ourTradableItems.Count == 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!theirTradableState.TryGetValue(set, out Dictionary<ulong, uint>? theirTradableItems) || (theirTradableItems.Count == 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Those 2 collections are on user-basis since we can't be sure that the trade passes through (and therefore we need to keep original state in case of failure)
|
||||
Dictionary<ulong, uint> ourFullSet = new(ourFullItems);
|
||||
Dictionary<ulong, uint> ourTradableSet = new(ourTradableItems);
|
||||
|
||||
// We also have to take into account changes that happened in previous trades with this user, so this block will adapt to that
|
||||
if (inventoryStateChanges.TryGetValue(set, out Dictionary<ulong, uint>? pastChanges) && (pastChanges.Count > 0)) {
|
||||
foreach ((ulong classID, uint amount) in pastChanges) {
|
||||
if (!ourFullSet.TryGetValue(classID, out uint fullAmount) || (fullAmount == 0) || (fullAmount < amount)) {
|
||||
Bot.ArchiLogger.LogNullError(fullAmount);
|
||||
|
||||
return (false, skippedSetsThisRound.Count > 0);
|
||||
}
|
||||
|
||||
if (fullAmount > amount) {
|
||||
ourFullSet[classID] = fullAmount - amount;
|
||||
} else {
|
||||
ourFullSet.Remove(classID);
|
||||
}
|
||||
|
||||
if (!ourTradableSet.TryGetValue(classID, out uint tradableAmount) || (tradableAmount == 0) || (tradableAmount < amount)) {
|
||||
Bot.ArchiLogger.LogNullError(tradableAmount);
|
||||
|
||||
return (false, skippedSetsThisRound.Count > 0);
|
||||
}
|
||||
|
||||
if (fullAmount > amount) {
|
||||
ourTradableSet[classID] = fullAmount - amount;
|
||||
} else {
|
||||
ourTradableSet.Remove(classID);
|
||||
}
|
||||
}
|
||||
|
||||
if (Trading.IsEmptyForMatching(ourFullSet, ourTradableSet)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
bool match;
|
||||
|
||||
do {
|
||||
match = false;
|
||||
|
||||
foreach ((ulong ourItem, uint ourFullAmount) in ourFullSet.Where(static item => item.Value > 1).OrderByDescending(static item => item.Value)) {
|
||||
if (!ourTradableSet.TryGetValue(ourItem, out uint ourTradableAmount) || (ourTradableAmount == 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ((ulong theirItem, uint theirTradableAmount) in theirTradableItems.OrderBy(item => ourFullSet.TryGetValue(item.Key, out uint ourAmountOfTheirItem) ? ourAmountOfTheirItem : 0)) {
|
||||
if (ourFullSet.TryGetValue(theirItem, out uint ourAmountOfTheirItem) && (ourFullAmount <= ourAmountOfTheirItem + 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!listedUser.MatchEverything) {
|
||||
// We have a potential match, let's check fairness for them
|
||||
fairClassIDsToGive.TryGetValue(ourItem, out uint fairGivenAmount);
|
||||
fairClassIDsToReceive.TryGetValue(theirItem, out uint fairReceivedAmount);
|
||||
fairClassIDsToGive[ourItem] = ++fairGivenAmount;
|
||||
fairClassIDsToReceive[theirItem] = ++fairReceivedAmount;
|
||||
|
||||
// Filter their inventory for the sets we're trading or have traded with this user
|
||||
HashSet<Asset> fairFiltered = theirInventory.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).Select(static item => item.CreateShallowCopy()).ToHashSet();
|
||||
|
||||
// Copy list to HashSet<Steam.Asset>
|
||||
HashSet<Asset> fairItemsToGive = Trading.GetTradableItemsFromInventory(ourInventory.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).Select(static item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToGive.ToDictionary(static classID => classID.Key, static classID => classID.Value));
|
||||
HashSet<Asset> fairItemsToReceive = Trading.GetTradableItemsFromInventory(fairFiltered.Select(static item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToReceive.ToDictionary(static classID => classID.Key, static classID => classID.Value));
|
||||
|
||||
// Actual check:
|
||||
if (!Trading.IsTradeNeutralOrBetter(fairFiltered, fairItemsToReceive, fairItemsToGive)) {
|
||||
if (fairGivenAmount > 1) {
|
||||
fairClassIDsToGive[ourItem] = fairGivenAmount - 1;
|
||||
} else {
|
||||
fairClassIDsToGive.Remove(ourItem);
|
||||
}
|
||||
|
||||
if (fairReceivedAmount > 1) {
|
||||
fairClassIDsToReceive[theirItem] = fairReceivedAmount - 1;
|
||||
} else {
|
||||
fairClassIDsToReceive.Remove(theirItem);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip this set from the remaining of this round
|
||||
skippedSetsThisTrade.Add(set);
|
||||
|
||||
// Update our state based on given items
|
||||
classIDsToGive[ourItem] = classIDsToGive.TryGetValue(ourItem, out uint ourGivenAmount) ? ourGivenAmount + 1 : 1;
|
||||
ourFullSet[ourItem] = ourFullAmount - 1; // We don't need to remove anything here because we can guarantee that ourItem.Value is at least 2
|
||||
|
||||
if (inventoryStateChanges.TryGetValue(set, out Dictionary<ulong, uint>? currentChanges)) {
|
||||
currentChanges[ourItem] = currentChanges.TryGetValue(ourItem, out uint amount) ? amount + 1 : 1;
|
||||
} else {
|
||||
inventoryStateChanges[set] = new Dictionary<ulong, uint> {
|
||||
{ ourItem, 1 }
|
||||
};
|
||||
}
|
||||
|
||||
// Update our state based on received items
|
||||
classIDsToReceive[theirItem] = classIDsToReceive.TryGetValue(theirItem, out uint ourReceivedAmount) ? ourReceivedAmount + 1 : 1;
|
||||
ourFullSet[theirItem] = ourAmountOfTheirItem + 1;
|
||||
|
||||
if (ourTradableAmount > 1) {
|
||||
ourTradableSet[ourItem] = ourTradableAmount - 1;
|
||||
} else {
|
||||
ourTradableSet.Remove(ourItem);
|
||||
}
|
||||
|
||||
// Update their state based on taken items
|
||||
if (theirTradableAmount > 1) {
|
||||
theirTradableItems[theirItem] = theirTradableAmount - 1;
|
||||
} else {
|
||||
theirTradableItems.Remove(theirItem);
|
||||
}
|
||||
|
||||
itemsInTrade += 2;
|
||||
|
||||
match = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (match) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (match && (itemsInTrade < Trading.MaxItemsPerTrade - 1));
|
||||
|
||||
if (itemsInTrade >= Trading.MaxItemsPerTrade - 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (skippedSetsThisTrade.Count == 0) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(skippedSetsThisTrade)));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove the items from inventories
|
||||
HashSet<Asset> itemsToGive = Trading.GetTradableItemsFromInventory(ourInventory, classIDsToGive);
|
||||
HashSet<Asset> itemsToReceive = Trading.GetTradableItemsFromInventory(theirInventory, classIDsToReceive);
|
||||
|
||||
if ((itemsToGive.Count != itemsToReceive.Count) || !Trading.IsFairExchange(itemsToGive, itemsToReceive)) {
|
||||
// Failsafe
|
||||
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, Strings.ErrorAborted));
|
||||
|
||||
return (false, skippedSetsThisRound.Count > 0);
|
||||
}
|
||||
|
||||
if (triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) previousAttempt)) {
|
||||
if ((previousAttempt.GivenAssetIDs == null) || (previousAttempt.ReceivedAssetIDs == null) || (itemsToGive.Select(static item => item.AssetID).All(previousAttempt.GivenAssetIDs.Contains) && itemsToReceive.Select(static item => item.AssetID).All(previousAttempt.ReceivedAssetIDs.Contains))) {
|
||||
// This user didn't respond in our previous round, avoid him for remaining ones
|
||||
triedSteamIDs[listedUser.SteamID] = (byte.MaxValue, previousAttempt.GivenAssetIDs, previousAttempt.ReceivedAssetIDs);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
previousAttempt.GivenAssetIDs.UnionWith(itemsToGive.Select(static item => item.AssetID));
|
||||
previousAttempt.ReceivedAssetIDs.UnionWith(itemsToReceive.Select(static item => item.AssetID));
|
||||
} else {
|
||||
previousAttempt.GivenAssetIDs = new HashSet<ulong>(itemsToGive.Select(static item => item.AssetID));
|
||||
previousAttempt.ReceivedAssetIDs = new HashSet<ulong>(itemsToReceive.Select(static item => item.AssetID));
|
||||
}
|
||||
|
||||
triedSteamIDs[listedUser.SteamID] = (++previousAttempt.Tries, previousAttempt.GivenAssetIDs, previousAttempt.ReceivedAssetIDs);
|
||||
|
||||
Bot.ArchiLogger.LogGenericTrace($"{Bot.SteamID} <- {string.Join(", ", itemsToReceive.Select(static item => $"{item.RealAppID}/{item.Type}-{item.ClassID} #{item.Amount}"))} | {string.Join(", ", itemsToGive.Select(static item => $"{item.RealAppID}/{item.Type}-{item.ClassID} #{item.Amount}"))} -> {listedUser.SteamID}");
|
||||
|
||||
(bool success, HashSet<ulong>? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(listedUser.SteamID, itemsToGive, itemsToReceive, listedUser.TradeToken, true).ConfigureAwait(false);
|
||||
|
||||
if ((mobileTradeOfferIDs?.Count > 0) && Bot.HasMobileAuthenticator) {
|
||||
(bool twoFactorSuccess, _, _) = await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false);
|
||||
|
||||
if (!twoFactorSuccess) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.WarningFailed);
|
||||
|
||||
return (false, skippedSetsThisRound.Count > 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.WarningFailed);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Add itemsToGive to theirInventory to reflect their current state if we're over MaxItemsPerTrade
|
||||
theirInventory.UnionWith(itemsToGive);
|
||||
|
||||
skippedSetsThisUser.UnionWith(skippedSetsThisTrade);
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.Success);
|
||||
}
|
||||
|
||||
if (skippedSetsThisUser.Count == 0) {
|
||||
if (skippedSetsThisRound.Count == 0) {
|
||||
// If we didn't find any match on clean round, this user isn't going to have anything interesting for us anytime soon
|
||||
triedSteamIDs[listedUser.SteamID] = (byte.MaxValue, null, null);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
skippedSetsThisRound.UnionWith(skippedSetsThisUser);
|
||||
|
||||
foreach ((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) skippedSet in skippedSetsThisUser) {
|
||||
ourFullState.Remove(skippedSet);
|
||||
ourTradableState.Remove(skippedSet);
|
||||
}
|
||||
|
||||
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
|
||||
// User doesn't have any more dupes in the inventory
|
||||
break;
|
||||
}
|
||||
|
||||
ourFullState.TrimExcess();
|
||||
ourTradableState.TrimExcess();
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ActivelyMatchingItemsRound, skippedSetsThisRound.Count));
|
||||
|
||||
// Keep matching when we either traded something this round (so it makes sense for a refresh) or if we didn't try all available bots yet (so it makes sense to keep going)
|
||||
return ((totalMatches > 0) && ((skippedSetsThisRound.Count > 0) || triedSteamIDs.Values.All(static data => data.Tries < 2)), skippedSetsThisRound.Count > 0);
|
||||
}
|
||||
}
|
||||
@@ -1,848 +0,0 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// |
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.Steam;
|
||||
using ArchiSteamFarm.Steam.Cards;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using ArchiSteamFarm.Steam.Exchange;
|
||||
using ArchiSteamFarm.Steam.Integration;
|
||||
using ArchiSteamFarm.Steam.Security;
|
||||
using ArchiSteamFarm.Steam.Storage;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using ArchiSteamFarm.Web;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.Core {
|
||||
internal sealed class Statistics : IAsyncDisposable {
|
||||
private const ushort MaxItemsForFairBots = ArchiWebHandler.MaxItemsInSingleInventoryRequest * WebBrowser.MaxTries; // Determines which fair bots we'll deprioritize when matching due to excessive number of inventory requests they need to make, which are likely to fail in the process or cause excessive delays
|
||||
private const byte MaxMatchedBotsHard = 40; // Determines how many bots we can attempt to match in total, where match attempt is equal to analyzing bot's inventory
|
||||
private const byte MaxMatchingRounds = 10; // Determines maximum amount of matching rounds we're going to consider before leaving the rest of work for the next batch
|
||||
private const byte MinAnnouncementCheckTTL = 6; // Minimum amount of hours we must wait before checking eligibility for Announcement, should be lower than MinPersonaStateTTL
|
||||
private const byte MinHeartBeatTTL = 10; // Minimum amount of minutes we must wait before sending next HeartBeat
|
||||
private const byte MinItemsCount = 100; // Minimum amount of items to be eligible for public listing
|
||||
private const byte MinPersonaStateTTL = 8; // Minimum amount of hours we must wait before requesting persona state update
|
||||
private const string URL = "https://" + SharedInfo.StatisticsServer;
|
||||
|
||||
private static readonly ImmutableHashSet<Asset.EType> AcceptedMatchableTypes = ImmutableHashSet.Create(
|
||||
Asset.EType.Emoticon,
|
||||
Asset.EType.FoilTradingCard,
|
||||
Asset.EType.ProfileBackground,
|
||||
Asset.EType.TradingCard
|
||||
);
|
||||
|
||||
private readonly Bot Bot;
|
||||
private readonly SemaphoreSlim MatchActivelySemaphore = new(1, 1);
|
||||
|
||||
#pragma warning disable CA2213 // False positive, .NET Framework can't understand DisposeAsync()
|
||||
private readonly Timer MatchActivelyTimer;
|
||||
#pragma warning restore CA2213 // False positive, .NET Framework can't understand DisposeAsync()
|
||||
|
||||
private readonly SemaphoreSlim RequestsSemaphore = new(1, 1);
|
||||
|
||||
private DateTime LastAnnouncementCheck;
|
||||
private DateTime LastHeartBeat;
|
||||
private DateTime LastPersonaStateRequest;
|
||||
private bool ShouldSendHeartBeats;
|
||||
|
||||
internal Statistics(Bot bot) {
|
||||
Bot = bot ?? throw new ArgumentNullException(nameof(bot));
|
||||
|
||||
MatchActivelyTimer = new Timer(
|
||||
MatchActively,
|
||||
null,
|
||||
TimeSpan.FromHours(1) + TimeSpan.FromSeconds(ASF.LoadBalancingDelay * Bot.Bots?.Count ?? 0), // Delay
|
||||
TimeSpan.FromHours(8) // Period
|
||||
);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync() {
|
||||
MatchActivelySemaphore.Dispose();
|
||||
RequestsSemaphore.Dispose();
|
||||
|
||||
await MatchActivelyTimer.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal async Task OnHeartBeat() {
|
||||
// Request persona update if needed
|
||||
if ((DateTime.UtcNow > LastPersonaStateRequest.AddHours(MinPersonaStateTTL)) && (DateTime.UtcNow > LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL))) {
|
||||
LastPersonaStateRequest = DateTime.UtcNow;
|
||||
Bot.RequestPersonaStateUpdate();
|
||||
}
|
||||
|
||||
if (!ShouldSendHeartBeats || (DateTime.UtcNow < LastHeartBeat.AddMinutes(MinHeartBeatTTL))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await RequestsSemaphore.WaitAsync(0).ConfigureAwait(false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Uri request = new($"{URL}/Api/HeartBeat");
|
||||
|
||||
Dictionary<string, string> data = new(2, StringComparer.Ordinal) {
|
||||
{ "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") },
|
||||
{ "SteamID", Bot.SteamID.ToString(CultureInfo.InvariantCulture) }
|
||||
};
|
||||
|
||||
BasicResponse? response = await Bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.StatusCode.IsClientErrorCode()) {
|
||||
LastHeartBeat = DateTime.MinValue;
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LastHeartBeat = DateTime.UtcNow;
|
||||
} finally {
|
||||
RequestsSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task OnLoggedOn() {
|
||||
if (!await Bot.ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID).ConfigureAwait(false)) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ArchiWebHandler.JoinGroup)));
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task OnPersonaState(string? nickname = null, string? avatarHash = null) {
|
||||
if ((DateTime.UtcNow < LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL)) && (ShouldSendHeartBeats || (LastHeartBeat == DateTime.MinValue))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await RequestsSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
if ((DateTime.UtcNow < LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL)) && (ShouldSendHeartBeats || (LastHeartBeat == DateTime.MinValue))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't announce if we don't meet conditions
|
||||
bool? eligible = await IsEligibleForListing().ConfigureAwait(false);
|
||||
|
||||
if (!eligible.HasValue) {
|
||||
// This is actually network failure, so we'll stop sending heartbeats but not record it as valid check
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eligible.Value) {
|
||||
LastAnnouncementCheck = DateTime.UtcNow;
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
string? tradeToken = await Bot.ArchiHandler.GetTradeToken().ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(tradeToken)) {
|
||||
// This is actually network failure, so we'll stop sending heartbeats but not record it as valid check
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<Asset.EType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
|
||||
if (acceptedMatchableTypes.Count == 0) {
|
||||
Bot.ArchiLogger.LogNullError(nameof(acceptedMatchableTypes));
|
||||
LastAnnouncementCheck = DateTime.UtcNow;
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<Asset> inventory;
|
||||
|
||||
try {
|
||||
inventory = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => item.Tradable && acceptedMatchableTypes.Contains(item.Type)).ToHashSetAsync().ConfigureAwait(false);
|
||||
} catch (HttpRequestException e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
// This is actually inventory failure, so we'll stop sending heartbeats but not record it as valid check
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericException(e);
|
||||
|
||||
// This is actually inventory failure, so we'll stop sending heartbeats but not record it as valid check
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LastAnnouncementCheck = DateTime.UtcNow;
|
||||
|
||||
// This is actual inventory
|
||||
if (inventory.Count < MinItemsCount) {
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Uri request = new($"{URL}/Api/Announce");
|
||||
|
||||
Dictionary<string, string> data = new(9, StringComparer.Ordinal) {
|
||||
{ "AvatarHash", avatarHash ?? "" },
|
||||
{ "GamesCount", inventory.Select(static item => item.RealAppID).Distinct().Count().ToString(CultureInfo.InvariantCulture) },
|
||||
{ "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") },
|
||||
{ "ItemsCount", inventory.Count.ToString(CultureInfo.InvariantCulture) },
|
||||
{ "MatchableTypes", JsonConvert.SerializeObject(acceptedMatchableTypes) },
|
||||
{ "MatchEverything", Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) ? "1" : "0" },
|
||||
{ "Nickname", nickname ?? "" },
|
||||
{ "SteamID", Bot.SteamID.ToString(CultureInfo.InvariantCulture) },
|
||||
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
{ "TradeToken", tradeToken! }
|
||||
};
|
||||
|
||||
BasicResponse? response = await Bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.StatusCode.IsClientErrorCode()) {
|
||||
LastHeartBeat = DateTime.MinValue;
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LastHeartBeat = DateTime.UtcNow;
|
||||
ShouldSendHeartBeats = true;
|
||||
} finally {
|
||||
RequestsSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ImmutableHashSet<ListedUser>?> GetListedUsers() {
|
||||
Uri request = new($"{URL}/Api/Bots");
|
||||
|
||||
ObjectResponse<ImmutableHashSet<ListedUser>>? response = await Bot.ArchiWebHandler.WebBrowser.UrlGetToJsonObject<ImmutableHashSet<ListedUser>>(request).ConfigureAwait(false);
|
||||
|
||||
return response?.Content;
|
||||
}
|
||||
|
||||
private async Task<bool?> IsEligibleForListing() {
|
||||
bool? isEligibleForMatching = await IsEligibleForMatching().ConfigureAwait(false);
|
||||
|
||||
if (isEligibleForMatching != true) {
|
||||
return isEligibleForMatching;
|
||||
}
|
||||
|
||||
// Bot must have public inventory
|
||||
bool? hasPublicInventory = await Bot.HasPublicInventory().ConfigureAwait(false);
|
||||
|
||||
if (hasPublicInventory != true) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.HasPublicInventory)}: {hasPublicInventory?.ToString() ?? "null"}"));
|
||||
|
||||
return hasPublicInventory;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool?> IsEligibleForMatching() {
|
||||
// Bot must have ASF 2FA
|
||||
if (!Bot.HasMobileAuthenticator) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.HasMobileAuthenticator)}: {Bot.HasMobileAuthenticator}"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bot must have STM enable in TradingPreferences
|
||||
if (!Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.SteamTradeMatcher)) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.BotConfig.TradingPreferences)}: {Bot.BotConfig.TradingPreferences}"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bot must have at least one accepted matchable type set
|
||||
if ((Bot.BotConfig.MatchableTypes.Count == 0) || Bot.BotConfig.MatchableTypes.All(static type => !AcceptedMatchableTypes.Contains(type))) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.BotConfig.MatchableTypes)}: {string.Join(", ", Bot.BotConfig.MatchableTypes)}"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bot must have valid API key (e.g. not being restricted account)
|
||||
bool? hasValidApiKey = await Bot.ArchiWebHandler.HasValidApiKey().ConfigureAwait(false);
|
||||
|
||||
if (hasValidApiKey != true) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.ArchiWebHandler.HasValidApiKey)}: {hasValidApiKey?.ToString() ?? "null"}"));
|
||||
|
||||
return hasValidApiKey;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async void MatchActively(object? state = null) {
|
||||
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) || !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively) || (await IsEligibleForMatching().ConfigureAwait(false) != true)) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<Asset.EType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
|
||||
if (acceptedMatchableTypes.Count == 0) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await MatchActivelySemaphore.WaitAsync(0).ConfigureAwait(false)) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.Starting);
|
||||
|
||||
Dictionary<ulong, (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs)> triedSteamIDs = new();
|
||||
|
||||
bool shouldContinueMatching = true;
|
||||
bool tradedSomething = false;
|
||||
|
||||
for (byte i = 0; (i < MaxMatchingRounds) && shouldContinueMatching; i++) {
|
||||
if ((i > 0) && tradedSomething) {
|
||||
// After each round we wait at least 5 minutes for all bots to react
|
||||
await Task.Delay(5 * 60 * 1000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) || !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively) || (await IsEligibleForMatching().ConfigureAwait(false) != true)) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
#pragma warning disable CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
|
||||
using (await Bot.Actions.GetTradingLock().ConfigureAwait(false)) {
|
||||
#pragma warning restore CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ActivelyMatchingItems, i));
|
||||
(shouldContinueMatching, tradedSomething) = await MatchActivelyRound(acceptedMatchableTypes, triedSteamIDs).ConfigureAwait(false);
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.DoneActivelyMatchingItems, i));
|
||||
}
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.Done);
|
||||
} finally {
|
||||
MatchActivelySemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldContinueMatching, bool TradedSomething)> MatchActivelyRound(IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, IDictionary<ulong, (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs)> triedSteamIDs) {
|
||||
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
|
||||
}
|
||||
|
||||
if (triedSteamIDs == null) {
|
||||
throw new ArgumentNullException(nameof(triedSteamIDs));
|
||||
}
|
||||
|
||||
HashSet<Asset> ourInventory;
|
||||
|
||||
try {
|
||||
ourInventory = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => acceptedMatchableTypes.Contains(item.Type) && !Bot.BotDatabase.MatchActivelyBlacklistedAppIDs.Contains(item.RealAppID)).ToHashSetAsync().ConfigureAwait(false);
|
||||
} catch (HttpRequestException e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
return (false, false);
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericException(e);
|
||||
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
if (ourInventory.Count == 0) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(ourInventory)));
|
||||
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
(Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourFullState, Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourTradableState) = Trading.GetDividedInventoryState(ourInventory);
|
||||
|
||||
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
|
||||
// User doesn't have any more dupes in the inventory
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, $"{nameof(ourFullState)} || {nameof(ourTradableState)}"));
|
||||
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
ImmutableHashSet<ListedUser>? listedUsers = await GetListedUsers().ConfigureAwait(false);
|
||||
|
||||
if ((listedUsers == null) || (listedUsers.Count == 0)) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(listedUsers)));
|
||||
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
byte maxTradeHoldDuration = ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration;
|
||||
byte totalMatches = 0;
|
||||
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisRound = new();
|
||||
|
||||
foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && (!triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) attempt) || (attempt.Tries < byte.MaxValue)) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) attempt) ? attempt.Tries : 0).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenByDescending(static listedUser => listedUser.MatchEverything || (listedUser.ItemsCount < MaxItemsForFairBots)).ThenByDescending(static listedUser => listedUser.Score)) {
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> wantedSets = ourTradableState.Keys.Where(set => !skippedSetsThisRound.Contains(set) && listedUser.MatchableTypes.Contains(set.Type)).ToHashSet();
|
||||
|
||||
if (wantedSets.Count == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (++totalMatches > MaxMatchedBotsHard) {
|
||||
break;
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericTrace($"{listedUser.SteamID}...");
|
||||
|
||||
byte? holdDuration = await Bot.ArchiWebHandler.GetTradeHoldDurationForUser(listedUser.SteamID, listedUser.TradeToken).ConfigureAwait(false);
|
||||
|
||||
switch (holdDuration) {
|
||||
case null:
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(holdDuration)));
|
||||
|
||||
continue;
|
||||
case > 0 when holdDuration.Value > maxTradeHoldDuration:
|
||||
Bot.ArchiLogger.LogGenericTrace($"{holdDuration.Value} > {maxTradeHoldDuration}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSet<Asset> theirInventory;
|
||||
|
||||
try {
|
||||
theirInventory = await Bot.ArchiWebHandler.GetInventoryAsync(listedUser.SteamID).Where(item => (!listedUser.MatchEverything || item.Tradable) && wantedSets.Contains((item.RealAppID, item.Type, item.Rarity)) && ((holdDuration.Value == 0) || !(item.Type is Asset.EType.FoilTradingCard or Asset.EType.TradingCard && CardsFarmer.SalesBlacklist.Contains(item.RealAppID)))).ToHashSetAsync().ConfigureAwait(false);
|
||||
} catch (HttpRequestException e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
continue;
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericException(e);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (theirInventory.Count == 0) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(theirInventory)));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisUser = new();
|
||||
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> theirTradableState = Trading.GetTradableInventoryState(theirInventory);
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> inventoryStateChanges = new();
|
||||
|
||||
for (byte i = 0; i < Trading.MaxTradesPerAccount; i++) {
|
||||
byte itemsInTrade = 0;
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisTrade = new();
|
||||
|
||||
Dictionary<ulong, uint> classIDsToGive = new();
|
||||
Dictionary<ulong, uint> classIDsToReceive = new();
|
||||
Dictionary<ulong, uint> fairClassIDsToGive = new();
|
||||
Dictionary<ulong, uint> fairClassIDsToReceive = new();
|
||||
|
||||
foreach (((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) set, Dictionary<ulong, uint> ourFullItems) in ourFullState.Where(set => !skippedSetsThisUser.Contains(set.Key) && listedUser.MatchableTypes.Contains(set.Key.Type) && set.Value.Values.Any(static count => count > 1))) {
|
||||
if (!ourTradableState.TryGetValue(set, out Dictionary<ulong, uint>? ourTradableItems) || (ourTradableItems.Count == 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!theirTradableState.TryGetValue(set, out Dictionary<ulong, uint>? theirTradableItems) || (theirTradableItems.Count == 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Those 2 collections are on user-basis since we can't be sure that the trade passes through (and therefore we need to keep original state in case of failure)
|
||||
Dictionary<ulong, uint> ourFullSet = new(ourFullItems);
|
||||
Dictionary<ulong, uint> ourTradableSet = new(ourTradableItems);
|
||||
|
||||
// We also have to take into account changes that happened in previous trades with this user, so this block will adapt to that
|
||||
if (inventoryStateChanges.TryGetValue(set, out Dictionary<ulong, uint>? pastChanges) && (pastChanges.Count > 0)) {
|
||||
foreach ((ulong classID, uint amount) in pastChanges) {
|
||||
if (!ourFullSet.TryGetValue(classID, out uint fullAmount) || (fullAmount == 0) || (fullAmount < amount)) {
|
||||
Bot.ArchiLogger.LogNullError(nameof(fullAmount));
|
||||
|
||||
return (false, skippedSetsThisRound.Count > 0);
|
||||
}
|
||||
|
||||
if (fullAmount > amount) {
|
||||
ourFullSet[classID] = fullAmount - amount;
|
||||
} else {
|
||||
ourFullSet.Remove(classID);
|
||||
}
|
||||
|
||||
if (!ourTradableSet.TryGetValue(classID, out uint tradableAmount) || (tradableAmount == 0) || (tradableAmount < amount)) {
|
||||
Bot.ArchiLogger.LogNullError(nameof(tradableAmount));
|
||||
|
||||
return (false, skippedSetsThisRound.Count > 0);
|
||||
}
|
||||
|
||||
if (fullAmount > amount) {
|
||||
ourTradableSet[classID] = fullAmount - amount;
|
||||
} else {
|
||||
ourTradableSet.Remove(classID);
|
||||
}
|
||||
}
|
||||
|
||||
if (Trading.IsEmptyForMatching(ourFullSet, ourTradableSet)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
bool match;
|
||||
|
||||
do {
|
||||
match = false;
|
||||
|
||||
foreach ((ulong ourItem, uint ourFullAmount) in ourFullSet.Where(static item => item.Value > 1).OrderByDescending(static item => item.Value)) {
|
||||
if (!ourTradableSet.TryGetValue(ourItem, out uint ourTradableAmount) || (ourTradableAmount == 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ((ulong theirItem, uint theirTradableAmount) in theirTradableItems.OrderBy(item => ourFullSet.TryGetValue(item.Key, out uint ourAmountOfTheirItem) ? ourAmountOfTheirItem : 0)) {
|
||||
if (ourFullSet.TryGetValue(theirItem, out uint ourAmountOfTheirItem) && (ourFullAmount <= ourAmountOfTheirItem + 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!listedUser.MatchEverything) {
|
||||
// We have a potential match, let's check fairness for them
|
||||
fairClassIDsToGive.TryGetValue(ourItem, out uint fairGivenAmount);
|
||||
fairClassIDsToReceive.TryGetValue(theirItem, out uint fairReceivedAmount);
|
||||
fairClassIDsToGive[ourItem] = ++fairGivenAmount;
|
||||
fairClassIDsToReceive[theirItem] = ++fairReceivedAmount;
|
||||
|
||||
// Filter their inventory for the sets we're trading or have traded with this user
|
||||
HashSet<Asset> fairFiltered = theirInventory.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).Select(static item => item.CreateShallowCopy()).ToHashSet();
|
||||
|
||||
// Copy list to HashSet<Steam.Asset>
|
||||
HashSet<Asset> fairItemsToGive = Trading.GetTradableItemsFromInventory(ourInventory.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).Select(static item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToGive.ToDictionary(static classID => classID.Key, static classID => classID.Value));
|
||||
HashSet<Asset> fairItemsToReceive = Trading.GetTradableItemsFromInventory(fairFiltered.Select(static item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToReceive.ToDictionary(static classID => classID.Key, static classID => classID.Value));
|
||||
|
||||
// Actual check:
|
||||
if (!Trading.IsTradeNeutralOrBetter(fairFiltered, fairItemsToReceive, fairItemsToGive)) {
|
||||
if (fairGivenAmount > 1) {
|
||||
fairClassIDsToGive[ourItem] = fairGivenAmount - 1;
|
||||
} else {
|
||||
fairClassIDsToGive.Remove(ourItem);
|
||||
}
|
||||
|
||||
if (fairReceivedAmount > 1) {
|
||||
fairClassIDsToReceive[theirItem] = fairReceivedAmount - 1;
|
||||
} else {
|
||||
fairClassIDsToReceive.Remove(theirItem);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip this set from the remaining of this round
|
||||
skippedSetsThisTrade.Add(set);
|
||||
|
||||
// Update our state based on given items
|
||||
classIDsToGive[ourItem] = classIDsToGive.TryGetValue(ourItem, out uint ourGivenAmount) ? ourGivenAmount + 1 : 1;
|
||||
ourFullSet[ourItem] = ourFullAmount - 1; // We don't need to remove anything here because we can guarantee that ourItem.Value is at least 2
|
||||
|
||||
if (inventoryStateChanges.TryGetValue(set, out Dictionary<ulong, uint>? currentChanges)) {
|
||||
currentChanges[ourItem] = currentChanges.TryGetValue(ourItem, out uint amount) ? amount + 1 : 1;
|
||||
} else {
|
||||
inventoryStateChanges[set] = new Dictionary<ulong, uint> {
|
||||
{ ourItem, 1 }
|
||||
};
|
||||
}
|
||||
|
||||
// Update our state based on received items
|
||||
classIDsToReceive[theirItem] = classIDsToReceive.TryGetValue(theirItem, out uint ourReceivedAmount) ? ourReceivedAmount + 1 : 1;
|
||||
ourFullSet[theirItem] = ourAmountOfTheirItem + 1;
|
||||
|
||||
if (ourTradableAmount > 1) {
|
||||
ourTradableSet[ourItem] = ourTradableAmount - 1;
|
||||
} else {
|
||||
ourTradableSet.Remove(ourItem);
|
||||
}
|
||||
|
||||
// Update their state based on taken items
|
||||
if (theirTradableAmount > 1) {
|
||||
theirTradableItems[theirItem] = theirTradableAmount - 1;
|
||||
} else {
|
||||
theirTradableItems.Remove(theirItem);
|
||||
}
|
||||
|
||||
itemsInTrade += 2;
|
||||
|
||||
match = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (match) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (match && (itemsInTrade < Trading.MaxItemsPerTrade - 1));
|
||||
|
||||
if (itemsInTrade >= Trading.MaxItemsPerTrade - 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (skippedSetsThisTrade.Count == 0) {
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(skippedSetsThisTrade)));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove the items from inventories
|
||||
HashSet<Asset> itemsToGive = Trading.GetTradableItemsFromInventory(ourInventory, classIDsToGive);
|
||||
HashSet<Asset> itemsToReceive = Trading.GetTradableItemsFromInventory(theirInventory, classIDsToReceive);
|
||||
|
||||
if ((itemsToGive.Count != itemsToReceive.Count) || !Trading.IsFairExchange(itemsToGive, itemsToReceive)) {
|
||||
// Failsafe
|
||||
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, Strings.ErrorAborted));
|
||||
|
||||
return (false, skippedSetsThisRound.Count > 0);
|
||||
}
|
||||
|
||||
if (triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) previousAttempt)) {
|
||||
if ((previousAttempt.GivenAssetIDs == null) || (previousAttempt.ReceivedAssetIDs == null) || (itemsToGive.Select(static item => item.AssetID).All(previousAttempt.GivenAssetIDs.Contains) && itemsToReceive.Select(static item => item.AssetID).All(previousAttempt.ReceivedAssetIDs.Contains))) {
|
||||
// This user didn't respond in our previous round, avoid him for remaining ones
|
||||
triedSteamIDs[listedUser.SteamID] = (byte.MaxValue, previousAttempt.GivenAssetIDs, previousAttempt.ReceivedAssetIDs);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
previousAttempt.GivenAssetIDs.UnionWith(itemsToGive.Select(static item => item.AssetID));
|
||||
previousAttempt.ReceivedAssetIDs.UnionWith(itemsToReceive.Select(static item => item.AssetID));
|
||||
} else {
|
||||
previousAttempt.GivenAssetIDs = new HashSet<ulong>(itemsToGive.Select(static item => item.AssetID));
|
||||
previousAttempt.ReceivedAssetIDs = new HashSet<ulong>(itemsToReceive.Select(static item => item.AssetID));
|
||||
}
|
||||
|
||||
triedSteamIDs[listedUser.SteamID] = (++previousAttempt.Tries, previousAttempt.GivenAssetIDs, previousAttempt.ReceivedAssetIDs);
|
||||
|
||||
Bot.ArchiLogger.LogGenericTrace($"{Bot.SteamID} <- {string.Join(", ", itemsToReceive.Select(static item => $"{item.RealAppID}/{item.Type}-{item.ClassID} #{item.Amount}"))} | {string.Join(", ", itemsToGive.Select(static item => $"{item.RealAppID}/{item.Type}-{item.ClassID} #{item.Amount}"))} -> {listedUser.SteamID}");
|
||||
|
||||
(bool success, HashSet<ulong>? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(listedUser.SteamID, itemsToGive, itemsToReceive, listedUser.TradeToken, true).ConfigureAwait(false);
|
||||
|
||||
if ((mobileTradeOfferIDs?.Count > 0) && Bot.HasMobileAuthenticator) {
|
||||
(bool twoFactorSuccess, _, _) = await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false);
|
||||
|
||||
if (!twoFactorSuccess) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.WarningFailed);
|
||||
|
||||
return (false, skippedSetsThisRound.Count > 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.WarningFailed);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Add itemsToGive to theirInventory to reflect their current state if we're over MaxItemsPerTrade
|
||||
theirInventory.UnionWith(itemsToGive);
|
||||
|
||||
skippedSetsThisUser.UnionWith(skippedSetsThisTrade);
|
||||
Bot.ArchiLogger.LogGenericTrace(Strings.Success);
|
||||
}
|
||||
|
||||
if (skippedSetsThisUser.Count == 0) {
|
||||
if (skippedSetsThisRound.Count == 0) {
|
||||
// If we didn't find any match on clean round, this user isn't going to have anything interesting for us anytime soon
|
||||
triedSteamIDs[listedUser.SteamID] = (byte.MaxValue, null, null);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
skippedSetsThisRound.UnionWith(skippedSetsThisUser);
|
||||
|
||||
foreach ((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) skippedSet in skippedSetsThisUser) {
|
||||
ourFullState.Remove(skippedSet);
|
||||
ourTradableState.Remove(skippedSet);
|
||||
}
|
||||
|
||||
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
|
||||
// User doesn't have any more dupes in the inventory
|
||||
break;
|
||||
}
|
||||
|
||||
ourFullState.TrimExcess();
|
||||
ourTradableState.TrimExcess();
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ActivelyMatchingItemsRound, skippedSetsThisRound.Count));
|
||||
|
||||
// Keep matching when we either traded something this round (so it makes sense for a refresh) or if we didn't try all available bots yet (so it makes sense to keep going)
|
||||
return ((totalMatches > 0) && ((skippedSetsThisRound.Count > 0) || triedSteamIDs.Values.All(static data => data.Tries < 2)), skippedSetsThisRound.Count > 0);
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
private sealed class ListedUser {
|
||||
#pragma warning disable CS0649 // False positive, it's a field set during json deserialization
|
||||
[JsonProperty(PropertyName = "items_count", Required = Required.Always)]
|
||||
internal readonly ushort ItemsCount;
|
||||
#pragma warning restore CS0649 // False positive, it's a field set during json deserialization
|
||||
|
||||
internal readonly HashSet<Asset.EType> MatchableTypes = new();
|
||||
|
||||
#pragma warning disable CS0649 // False positive, it's a field set during json deserialization
|
||||
[JsonProperty(PropertyName = "steam_id", Required = Required.Always)]
|
||||
internal readonly ulong SteamID;
|
||||
#pragma warning restore CS0649 // False positive, it's a field set during json deserialization
|
||||
|
||||
[JsonProperty(PropertyName = "trade_token", Required = Required.Always)]
|
||||
internal readonly string TradeToken = "";
|
||||
|
||||
internal float Score => GamesCount / (float) ItemsCount;
|
||||
|
||||
#pragma warning disable CS0649 // False positive, it's a field set during json deserialization
|
||||
[JsonProperty(PropertyName = "games_count", Required = Required.Always)]
|
||||
private readonly ushort GamesCount;
|
||||
#pragma warning restore CS0649 // False positive, it's a field set during json deserialization
|
||||
|
||||
internal bool MatchEverything { get; private set; }
|
||||
|
||||
[JsonProperty(PropertyName = "matchable_backgrounds", Required = Required.Always)]
|
||||
private byte MatchableBackgroundsNumber {
|
||||
set {
|
||||
switch (value) {
|
||||
case 0:
|
||||
MatchableTypes.Remove(Asset.EType.ProfileBackground);
|
||||
|
||||
break;
|
||||
case 1:
|
||||
MatchableTypes.Add(Asset.EType.ProfileBackground);
|
||||
|
||||
break;
|
||||
default:
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "matchable_cards", Required = Required.Always)]
|
||||
private byte MatchableCardsNumber {
|
||||
set {
|
||||
switch (value) {
|
||||
case 0:
|
||||
MatchableTypes.Remove(Asset.EType.TradingCard);
|
||||
|
||||
break;
|
||||
case 1:
|
||||
MatchableTypes.Add(Asset.EType.TradingCard);
|
||||
|
||||
break;
|
||||
default:
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "matchable_emoticons", Required = Required.Always)]
|
||||
private byte MatchableEmoticonsNumber {
|
||||
set {
|
||||
switch (value) {
|
||||
case 0:
|
||||
MatchableTypes.Remove(Asset.EType.Emoticon);
|
||||
|
||||
break;
|
||||
case 1:
|
||||
MatchableTypes.Add(Asset.EType.Emoticon);
|
||||
|
||||
break;
|
||||
default:
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "matchable_foil_cards", Required = Required.Always)]
|
||||
private byte MatchableFoilCardsNumber {
|
||||
set {
|
||||
switch (value) {
|
||||
case 0:
|
||||
MatchableTypes.Remove(Asset.EType.FoilTradingCard);
|
||||
|
||||
break;
|
||||
case 1:
|
||||
MatchableTypes.Add(Asset.EType.FoilTradingCard);
|
||||
|
||||
break;
|
||||
default:
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "match_everything", Required = Required.Always)]
|
||||
private byte MatchEverythingNumber {
|
||||
set {
|
||||
switch (value) {
|
||||
case 0:
|
||||
MatchEverything = false;
|
||||
|
||||
break;
|
||||
case 1:
|
||||
MatchEverything = true;
|
||||
|
||||
break;
|
||||
default:
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
private ListedUser() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user