Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1386afc7da | ||
|
|
88d325cf8f | ||
|
|
74ebef9b22 | ||
|
|
64578db453 | ||
|
|
3d0e102afc | ||
|
|
f9f26f36c3 | ||
|
|
644c2a106c | ||
|
|
4b003bd95e | ||
|
|
f2d23e511d | ||
|
|
07be1a7b9c | ||
|
|
8178104612 | ||
|
|
23b0f0f66b | ||
|
|
2d69b246d9 | ||
|
|
4f0f3df8bb | ||
|
|
2c25b67cf9 | ||
|
|
f6e700c921 | ||
|
|
23e7e54f8d | ||
|
|
b52f7ebca9 | ||
|
|
329b7e77d9 | ||
|
|
5ab9d1c68a | ||
|
|
15a90bfd47 | ||
|
|
17f32e65b2 | ||
|
|
b397d07baf | ||
|
|
eee99c069b | ||
|
|
81ee6557f4 | ||
|
|
f484a9bef1 | ||
|
|
6d19819de5 | ||
|
|
71311cb584 | ||
|
|
5c4f62ac25 | ||
|
|
869de31315 | ||
|
|
6f6c663b43 | ||
|
|
fdd0fc2230 | ||
|
|
5d5a9c2a63 | ||
|
|
eb75fc9aee | ||
|
|
600904e6bc | ||
|
|
ce821695e8 | ||
|
|
455fe8e64f | ||
|
|
f4bc3b7401 | ||
|
|
b528957c40 | ||
|
|
f8c9d81f52 | ||
|
|
1a5a261f7c | ||
|
|
95e019e88d | ||
|
|
466197fd8b | ||
|
|
4a4bf0300a | ||
|
|
953b9aedb3 | ||
|
|
2ce9636d3a | ||
|
|
b03b2fd226 | ||
|
|
d96eb2d34c | ||
|
|
5f2149ed1e | ||
|
|
a7509c90e7 | ||
|
|
8b48b583b9 | ||
|
|
58f761c372 | ||
|
|
36d364517b | ||
|
|
869eeb5539 | ||
|
|
b2afbaab93 | ||
|
|
9512856cfc | ||
|
|
593e509bc6 | ||
|
|
68ed191efc | ||
|
|
77925964a0 | ||
|
|
665e6b6ce4 | ||
|
|
51306f3067 | ||
|
|
c47d9573ad | ||
|
|
bbe5395f4a | ||
|
|
6f7a9b681d | ||
|
|
ed0a8270cf | ||
|
|
4b3a47b13c | ||
|
|
6f4f4ca472 | ||
|
|
55f7d86b3d | ||
|
|
01f03ea9af | ||
|
|
82ac44c4c1 | ||
|
|
65ee5a6b5c | ||
|
|
b7c83e6208 | ||
|
|
f2c639fc64 | ||
|
|
0978258513 | ||
|
|
b243aec74f | ||
|
|
ec5a7e4292 | ||
|
|
4852ef9e3a | ||
|
|
d7a1d65348 | ||
|
|
758a0a3db8 | ||
|
|
b27911adae | ||
|
|
43e5a8ecab | ||
|
|
b52ed15079 | ||
|
|
c82051f276 | ||
|
|
6fc80f0a96 | ||
|
|
2da55e138c | ||
|
|
5cbf7ea366 | ||
|
|
a2d27fd4c3 | ||
|
|
be446fc9d9 | ||
|
|
4b93a50927 | ||
|
|
76cbe0abbc | ||
|
|
a54b674ce6 | ||
|
|
11440a3d24 | ||
|
|
5f478a6884 | ||
|
|
fb2edcf70a | ||
|
|
ed2a921d9e | ||
|
|
ba8a412309 | ||
|
|
5d42a46891 | ||
|
|
c13367f221 | ||
|
|
c94be67727 | ||
|
|
1e8bb16a22 | ||
|
|
3e87fe9d6f | ||
|
|
b517a80bea | ||
|
|
b2938e6297 | ||
|
|
3a96bf69cc | ||
|
|
429a1cd4ae | ||
|
|
dab2fd9259 | ||
|
|
8048d87222 | ||
|
|
edcefbe62c | ||
|
|
aea75aa417 | ||
|
|
30d407e978 | ||
|
|
8df42bbcb1 | ||
|
|
da396208f0 | ||
|
|
466c698b1d | ||
|
|
58bc20ce0e | ||
|
|
25fdd5485d | ||
|
|
bfccf60a07 | ||
|
|
8be1223e32 | ||
|
|
df1259f87c | ||
|
|
1ca3578e06 | ||
|
|
3c70214ca9 | ||
|
|
8c44d7ad17 | ||
|
|
4199705fe8 | ||
|
|
7141e671d1 | ||
|
|
c20ec7343c | ||
|
|
9a714b969e | ||
|
|
d518389493 | ||
|
|
c6f81c70a6 | ||
|
|
70659b7544 | ||
|
|
aa56bbc484 | ||
|
|
901592a54a | ||
|
|
a51db00a15 | ||
|
|
0197298e31 | ||
|
|
a4dada9237 | ||
|
|
6b2f719605 | ||
|
|
ed70c8e688 | ||
|
|
f68ebf0861 | ||
|
|
39788df29c | ||
|
|
f1ea45ce2a | ||
|
|
31a0579f65 | ||
|
|
ad8ea13444 | ||
|
|
5df97c4a7e | ||
|
|
f5538916fe | ||
|
|
d7be49edd7 | ||
|
|
6e10c549e4 | ||
|
|
656e52ffff | ||
|
|
8c9e98ebd8 | ||
|
|
da09cb820a | ||
|
|
d45d974280 | ||
|
|
613d4f0ff3 | ||
|
|
8f4942a3a2 | ||
|
|
ef34efdd54 | ||
|
|
9886f2ee96 | ||
|
|
3e4c0d8fa5 | ||
|
|
f72856a8bd | ||
|
|
980a8f5036 | ||
|
|
9f1a4772cd | ||
|
|
c68d92c8e1 | ||
|
|
076737672e | ||
|
|
689a93a3f7 | ||
|
|
9ed7869ddd | ||
|
|
c62c237442 | ||
|
|
4e10dbb2c7 | ||
|
|
197ff41e69 | ||
|
|
5089abaca6 | ||
|
|
8469d51f0c | ||
|
|
9b2d52a39b | ||
|
|
d95b581dae | ||
|
|
759c4fff82 | ||
|
|
d5bdb769b3 | ||
|
|
8a1a15d77d | ||
|
|
24d2da8e67 | ||
|
|
6d1779ea5f | ||
|
|
0cbc5f759e | ||
|
|
ce1d99bfd8 | ||
|
|
257d687166 | ||
|
|
cfc192e1dc | ||
|
|
161b9226fb | ||
|
|
46b8d9bf17 | ||
|
|
9faa3fd25a | ||
|
|
bca0471d9f | ||
|
|
5610490db6 | ||
|
|
5efebd0bb7 | ||
|
|
81bc9028d1 | ||
|
|
3de9f6db93 | ||
|
|
0f08cd8e93 | ||
|
|
ea77ac5fe7 | ||
|
|
1df9e0a28f | ||
|
|
54316a1eac | ||
|
|
76b911f50f | ||
|
|
5b9c56a2c6 | ||
|
|
2b2de1baf3 | ||
|
|
0e800cc3e2 | ||
|
|
42639c8273 | ||
|
|
9a5ee8cd5e | ||
|
|
e6b65a6a10 | ||
|
|
5597df55ca | ||
|
|
dc75cce5dc | ||
|
|
4a0396fe81 | ||
|
|
0f34b4951c | ||
|
|
d7f776ad37 | ||
|
|
723a75e672 | ||
|
|
541810f2e3 | ||
|
|
e9b5cf6dc5 | ||
|
|
4b2e1f7059 | ||
|
|
6d43868b4d | ||
|
|
1c07b74f79 | ||
|
|
4deed0867a | ||
|
|
99579bfda4 | ||
|
|
cb1f443d23 | ||
|
|
e890be4b38 | ||
|
|
2d3deb74ae | ||
|
|
0acd6d8cc9 | ||
|
|
d77ad0dc7c | ||
|
|
8bc909eb72 | ||
|
|
329f4fcb5c | ||
|
|
5fd89b5b6b | ||
|
|
f9780c5942 | ||
|
|
d895dd0b40 | ||
|
|
a786ba4f08 | ||
|
|
fe94a012eb | ||
|
|
9d4c8681ca | ||
|
|
8a31454a4c | ||
|
|
f7297fce3c | ||
|
|
28067f0c94 | ||
|
|
86d2cc32e7 | ||
|
|
82ec22fa69 | ||
|
|
eb0917c4e7 | ||
|
|
5f86424c4a | ||
|
|
487d113432 | ||
|
|
21a79272ea | ||
|
|
410d13c764 | ||
|
|
5127417bd3 | ||
|
|
e1c461e3b2 | ||
|
|
3b3282acea | ||
|
|
7426e79504 | ||
|
|
5bceafec8b | ||
|
|
3d004062d7 | ||
|
|
0fcddd2aed | ||
|
|
237dfd6f34 | ||
|
|
70debc2b0d | ||
|
|
241fc15269 | ||
|
|
16a9832af0 | ||
|
|
94f6c82560 | ||
|
|
6eebe281ac | ||
|
|
bd092bd8d2 | ||
|
|
cdd16dbb92 | ||
|
|
0870384b8d | ||
|
|
bcea0129ba | ||
|
|
8989738c58 | ||
|
|
b457877623 | ||
|
|
d047886630 | ||
|
|
54888942f5 | ||
|
|
129c00ac44 | ||
|
|
f7ea236823 | ||
|
|
f94bd16b2b | ||
|
|
8db079f720 | ||
|
|
084bdb6573 | ||
|
|
ac3e79401b | ||
|
|
baa6bf0e9b | ||
|
|
f0c6d5a6b7 | ||
|
|
fd7294b895 | ||
|
|
7e700ad75e | ||
|
|
fcaffc5cf2 | ||
|
|
3e6f9a9bf1 | ||
|
|
700e7e23b2 | ||
|
|
d41771a6c4 | ||
|
|
58d2834949 | ||
|
|
215e589438 | ||
|
|
cf7eaa6d09 | ||
|
|
a30039bc9e | ||
|
|
27ebd1b4db | ||
|
|
203dce6775 | ||
|
|
63b761b5dc | ||
|
|
580d9c6253 | ||
|
|
fdc25d6c4d | ||
|
|
99778ea51f | ||
|
|
3e6521c2e6 | ||
|
|
648d42ed84 | ||
|
|
328ff67a4b | ||
|
|
f3d6677ef2 | ||
|
|
bbc183e82e | ||
|
|
e879514846 | ||
|
|
0e15b7ccdb | ||
|
|
5bdc6caa13 | ||
|
|
376fb05225 | ||
|
|
886fc88816 | ||
|
|
e156671b17 | ||
|
|
ea5071cf85 | ||
|
|
a15f7207fe | ||
|
|
e0e0ff01f1 | ||
|
|
20f59028e2 | ||
|
|
7bc34a7b2f | ||
|
|
2dc1356cf7 | ||
|
|
ae0ab54efe | ||
|
|
bfb367ee62 | ||
|
|
8d925509fa | ||
|
|
811d656022 | ||
|
|
c7c0e9e1a3 | ||
|
|
9bc085af3a | ||
|
|
a5d7fdaffa | ||
|
|
16bf599d88 | ||
|
|
59ba4409b5 | ||
|
|
f510965d05 | ||
|
|
d43441358d | ||
|
|
1ece05f7a7 | ||
|
|
ca93f3aa5d | ||
|
|
5c6e2211d6 | ||
|
|
f469d98f74 | ||
|
|
699fcac7af | ||
|
|
c22275c1bb | ||
|
|
470f42b1b9 | ||
|
|
4b5b8385f8 | ||
|
|
42655d5209 | ||
|
|
09a33ccf3d | ||
|
|
fd93910941 | ||
|
|
51de6f9f00 | ||
|
|
d615f11608 | ||
|
|
c6d2f3f068 | ||
|
|
0606adb5da | ||
|
|
82576282ab | ||
|
|
e6fe7ef609 | ||
|
|
45e7a5ca9a | ||
|
|
7d3710d1e8 | ||
|
|
e354ed8074 | ||
|
|
c4a5c6fe34 | ||
|
|
2079b81df4 | ||
|
|
e64b312562 | ||
|
|
cceb7905c7 | ||
|
|
4007087ceb | ||
|
|
e64f1151f3 | ||
|
|
46202e5a69 | ||
|
|
782001d338 | ||
|
|
4eeadb934f | ||
|
|
1f16c13300 | ||
|
|
ee479fb526 | ||
|
|
d05ff50f76 | ||
|
|
927ade6108 | ||
|
|
5eda34ded0 | ||
|
|
d9b9d66e3b | ||
|
|
1adeb3a32a | ||
|
|
1abf57eae1 | ||
|
|
65797eba8f | ||
|
|
03a6e8b05c | ||
|
|
2c09459bde | ||
|
|
7564fea4b0 | ||
|
|
95b27f099f | ||
|
|
e96c042853 | ||
|
|
57bf7a0f7d | ||
|
|
da892d9043 | ||
|
|
7fad5e9aff | ||
|
|
840ffcdf0c | ||
|
|
1078563f2d | ||
|
|
c1f2cad291 | ||
|
|
285e635b0d | ||
|
|
54203ff036 | ||
|
|
6b378a4606 | ||
|
|
9707fc4563 | ||
|
|
1a4e6c9533 | ||
|
|
fb22b86d11 | ||
|
|
3c3f95142d | ||
|
|
029b8dcdc6 | ||
|
|
400d43c49c | ||
|
|
d2a3670a6a | ||
|
|
bf421a67a9 | ||
|
|
f51de30fbd | ||
|
|
32d7dc2e2a | ||
|
|
37d41ed008 | ||
|
|
5e55872668 | ||
|
|
a299051ecd | ||
|
|
7056a58958 | ||
|
|
006fb6516f | ||
|
|
04b3367935 | ||
|
|
32f67d56df | ||
|
|
49669e9919 | ||
|
|
ad2cb6a365 | ||
|
|
98f69211c1 | ||
|
|
8515b6da92 | ||
|
|
47993f141c | ||
|
|
9726690c07 | ||
|
|
f37c916e72 | ||
|
|
994646d811 | ||
|
|
c8eb88f672 | ||
|
|
8fa729ddab | ||
|
|
59d0795153 | ||
|
|
69d546f5ee | ||
|
|
9b22917660 | ||
|
|
cd979b3e4a | ||
|
|
528400503c | ||
|
|
59c8a898b7 | ||
|
|
637920fc4e | ||
|
|
3985817b19 | ||
|
|
51c7216068 | ||
|
|
196f99e5e6 | ||
|
|
92300818c3 | ||
|
|
e46edbc6aa | ||
|
|
e304e227c8 | ||
|
|
77149a621f | ||
|
|
6a213014b8 | ||
|
|
a9c3e125ab | ||
|
|
94a617f9ae | ||
|
|
c85a360152 | ||
|
|
81a783624e | ||
|
|
d679858d30 | ||
|
|
5bb62a9642 | ||
|
|
27388b82fc | ||
|
|
35b5d7d6f8 | ||
|
|
883aa4468a | ||
|
|
64908630f4 | ||
|
|
56bde23156 | ||
|
|
9c0fa2207c | ||
|
|
a1a1a5f375 | ||
|
|
4e35b8a1a8 | ||
|
|
589fda539f | ||
|
|
c65ae545df | ||
|
|
5ae6194d23 | ||
|
|
b83dbb8e8d | ||
|
|
f8e41d525b | ||
|
|
d9624e04c1 | ||
|
|
f4ff1b953f | ||
|
|
da6ccc6250 | ||
|
|
a87d72a044 | ||
|
|
ab1d496fff | ||
|
|
5d4e7e8dec | ||
|
|
e4f2ae4826 | ||
|
|
1315d9decb | ||
|
|
c358282e68 | ||
|
|
c57e820d63 | ||
|
|
6b1e155197 | ||
|
|
47c5e20238 | ||
|
|
36d3c2e913 | ||
|
|
06623142f7 | ||
|
|
1b9fdad539 | ||
|
|
c0a598cc34 | ||
|
|
e3fd1b61f5 | ||
|
|
2dcc8fcdfe | ||
|
|
6fe9a18d43 | ||
|
|
9f8bed6efb | ||
|
|
0121c7af2e | ||
|
|
1d84a86e0b | ||
|
|
99f8c65c2e | ||
|
|
0ec7ced4d6 | ||
|
|
551a547ddb | ||
|
|
09caab2120 | ||
|
|
2905ee5d31 | ||
|
|
e9d31f220f | ||
|
|
27f0e3d793 | ||
|
|
5e1a783394 | ||
|
|
7aef2a6a73 | ||
|
|
9761f87826 | ||
|
|
3321b7bc84 | ||
|
|
667c2a4dc5 | ||
|
|
87b4737901 | ||
|
|
376547ebfe | ||
|
|
7935da58a7 | ||
|
|
b890ff266f | ||
|
|
c939877c7b | ||
|
|
1bc392327c | ||
|
|
df0e70470c | ||
|
|
2eb9f84520 | ||
|
|
5c6fd83155 | ||
|
|
6226956af4 | ||
|
|
04dac4070f | ||
|
|
bcf5bd4f11 | ||
|
|
cf8c393cf1 | ||
|
|
070fd9d98e | ||
|
|
63476a402d | ||
|
|
28ca8d2ee7 | ||
|
|
bc24fea4e9 | ||
|
|
4ea5e4d265 | ||
|
|
1fe56c95c6 | ||
|
|
b5c0e8d0dc | ||
|
|
39dd868a34 | ||
|
|
5f1ffd153b | ||
|
|
01765e0616 | ||
|
|
99c4348bc3 | ||
|
|
c8b59c338b | ||
|
|
7f7acf1e2a | ||
|
|
6ddca4c7db | ||
|
|
85bc2dae29 | ||
|
|
c3365e3d02 | ||
|
|
8104b87083 | ||
|
|
a4e435fec4 | ||
|
|
51715433a9 | ||
|
|
a8ba63d968 | ||
|
|
39f65ab1fb | ||
|
|
8dfe8a2163 | ||
|
|
f450dc7591 | ||
|
|
c22230756b | ||
|
|
48b2c42eb2 | ||
|
|
c3f519aa7b | ||
|
|
854509b489 | ||
|
|
640bf93de4 | ||
|
|
19f45d0270 | ||
|
|
dcccee68f2 | ||
|
|
ded772b377 | ||
|
|
faed0154aa | ||
|
|
f695954cbb | ||
|
|
d9d1dda9cf | ||
|
|
e61933c49f | ||
|
|
dff178bbc7 | ||
|
|
b01ff94b8a | ||
|
|
739e5d3014 | ||
|
|
c92d32b6de | ||
|
|
2924836a32 | ||
|
|
a65df17323 | ||
|
|
9339a3b432 | ||
|
|
2d1847fce4 | ||
|
|
a60849887c | ||
|
|
d35fda18c0 | ||
|
|
e099848ed4 | ||
|
|
fdfdd81b84 | ||
|
|
aa5af79e85 | ||
|
|
e3093ee50a | ||
|
|
4e4dfc695e | ||
|
|
e7d40c286e | ||
|
|
992bfa91e8 | ||
|
|
3218d84e02 | ||
|
|
01e8078067 | ||
|
|
b12904132c | ||
|
|
2bdedf40eb | ||
|
|
df05d9ff02 | ||
|
|
55313f46bb | ||
|
|
e66661c3c8 | ||
|
|
32011d815b | ||
|
|
36d5f2a532 | ||
|
|
7266468d85 | ||
|
|
96bc1ef7cb | ||
|
|
2e0be629d8 | ||
|
|
0da682043e | ||
|
|
2a6f1bd6a4 | ||
|
|
a0b2917cc5 | ||
|
|
777c6da302 | ||
|
|
5f0a804a41 | ||
|
|
a2c308b6f8 | ||
|
|
d8942f92b4 | ||
|
|
0fe0b55661 | ||
|
|
343934970d | ||
|
|
268652b4ff | ||
|
|
d4de72b285 | ||
|
|
430b6f5394 | ||
|
|
5dbb5c86d4 | ||
|
|
515e308bab | ||
|
|
ba1f8de798 | ||
|
|
88c9e28d45 | ||
|
|
badcd77ea6 | ||
|
|
b3d756eeba | ||
|
|
579ab25c7d | ||
|
|
65f7641884 | ||
|
|
4ab5908336 | ||
|
|
8fc3155868 | ||
|
|
69ba480c08 | ||
|
|
7eef36c2df | ||
|
|
3b6e92fcb1 | ||
|
|
f1dbce3a69 | ||
|
|
22b468e771 | ||
|
|
aaae0ac41c | ||
|
|
b1e4f82c25 | ||
|
|
2f37b2b1da | ||
|
|
da401dc9bc | ||
|
|
77d04a0c21 | ||
|
|
303082ff99 | ||
|
|
bb523b34fe | ||
|
|
db7c011ded | ||
|
|
43244e85b9 | ||
|
|
96e42049eb | ||
|
|
d4836a1e4f | ||
|
|
68bb17428a | ||
|
|
ddd92ad303 | ||
|
|
f60f6539ff | ||
|
|
2cc9ee36c7 | ||
|
|
269fd2246f | ||
|
|
5f44213692 | ||
|
|
d6a8de3c80 | ||
|
|
b5805a19aa | ||
|
|
44d9cb40f2 | ||
|
|
616e6b25b9 | ||
|
|
5993e876e8 | ||
|
|
752bd813ea | ||
|
|
feafdca33d | ||
|
|
a267c9fbf2 | ||
|
|
d766f4222c | ||
|
|
e1fa484cb8 | ||
|
|
bfcd2babaa | ||
|
|
db63ce66b6 | ||
|
|
29a4eaf9b2 | ||
|
|
b33135f704 | ||
|
|
67e03fc938 | ||
|
|
3c39cfa5d9 | ||
|
|
be1f1ce08c | ||
|
|
7b7815a021 | ||
|
|
5b70c24c7b | ||
|
|
636745f47b | ||
|
|
4d083bee83 | ||
|
|
fe8c6d5de0 | ||
|
|
857052c75d | ||
|
|
bf7ce99c5f | ||
|
|
b6260327b7 | ||
|
|
8a85f38819 | ||
|
|
c2bd99af18 | ||
|
|
5e43da1f02 | ||
|
|
f5fb8561c6 | ||
|
|
eb6dd85063 | ||
|
|
fd2d57fb0c | ||
|
|
fa1297e15e | ||
|
|
e184db7045 | ||
|
|
d46624e932 | ||
|
|
6d88ccb379 | ||
|
|
2d9307cb94 | ||
|
|
2aff1800ef | ||
|
|
17b7fcc2a2 | ||
|
|
1e1b4a2da0 | ||
|
|
68f2f0f3ce | ||
|
|
05fe77a5d0 | ||
|
|
c2a017c517 | ||
|
|
4c52045f08 | ||
|
|
f1d228c9d7 | ||
|
|
0f156be4d6 | ||
|
|
86db356ca4 | ||
|
|
2c53524e79 | ||
|
|
ea74a3b43f | ||
|
|
308c74ac4f | ||
|
|
ede3dcd91b | ||
|
|
8a773f4e5c | ||
|
|
496340be8d | ||
|
|
5c877c428e | ||
|
|
876df0dd5e | ||
|
|
d69a9942ac | ||
|
|
cec307cf9c | ||
|
|
825e787dbe | ||
|
|
702a2ebfca | ||
|
|
0156b80ee6 | ||
|
|
2043c45d48 | ||
|
|
503be01463 | ||
|
|
84a5b6257f | ||
|
|
81d513b0c9 | ||
|
|
e64891dbbf | ||
|
|
3bd3364b4b | ||
|
|
afefdfd6b4 | ||
|
|
7c5e94979b | ||
|
|
93d2a4bc8b | ||
|
|
9559cf0e23 | ||
|
|
aacb344f32 | ||
|
|
69870e73c5 | ||
|
|
ea8fc96f83 | ||
|
|
54d27cace8 | ||
|
|
7e365fb1cf | ||
|
|
ca9e459de6 | ||
|
|
2efcd66d99 | ||
|
|
6e38fd5dd7 | ||
|
|
37a4c8e0b2 | ||
|
|
872fcccb00 | ||
|
|
9644e7a36e | ||
|
|
19af63f965 | ||
|
|
ee29e0918b | ||
|
|
571f2101e8 | ||
|
|
18c5344af7 | ||
|
|
f7b1c834c9 | ||
|
|
bbda12709f | ||
|
|
0b6b1bfa2a | ||
|
|
01284ae62e | ||
|
|
af5e4ffebe | ||
|
|
e6491d8642 | ||
|
|
589defb38e | ||
|
|
b0d3552121 | ||
|
|
6554ad167b | ||
|
|
5822151d14 | ||
|
|
8b438b25ff | ||
|
|
3459d4a35e | ||
|
|
84d1bf1559 | ||
|
|
0d6c02486a | ||
|
|
430c2557d7 | ||
|
|
4bd140cac4 | ||
|
|
7dbbbfbebf | ||
|
|
32c9483afd | ||
|
|
e47c98b80c | ||
|
|
861b4ff639 | ||
|
|
914ac74d2f | ||
|
|
5d91d2ff14 | ||
|
|
0e4d4e7dff | ||
|
|
ad614cb000 | ||
|
|
3da81815bd | ||
|
|
916777b86d | ||
|
|
32dc347e7e | ||
|
|
10015fc73c | ||
|
|
3fea6d7399 | ||
|
|
55bead2c9f | ||
|
|
eaf0beb83b | ||
|
|
0f60502ce1 | ||
|
|
69f94b2c1f | ||
|
|
80d2a14d5c | ||
|
|
6f0a000bf1 | ||
|
|
0cee182a8d | ||
|
|
56b703e8a8 | ||
|
|
076fafb64e | ||
|
|
dd0084caad | ||
|
|
1450e158f5 | ||
|
|
4c28ab9461 | ||
|
|
e34a33ac98 | ||
|
|
1e51c6e5e5 | ||
|
|
1ab1195a31 | ||
|
|
4a9ff45959 | ||
|
|
9337e7d2bc | ||
|
|
3f81bf3d75 | ||
|
|
65a8085065 | ||
|
|
1d7fcbafca | ||
|
|
71debec858 | ||
|
|
0477d0d2bf | ||
|
|
0234aa1fdf | ||
|
|
d9f5f55ea2 | ||
|
|
0dcbb98e89 | ||
|
|
ca4d51e48e | ||
|
|
33b0b533e6 | ||
|
|
18ded27113 | ||
|
|
5ef427b85b | ||
|
|
8bdf5e2857 | ||
|
|
c6829b4ab0 | ||
|
|
e114c89d65 | ||
|
|
ffd748bd3c | ||
|
|
7805722c1b | ||
|
|
71c24fcb64 | ||
|
|
6def14dc98 | ||
|
|
6fbbdb46dc | ||
|
|
df7ef97575 | ||
|
|
bfb8c16fa0 | ||
|
|
28a455d964 | ||
|
|
654f6ed91e | ||
|
|
fe6019079b | ||
|
|
558be5ab9a | ||
|
|
36fc3d7574 | ||
|
|
66e0c9a52d | ||
|
|
3fa0471198 | ||
|
|
0b318e1131 | ||
|
|
6b869570a4 | ||
|
|
fdcdd90b43 | ||
|
|
e9c877029d | ||
|
|
54e2b818b6 | ||
|
|
b31f999722 | ||
|
|
2b7ca3472c | ||
|
|
55e5ddb384 | ||
|
|
b27ebc7a26 | ||
|
|
e76c2e9904 | ||
|
|
735059f15e | ||
|
|
4e3dfdb7c2 | ||
|
|
589faf2a82 | ||
|
|
2d9634a6d3 | ||
|
|
79fbd1285a | ||
|
|
a0dd945c28 | ||
|
|
f5dad6e79b | ||
|
|
473912417f | ||
|
|
98996c321b | ||
|
|
16b6f9d231 | ||
|
|
00833e3228 | ||
|
|
9911334289 | ||
|
|
5c461f096a | ||
|
|
bc8aea48a8 | ||
|
|
918ccc4f4e | ||
|
|
e5dd04466e | ||
|
|
6bea13545a | ||
|
|
6cf3b4c236 | ||
|
|
9bd9eba6b2 | ||
|
|
d50d6c37fa | ||
|
|
781d9a3494 | ||
|
|
3817638264 | ||
|
|
7e2c8df8db | ||
|
|
f965c36464 | ||
|
|
ece7abb6e8 | ||
|
|
db28973d15 | ||
|
|
585c0ccd85 | ||
|
|
02fe089f68 | ||
|
|
e601e39c98 | ||
|
|
6b1cdc4425 | ||
|
|
be12f8dcd5 | ||
|
|
fd883077e0 | ||
|
|
5267443210 | ||
|
|
ad3925fac8 | ||
|
|
c0bf19912e | ||
|
|
dfe1a7372c | ||
|
|
ab5f006a5f | ||
|
|
d008b14230 | ||
|
|
d12dfec564 | ||
|
|
28156fc50f | ||
|
|
f6edcd89ba | ||
|
|
ab642066cf | ||
|
|
8c04a29d9a | ||
|
|
f279c4193c | ||
|
|
e3ab8fa849 | ||
|
|
6cdb7b1c8a | ||
|
|
967184d48a | ||
|
|
da7a72efc6 | ||
|
|
c20d936d3c | ||
|
|
6a6421e8d3 | ||
|
|
a9f9531c58 | ||
|
|
30d383edab | ||
|
|
d63d72735e | ||
|
|
95c87404ab | ||
|
|
90f939011d | ||
|
|
62b4129fb1 | ||
|
|
bd133ce55f | ||
|
|
8e7930667a | ||
|
|
7ff93d8809 | ||
|
|
dbf5e1dd7d | ||
|
|
ea8a93539e | ||
|
|
b6fb588599 | ||
|
|
db994eeee4 | ||
|
|
8f772daf86 | ||
|
|
89bea55100 | ||
|
|
8c5a5fc656 | ||
|
|
da6faba447 | ||
|
|
934e1df26f | ||
|
|
5750825c9e | ||
|
|
302d1c9ba0 | ||
|
|
568378fea8 | ||
|
|
20229ef10f | ||
|
|
40b01fd7c0 | ||
|
|
375febc48d | ||
|
|
39f298e9ed | ||
|
|
50cff5cef9 | ||
|
|
751fdea7ab | ||
|
|
5adcb42c71 | ||
|
|
62dbde1e25 | ||
|
|
307e8e40a8 | ||
|
|
f11c502f18 | ||
|
|
054ad70598 | ||
|
|
8501fe1114 | ||
|
|
4ad6246c54 | ||
|
|
2c7bb42907 | ||
|
|
2b018a7f65 | ||
|
|
810a76aba7 | ||
|
|
ce498893d3 | ||
|
|
8d7851a188 | ||
|
|
c339175477 | ||
|
|
21102e3de8 | ||
|
|
eb29821dcc | ||
|
|
752ac290c5 | ||
|
|
d47542ed0f | ||
|
|
b7d1ef676b | ||
|
|
65f74acba0 | ||
|
|
abb4cfb97c | ||
|
|
9d2ad41c52 | ||
|
|
9680760ae6 | ||
|
|
333bb7feb1 | ||
|
|
e7927fd86a | ||
|
|
9e46126c9e | ||
|
|
77f52558ba | ||
|
|
32a619a0b2 | ||
|
|
af807bb8e2 | ||
|
|
fa9f7b2f3e | ||
|
|
5fd3aabf72 | ||
|
|
125925fb3e | ||
|
|
a1404f6f4f | ||
|
|
90cadb4843 | ||
|
|
730b68b2db | ||
|
|
3d36d9286b | ||
|
|
ecf82eeaac | ||
|
|
0a4645769e | ||
|
|
a6d880b2c3 | ||
|
|
f5add914ef | ||
|
|
2cd540e3ec | ||
|
|
2ebbf02cfc | ||
|
|
b3bed8a1fe | ||
|
|
8ef3362cff | ||
|
|
9526d4c56e | ||
|
|
efaf48608c | ||
|
|
f42b7a83c3 | ||
|
|
24cd471db7 | ||
|
|
d69967af74 | ||
|
|
80beb88c4b | ||
|
|
bd36bcab25 | ||
|
|
8a006fc39f | ||
|
|
6eaabc4f4b | ||
|
|
02790f3e2a | ||
|
|
860d6e55b0 | ||
|
|
c64daabed6 | ||
|
|
53a4601de5 | ||
|
|
46879379b9 | ||
|
|
c17117d56b | ||
|
|
64d3941714 | ||
|
|
d1abb21511 | ||
|
|
2cdab92ba9 | ||
|
|
efabf671d0 | ||
|
|
3de6824156 | ||
|
|
cc3372d286 | ||
|
|
cbfacdac03 | ||
|
|
91e5c18f11 | ||
|
|
a0e1e0cb53 | ||
|
|
18f93d70f3 | ||
|
|
ea8a382448 | ||
|
|
eaa2d67c42 | ||
|
|
10d912ba25 | ||
|
|
d442090ffe | ||
|
|
52b1fc2f20 | ||
|
|
18aa8aba58 | ||
|
|
8ece6cfa59 | ||
|
|
3a7183a834 | ||
|
|
af655cafaf | ||
|
|
5a07a69839 | ||
|
|
8a0da4993c | ||
|
|
7ff5bdce7b | ||
|
|
7181494a86 | ||
|
|
79edfaf163 | ||
|
|
2fd3ce763a | ||
|
|
32c34fdfb4 | ||
|
|
1168210420 | ||
|
|
b289e4cbb5 | ||
|
|
a588a5c9e6 | ||
|
|
3f19c02f30 | ||
|
|
f34d746556 | ||
|
|
54af636d8f | ||
|
|
41c05cc614 | ||
|
|
183bae8583 | ||
|
|
6f190c8065 | ||
|
|
6260a0ce30 | ||
|
|
35df3bae7b | ||
|
|
26c5ee9de5 | ||
|
|
1cfde7f560 | ||
|
|
282f7a5606 | ||
|
|
1a148afcfb | ||
|
|
a91a91dc7c | ||
|
|
648d89ff28 | ||
|
|
489cc4ec36 | ||
|
|
fddd72c3a5 | ||
|
|
5abe2fb33e | ||
|
|
df12dbd57c | ||
|
|
9a3b054248 | ||
|
|
26246f141b | ||
|
|
8d1f84062d | ||
|
|
a7d31775f1 | ||
|
|
7c52b05d10 | ||
|
|
a7d7a96268 | ||
|
|
8e48dfe3d0 | ||
|
|
c89b35ff33 | ||
|
|
0d6ab59277 | ||
|
|
01bdc022ec | ||
|
|
06b0d4fb27 | ||
|
|
c751259319 | ||
|
|
b1113b8f5a | ||
|
|
efced03d83 | ||
|
|
e69a197135 | ||
|
|
90869ebfbb | ||
|
|
b9ccc733df | ||
|
|
0635497a47 | ||
|
|
8279332579 | ||
|
|
21dc086ebd | ||
|
|
b80888aca6 | ||
|
|
d59fbc0cd3 | ||
|
|
652823189c | ||
|
|
1f1437ce1d | ||
|
|
92aed28cea | ||
|
|
9da33ba49c | ||
|
|
616a76726d | ||
|
|
1a36dcc861 | ||
|
|
ff2bf94774 | ||
|
|
c33de8837b | ||
|
|
c96799c68d | ||
|
|
7dee0891f4 | ||
|
|
7d16e96feb | ||
|
|
37d5c4d7d0 | ||
|
|
2c6faf6f40 | ||
|
|
82cf839b7d | ||
|
|
eb2c8b5b88 | ||
|
|
c1936a7b7b | ||
|
|
a27bbac5c1 | ||
|
|
a8141b91db |
@@ -0,0 +1,16 @@
|
||||
# .coveragerc to control coverage.py
|
||||
[run]
|
||||
branch = True
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_lines =
|
||||
# Have to re-enable the standard pragma
|
||||
pragma: no cover
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
if 0:
|
||||
if __name__ == "__main__":
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help improve botty
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is it really a Bug report?**
|
||||
If you have questions regarding setup or feature information/requests, please consider asking in the Discord channel: https://discord.gg/Jf3J8cuXWg. The Github issues section is for bug reports only.
|
||||
|
||||
**Botty Version**
|
||||
What version of botty are you running? Are you using a pre-compiled release? Which one. Are you using the latest master branch? On which commit?
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. etc.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots / Video**
|
||||
If applicable, add screenshots or even better a short video of the error happening to help explain your problem. Botty often automatically makes screenshots when something goes wrong. Add these also.
|
||||
|
||||
**Logs**
|
||||
Add the relevant part of the log/log_(time).txt in this section. Either upload it as a fail or copy-paste the relevant part in here.
|
||||
|
||||
**Additional info**
|
||||
Add any other information about the problem here.
|
||||
@@ -0,0 +1,91 @@
|
||||
name: Botty - CI
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main, mine]
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Miniconda Python 3.10
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
python-version: '3.10'
|
||||
activate-environment: botty
|
||||
channel-priority: strict
|
||||
environment-file: environment.yml
|
||||
use-only-tar-bz2: true
|
||||
|
||||
- name: Download test assets
|
||||
shell: powershell
|
||||
run: |
|
||||
C:\Miniconda\condabin\conda.bat activate botty
|
||||
python -c "import utils.download_test_assets"
|
||||
|
||||
- name: Pytest
|
||||
shell: powershell
|
||||
run: |
|
||||
C:\Miniconda\condabin\conda.bat activate botty
|
||||
python -c "import sys; print(sys.version)"
|
||||
python -m pytest test/smoke_test.py test/nip/ test/game_stats_test.py test/closest_non_hud_test.py test/transmute/ -v -s
|
||||
|
||||
- name: Coverage
|
||||
shell: powershell
|
||||
run: |
|
||||
C:\Miniconda\condabin\conda.bat activate botty
|
||||
coverage xml
|
||||
|
||||
build:
|
||||
needs: test
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Miniconda Python 3.10
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
python-version: '3.10'
|
||||
activate-environment: botty
|
||||
channel-priority: strict
|
||||
environment-file: environment.yml
|
||||
use-only-tar-bz2: true
|
||||
|
||||
- name: Build exe
|
||||
shell: powershell
|
||||
env:
|
||||
BOTTY_NO_RENAME: '1'
|
||||
run: |
|
||||
C:\Miniconda\condabin\conda.bat activate botty
|
||||
python build.py --conda_path C:\Miniconda
|
||||
|
||||
- name: Prepare release zip
|
||||
shell: powershell
|
||||
run: |
|
||||
$BOTTY_DIR = Get-ChildItem -Directory -Name | Where-Object { $_ -match '^botty_v' } | Select-Object -First 1
|
||||
Write-Host "Botty dir: $BOTTY_DIR"
|
||||
Get-ChildItem -Path $BOTTY_DIR -Recurse | Select-Object -Property FullName, Length
|
||||
$ZIP = "${BOTTY_DIR}.zip"
|
||||
Compress-Archive -Path "${BOTTY_DIR}\*" -DestinationPath $ZIP -Force
|
||||
Write-Host "Release zip: $ZIP"
|
||||
Get-Item $ZIP | Select-Object -Property FullName, Length
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: botty-build
|
||||
path: botty_v*/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload to Release
|
||||
if: github.event_name == 'release'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
BOTTY_DIR=$(ls -d botty_v* | head -1)
|
||||
gh release upload "${{ github.event.release.tag_name }}" "${BOTTY_DIR}.zip" --clobber
|
||||
@@ -0,0 +1,37 @@
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
*_generated.py
|
||||
playground.py
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.spec
|
||||
dist/
|
||||
build/
|
||||
main.build/
|
||||
main.dist/
|
||||
main.onefile-build/
|
||||
generated/
|
||||
botty_v*/
|
||||
custom.ini
|
||||
config/custom.ini
|
||||
config/custom.*.ini
|
||||
.vscode
|
||||
.vs/
|
||||
test/assets/
|
||||
.venv
|
||||
*.bak
|
||||
.coverage
|
||||
htmlcov/
|
||||
coverage.xml
|
||||
utils/live-view/
|
||||
config/bnip/*
|
||||
!config/bnip/.gitkeep
|
||||
stats/
|
||||
info_screenshots/
|
||||
item_error_screenshots/
|
||||
loot_screenshots/
|
||||
pickit_screenshots/
|
||||
*info_log_parsed.txt
|
||||
*info_*.png
|
||||
*.log
|
||||
log/
|
||||
@@ -0,0 +1,87 @@
|
||||
# Botty Improvements Implementation Plan
|
||||
|
||||
## Status Legend
|
||||
- [ ] Not started
|
||||
- [~] In progress
|
||||
- [x] Done
|
||||
- [-] Cancelled / low priority
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Key Auto-Detection (Issue #940)
|
||||
Read D2R .key file and auto-fill hotkeys.
|
||||
|
||||
- [x] Create src/utils/key_detector.py module
|
||||
- [x] VK code mapping (partial — needs review for accuracy)
|
||||
- [x] Parse .key file (text format: VK action_type param)
|
||||
- [x] Auto-fill empty [char] hotkeys from detected bindings
|
||||
- [x] Auto-fill build-specific skill hotkeys (fohdin, hammerdin, etc.)
|
||||
- [x] Wire into config.py load_data()
|
||||
- [ ] REVIEW: Verify VK_MAP accuracy (D2R uses its own VK offset scheme)
|
||||
- [ ] REVIEW: Skill slot-to-config matching is heuristic — may misassign
|
||||
- [ ] TEST: Verify against actual D2R .key file on user's machine
|
||||
|
||||
## Phase 2: Target Detection False Positives (Issues #959/#964)
|
||||
Health bars and "immune to X" text mistaken for targets.
|
||||
|
||||
- [ ] Analyze current get_visible_targets() in target_detect.py
|
||||
- [ ] Add shape/size filtering: health bars are thin horizontal strips, immune text is small
|
||||
- [ ] Add aspect ratio check: real targets (poison/freeze auras) are roughly circular/elliptical
|
||||
- [ ] Add minimum bounding box height constraint (filter out thin text)
|
||||
- [ ] Optionally: add color temperature check (immune text is yellow/gold, not blue/green)
|
||||
- [ ] Test with screenshots of edge cases
|
||||
|
||||
## Phase 3: Pickit Timing Fix (Issue #939)
|
||||
Items skipped because bot teleports away before grabbing.
|
||||
|
||||
- [ ] Review pickit.py _yoink_item() for timing issues
|
||||
- [ ] Add configurable pickup_delay parameter (current: fixed timing)
|
||||
- [ ] Add retry logic: if item still visible after pickup attempt, re-try
|
||||
- [ ] Add "slow mode" for large/heavy items (framed/magic items may animate longer)
|
||||
- [ ] Ensure bot doesn't teleport until pickup animation completes
|
||||
- [ ] Test: verify no "Attempt to pick xyz" warnings followed by teleport
|
||||
|
||||
## Phase 4: Parallel Template Search (Issue #848)
|
||||
Speed up template_finder.search() with threading.
|
||||
|
||||
- [ ] Add ThreadPoolExecutor-based search_all_parallel()
|
||||
- [ ] Keep existing search() for single-template (no overhead)
|
||||
- [ ] Only parallelize when searching >3 templates simultaneously
|
||||
- [ ] Benchmark: measure speedup on typical 1280x720 grab
|
||||
|
||||
## Phase 5: Async Mouse Moves (Issue #955)
|
||||
Non-blocking mouse movement.
|
||||
|
||||
- [ ] Add async_move() to utils/custom_mouse.py
|
||||
- [ ] Run movement in background thread
|
||||
- [ ] Add is_moving() / wait_for_move() synchronization
|
||||
- [ ] Integrate into game_controller.py for smoother action chains
|
||||
|
||||
## Phase 6: Hardcore Chicken Loop Fix (Issue #942)
|
||||
Prevent infinite death loops on Hardcore characters.
|
||||
|
||||
- [ ] Review death_manager.py chicken logic
|
||||
- [ ] Add max_chicken_count config parameter (default: 3)
|
||||
- [ ] If max chicken count exceeded on HC, exit gracefully instead of re-entering
|
||||
- [ ] Add defensive chicken config option (chicken to TP instead of full chicken)
|
||||
- [ ] Test: verify HC character exits cleanly after N deaths
|
||||
|
||||
## Phase 7: Auto-Label NPCs (Issue #950)
|
||||
Learn vendor identities automatically during gameplay.
|
||||
|
||||
- [ ] During town states, detect NPC name plates via OCR
|
||||
- [ ] Cross-reference detected names with known NPC list
|
||||
- [ ] Auto-capture NPC templates when confidence is high
|
||||
- [ ] Store learned templates in assets/npc/
|
||||
- [ ] This is a long-term feature — lower priority
|
||||
|
||||
---
|
||||
|
||||
## Priority Order (implement in this order)
|
||||
1. **Phase 1** - Key auto-detection (already partially done, needs review + test)
|
||||
2. **Phase 3** - Pickit timing (high impact on loot collection)
|
||||
3. **Phase 2** - Target detection (high impact on kill reliability)
|
||||
4. **Phase 6** - Hardcore chicken fix (safety critical)
|
||||
5. **Phase 4** - Parallel template search (performance)
|
||||
6. **Phase 5** - Async mouse moves (quality of life)
|
||||
7. **Phase 7** - Auto-label NPCs (long-term feature)
|
||||
@@ -0,0 +1,561 @@
|
||||
# Quest Framework + Den of Evil Plan
|
||||
|
||||
## Goal
|
||||
Build a quest automation framework in botty that can interact with D2R NPCs, handle dialogue,
|
||||
track quest progress, and run Den of Evil as the first quest -- all usable by a low-level FoHdin.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
The quest framework is a new subsystem that plugs into the existing botty state machine.
|
||||
It follows the same patterns as existing runs (approach -> battle -> return to town) but adds
|
||||
NPC dialogue interaction and quest state persistence.
|
||||
|
||||
### New files
|
||||
|
||||
```
|
||||
src/quest/
|
||||
__init__.py # Exports
|
||||
quest_manager.py # Quest state machine + persistence (JSON)
|
||||
quest_dialogue.py # OCR-based NPC dialogue interaction
|
||||
quest_items.py # Quest item detection/pickup
|
||||
quest_combat.py # Lightweight combat wrapper (killing trash)
|
||||
a1/
|
||||
__init__.py
|
||||
q_den_of_evil.py # Den of Evil run
|
||||
```
|
||||
|
||||
### Modified files
|
||||
|
||||
```
|
||||
src/npc_manager.py # Add TOWN_MAIDEN NPC constant + templates
|
||||
src/pather.py # Add A1_ROARING_CANYON + DoE entrance locations
|
||||
src/bot.py # Add quest state, transitions, handler
|
||||
src/run/__init__.py # Export DenOfEvil
|
||||
src/town/a1.py # (optional) Add can_do_den_of_evil method
|
||||
config/params.ini # Add run_doe to [routes]
|
||||
config/bnip/ # Add town_maiden.png template
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation
|
||||
|
||||
### 1.1 `src/quest/quest_manager.py`
|
||||
|
||||
Purpose: Track which quests are done, persist between sessions, dispatch to quest modules.
|
||||
|
||||
```python
|
||||
class QuestManager:
|
||||
"""Manages quest state: tracks done/available quests per act, persists to JSON."""
|
||||
|
||||
# Quest definitions per act
|
||||
QUESTS = {
|
||||
"a1": ["den_of_evil"],
|
||||
"a2": [], # future: radament, horadric_staff, etc.
|
||||
...
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._state_file = "config/quest_state.json"
|
||||
self._state = self._load()
|
||||
|
||||
def is_done(self, quest_name: str) -> bool:
|
||||
return self._state.get(quest_name, False)
|
||||
|
||||
def mark_done(self, quest_name: str):
|
||||
self._state[quest_name] = True
|
||||
self._save()
|
||||
|
||||
def mark_all_done(self, act: str):
|
||||
for q in self.QUESTS.get(act, []):
|
||||
self._state[q] = True
|
||||
self._save()
|
||||
|
||||
def next_pending(self, act: str) -> str | None:
|
||||
for q in self.QUESTS.get(act, []):
|
||||
if not self.is_done(q):
|
||||
return q
|
||||
return None
|
||||
|
||||
def all_done(self, act: str) -> bool:
|
||||
return all(self._state.get(q, False) for q in self.QUESTS.get(act, []))
|
||||
|
||||
def _load(self) -> dict:
|
||||
if os.path.exists(self._state_file):
|
||||
with open(self._state_file) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def _save(self):
|
||||
with open(self._state_file, "w") as f:
|
||||
json.dump(self._state, f, indent=2)
|
||||
```
|
||||
|
||||
JSON format (config/quest_state.json):
|
||||
```json
|
||||
{
|
||||
"den_of_evil": true,
|
||||
"search_for_smith": true,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 `src/quest/quest_dialogue.py`
|
||||
|
||||
Purpose: Talk to NPCs, read dialogue options via OCR, click the right branch.
|
||||
This is the core of quest automation -- it makes the bot "converse" with NPCs.
|
||||
|
||||
```python
|
||||
class QuestDialogue:
|
||||
"""OCR-based NPC dialogue interaction for quest conversations."""
|
||||
|
||||
# ROI at 1280x720
|
||||
DIALOGUE_TEXT_ROI = (200, 470, 680, 100) # NPC speech text
|
||||
DIALOGUE_OPTIONS_ROI = (200, 560, 680, 140) # Player response buttons
|
||||
DIALOGUE_CLOSE_Y = 670 # Close button area
|
||||
|
||||
@staticmethod
|
||||
def open_dialogue(npc_name: str) -> bool:
|
||||
"""Walk to NPC and open their dialogue menu."""
|
||||
from npc_manager import Npc, open_npc_menu
|
||||
return open_npc_menu(getattr(Npc, npc_name.upper()))
|
||||
|
||||
@staticmethod
|
||||
def read_dialogue() -> dict:
|
||||
"""OCR the current dialogue box. Returns:
|
||||
{
|
||||
'npc_text': str, # What the NPC said
|
||||
'options': [str, ...], # Response options (may be empty if no choice)
|
||||
'has_continue': bool # True if just need to click continue
|
||||
}
|
||||
"""
|
||||
img = grab()
|
||||
npc_text = ocr_roi(img, self.DIALOGUE_TEXT_ROI)
|
||||
options_text = ocr_roi(img, self.DIALOGUE_OPTIONS_ROI)
|
||||
# Parse options: split by line, filter out empty, return list
|
||||
options = [line.strip() for line in options_text.split('\n') if line.strip()]
|
||||
has_continue = len(options) == 0 or "continue" in options_text.lower()
|
||||
return {
|
||||
'npc_text': npc_text.strip(),
|
||||
'options': options,
|
||||
'has_continue': has_continue
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def click_option(option_text: str) -> bool:
|
||||
"""Find and click a specific dialogue option by matching text via OCR.
|
||||
Searches the options ROI for a template match of the option text."""
|
||||
img = grab()
|
||||
options_img = cut_roi(img, self.DIALOGUE_OPTIONS_ROI)
|
||||
# Use template_finder or OCR to locate which button matches
|
||||
# Then click at that position
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def continue_dialogue() -> bool:
|
||||
"""Click the close/continue button to advance dialogue."""
|
||||
# Click in the close button area
|
||||
x, y, w, h = self.DIALOGUE_CLOSE_Y
|
||||
mouse.click at center of close area
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def follow_conversation(expected_options: list[str]) -> bool:
|
||||
"""Follow a multi-step conversation:
|
||||
- Read NPC text
|
||||
- If options present, click the expected one
|
||||
- If no options, click continue
|
||||
- Repeat until dialogue closes or unexpected text appears
|
||||
"""
|
||||
max_steps = 20 # Safety limit
|
||||
for i in range(max_steps):
|
||||
dialogue = self.read_dialogue()
|
||||
if not dialogue['has_continue'] and dialogue['options']:
|
||||
# We have a choice - click the expected option
|
||||
for opt in expected_options:
|
||||
if opt.lower() in ' '.join(dialogue['options']).lower():
|
||||
if not self.click_option(opt):
|
||||
return False
|
||||
break
|
||||
else:
|
||||
Logger.warning(f"Unexpected dialogue options: {dialogue['options']}")
|
||||
return False
|
||||
else:
|
||||
# Just continue
|
||||
if not self.continue_dialogue():
|
||||
return False
|
||||
wait(1.0, 1.5)
|
||||
# Check if dialogue box is still visible
|
||||
if not is_visible(ScreenObjects.NPCDialogue):
|
||||
return True # Done
|
||||
return False # Hit max steps
|
||||
```
|
||||
|
||||
Key design: `follow_conversation()` takes a list of expected response text. It will
|
||||
match against whatever options the NPC presents and click the right one. This handles
|
||||
multi-branch dialogues without hardcoding step-by-step clicks.
|
||||
|
||||
### 1.3 `src/quest/quest_combat.py`
|
||||
|
||||
Purpose: Lightweight combat for clearing trash during quests. Reuses existing char methods.
|
||||
|
||||
```python
|
||||
class QuestCombat:
|
||||
"""Combat helpers for quest areas -- reuses existing character combat logic."""
|
||||
|
||||
@staticmethod
|
||||
def clear_area(pather: Pather, char: IChar, path_nodes: list[int],
|
||||
timeout: float = 60) -> bool:
|
||||
"""Walk a path while killing monsters until timeout or all nodes cleared.
|
||||
This is the core of DoE: walk down, kill, walk back."""
|
||||
return pather.traverse_nodes(path_nodes, char, timeout=timeout, do_combat=True)
|
||||
|
||||
@staticmethod
|
||||
def wait_for_clear(char: IChar, timeout: float = 15) -> bool:
|
||||
"""Wait until no monsters are visible (area is clear)."""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
targets = get_visible_targets()
|
||||
if not targets or len(targets) == 0:
|
||||
return True
|
||||
# Attack if enemies present
|
||||
char.attack()
|
||||
wait(0.5)
|
||||
return False
|
||||
```
|
||||
|
||||
### 1.4 `src/quest/quest_items.py`
|
||||
|
||||
Purpose: Detect and pick up quest items (gold glow detection).
|
||||
|
||||
```python
|
||||
class QuestItems:
|
||||
"""Quest item detection and management."""
|
||||
|
||||
@staticmethod
|
||||
def detect_quest_items(img: np.ndarray) -> list[tuple[float, float]]:
|
||||
"""Detect gold-glowing items on screen (quest items).
|
||||
Returns list of (x, y) positions in monitor coords."""
|
||||
quest_item_mask, _ = color_filter(img, Config().colors.get("gold_glow", [
|
||||
(180, 140, 0), (255, 220, 80)
|
||||
]))
|
||||
# Find contours, return centers
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def pick_up_quest_items(char: IChar, img: np.ndarray = None) -> bool:
|
||||
"""Find and pick up any quest items currently visible."""
|
||||
if img is None:
|
||||
img = grab()
|
||||
items = self.detect_quest_items(img)
|
||||
for pos in items:
|
||||
char.pick_up_item(pos, item_name="Quest Item")
|
||||
wait(0.5)
|
||||
return len(items) > 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: NPC & Location additions
|
||||
|
||||
### 2.1 Add Town_Maiden to `src/npc_manager.py`
|
||||
|
||||
```python
|
||||
# In class Npc:
|
||||
TOWN_MAIDEN = "town_maiden" # Act 1, Roaring Canyon
|
||||
|
||||
# In _build_npcs():
|
||||
Npc.TOWN_MAIDEN: {
|
||||
"head": "town_maiden.png", # Need to capture template
|
||||
"actions": {} # No trade/identify - just dialogue
|
||||
}
|
||||
```
|
||||
|
||||
The Town Maiden sits in Roaring Canyon (eastern part of Act 1 town). She has a simple
|
||||
dialogue: you talk to her to "unlock" the Den of Evil entrance, then you talk to her
|
||||
again after clearing it to get the XP reward and reset it for another run.
|
||||
|
||||
### 2.2 Add locations to `src/pather.py`
|
||||
|
||||
```python
|
||||
class Location:
|
||||
# ... existing locations ...
|
||||
|
||||
# Act 1 Roaring Canyon / Den of Evil
|
||||
A1_ROARING_CANYON = "a1_roaring_canyon" # Town area where Maiden is
|
||||
A1_DEN_OF_EVIL_ENTRANCE = "a1_doe_entrance" # Stairs down to DoE
|
||||
A1_DEN_LEVEL_1 = "a1_doe_level_1"
|
||||
A1_DEN_LEVEL_2 = "a1_doe_level_2"
|
||||
A1_DEN_LEVEL_3 = "a1_doe_level_3"
|
||||
A1_DEN_LEVEL_4 = "a1_doe_level_4"
|
||||
# (DoE has 3-5 levels depending on game version - need to confirm)
|
||||
```
|
||||
|
||||
Path nodes will need to be added for the Roaring Canyon area and each DoE level.
|
||||
These are captured via quest_debug.py by walking the path and recording waypoints.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Den of Evil run module
|
||||
|
||||
### 3.1 `src/quest/a1/q_den_of_evil.py`
|
||||
|
||||
```python
|
||||
class DenOfEvil:
|
||||
"""Den of Evil run - Act 1 repeatable quest for XP.
|
||||
|
||||
Flow:
|
||||
1. Ensure character is in Act 1
|
||||
2. Walk to Roaring Canyon (Town Maiden)
|
||||
3. Talk to Town Maiden (unlock entrance if needed)
|
||||
4. Enter Den of Evil
|
||||
5. Pre-buff (FoH + Conviction for FoHdin)
|
||||
6. Walk through each level, killing trash
|
||||
7. Exit back to Roaring Canyon
|
||||
8. Talk to Town Maiden again for reward
|
||||
9. Return to town center
|
||||
"""
|
||||
|
||||
name = "run_doe"
|
||||
|
||||
# Path nodes per level (to be filled in via quest_debug.py)
|
||||
LEVEL_PATHS = {
|
||||
1: [], # Entrance to level 1 stairs
|
||||
2: [], # Level 1 to level 2
|
||||
3: [], # Level 2 to level 3
|
||||
4: [], # Level 3 to level 4 (or final area)
|
||||
}
|
||||
|
||||
def __init__(self, pather, town_manager, char, pickit, runs):
|
||||
self._pather = pather
|
||||
self._town_manager = town_manager
|
||||
self._char = char
|
||||
self._pickit = pickit
|
||||
self._runs = runs
|
||||
self._quest_manager = QuestManager()
|
||||
self._dialogue = QuestDialogue()
|
||||
|
||||
def approach(self, curr_loc: Location, do_buff: bool) -> Location | bool:
|
||||
"""Get to Roaring Canyon and talk to Town Maiden."""
|
||||
Logger.info("Run Den of Evil")
|
||||
|
||||
# Ensure we're in Act 1
|
||||
if TownManager.get_act_from_location(curr_loc) != Location.A1_TOWN_START:
|
||||
curr_loc = self._town_manager.go_to_act(1, curr_loc)
|
||||
if not curr_loc:
|
||||
return False
|
||||
|
||||
# Walk to Roaring Canyon (Town Maiden area)
|
||||
if not self._pather.traverse_nodes(
|
||||
(curr_loc, Location.A1_ROARING_CANYON), self._char, force_move=True
|
||||
):
|
||||
return False
|
||||
|
||||
# Talk to Town Maiden to unlock/open the Den
|
||||
if not self._dialogue.open_dialogue("town_maiden"):
|
||||
return False
|
||||
|
||||
# Follow the conversation (expect "Oh no, not again" or similar)
|
||||
if not self._dialogue.follow_conversation(["Tell me more", "I'll help you"]):
|
||||
return False
|
||||
|
||||
# Enter the Den
|
||||
if not self._pather.traverse_nodes(
|
||||
(Location.A1_ROARING_CANYON, Location.A1_DEN_OF_EVIL_ENTRANCE),
|
||||
self._char, force_move=True
|
||||
):
|
||||
return False
|
||||
|
||||
return Location.A1_DEN_OF_EVIL_ENTRANCE
|
||||
|
||||
def battle(self, do_pre_buff: bool) -> bool | tuple[Location, bool]:
|
||||
"""Fight through the Den of Evil."""
|
||||
# Pre-buff
|
||||
if do_pre_buff:
|
||||
if not self._char.pre_buff():
|
||||
return False
|
||||
|
||||
# Clear each level
|
||||
for level in sorted(self.LEVEL_PATHS.keys()):
|
||||
Logger.info(f"Clearing Den of Evil level {level}")
|
||||
if not self._pather.traverse_nodes(
|
||||
self.LEVEL_PATHS[level], self._char, timeout=120, do_combat=True
|
||||
):
|
||||
Logger.error(f"Failed to clear DoE level {level}")
|
||||
return False
|
||||
|
||||
# Pick up any quest items / loot
|
||||
self._pickit.pick_up_items(self._char)
|
||||
QuestItems.pick_up_quest_items(self._char)
|
||||
|
||||
# Walk back to Roaring Canyon
|
||||
if not self._pather.traverse_nodes(
|
||||
(Location.A1_DEN_OF_EVIL_ENTRANCE, Location.A1_ROARING_CANYON),
|
||||
self._char, force_move=True
|
||||
):
|
||||
return False
|
||||
|
||||
# Talk to Town Maiden for reward
|
||||
if not self._dialogue.open_dialogue("town_maiden"):
|
||||
return False
|
||||
if not self._dialogue.follow_conversation(["Yes", "Thank you"]):
|
||||
Logger.warning("Failed to collect DoE reward from Town Maiden")
|
||||
|
||||
# Mark as done (for non-repeatable quests) or just return success
|
||||
# Note: DoE is repeatable once per real-day, so we DON'T mark permanently done
|
||||
# self._quest_manager.mark_done("den_of_evil") # Only if non-repeatable
|
||||
|
||||
return (Location.A1_ROARING_CANYON, True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Bot integration
|
||||
|
||||
### 4.1 `src/bot.py` changes
|
||||
|
||||
```python
|
||||
# Add import
|
||||
from quest.a1.q_den_of_evil import DenOfEvil
|
||||
|
||||
# In __init__:
|
||||
self._do_runs["run_doe"] = Config().routes.get("run_doe")
|
||||
|
||||
self._doe = DenOfEvil(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
|
||||
# In _states list:
|
||||
# (No new state needed - DoE uses the existing pattern: town -> doe -> end_run -> town)
|
||||
|
||||
# In _transitions list (add):
|
||||
{ 'trigger': 'run_doe', 'source': 'town', 'dest': 'doe', 'before': "on_run_doe" },
|
||||
|
||||
# Add 'doe' to end_run source list:
|
||||
{ 'trigger': 'end_run', 'source': [..., 'doe'], 'dest': 'town', 'before': "on_end_run" },
|
||||
|
||||
# Add end_game source:
|
||||
{ 'trigger': 'end_game', 'source': [..., 'doe'], 'dest': 'initialization', 'before': "on_end_game" },
|
||||
|
||||
# Add handler method:
|
||||
def on_run_doe(self):
|
||||
res = False
|
||||
self._do_runs["run_doe"] = False
|
||||
self._game_stats.update_location("DoE")
|
||||
self._curr_loc = self._doe.approach(self._curr_loc, not self._pre_buffed)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
res = self._doe.battle(not self._pre_buffed)
|
||||
self._ending_run_helper(res)
|
||||
```
|
||||
|
||||
### 4.2 `src/run/__init__.py` changes
|
||||
|
||||
```python
|
||||
# No change needed if DoE lives in src/quest/ (not src/run/)
|
||||
# But if we want consistency, add:
|
||||
from quest.a1.q_den_of_evil import DenOfEvil
|
||||
```
|
||||
|
||||
### 4.3 `config/params.ini` changes
|
||||
|
||||
```ini
|
||||
[routes]
|
||||
; ... existing runs ...
|
||||
; run_doe (Act 1 Den of Evil - repeatable daily XP)
|
||||
order=run_doe
|
||||
```
|
||||
|
||||
### 4.4 `config/params.ini` FoHdin config
|
||||
|
||||
For a lvl 1 Paladin running DoE, the params.ini needs:
|
||||
|
||||
```ini
|
||||
[char]
|
||||
type=fohdin
|
||||
...
|
||||
|
||||
[fohdin]
|
||||
; FoHdin-specific config for low-level DoE runs
|
||||
teleport=
|
||||
; No teleport at lvl 1-9, so pathing is on foot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Testing workflow
|
||||
|
||||
### What needs user input (I cannot see D2R):
|
||||
|
||||
1. **Capture Town_Maiden template:**
|
||||
- Go to Roaring Canyon in Act 1
|
||||
- Stand near the Town Maiden
|
||||
- Run `quest_debug.py`, press F4 (NPC detection)
|
||||
- Paste output so I can save the template
|
||||
|
||||
2. **Capture DoE path nodes:**
|
||||
- Enter the Den of Evil
|
||||
- Run `quest_debug.py`, press F1 at each waypoint
|
||||
- Walk from entrance through each level
|
||||
- Paste outputs so I can build the path arrays
|
||||
|
||||
3. **Capture dialogue:**
|
||||
- Talk to Town Maiden (both before and after clearing)
|
||||
- Run `quest_debug.py`, press F2 (dialogue OCR)
|
||||
- Paste output so I can code the conversation flow
|
||||
|
||||
4. **Test run:**
|
||||
- After I write the code, you run botty with `run_doe` in the route order
|
||||
- Report what happens / paste terminal output
|
||||
- I iterate based on results
|
||||
|
||||
### Lvl 1 Paladin specifics:
|
||||
|
||||
- **FoHdin requires FOH skill lvl 6 for Feign of Life passive** -- this needs 3 skill points
|
||||
in FoH, meaning character level 9 minimum (or level 4 with a +1 skill weapon)
|
||||
- Before reaching lvl 9, the bot can still run DoE but will be much more fragile
|
||||
- Recommended: manually level Paladin to ~lvl 4-5 (short runs in Area 1 or 2) before
|
||||
letting the bot solo DoE with FoH
|
||||
- The bot pathing should handle the walk-through at low speed with heavy FoH spam
|
||||
|
||||
---
|
||||
|
||||
## Implementation order
|
||||
|
||||
1. Write `quest_manager.py` (simple JSON state tracker)
|
||||
2. Write `quest_dialogue.py` (OCR-based NPC interaction)
|
||||
3. Write `quest_items.py` + `quest_combat.py` (lightweight helpers)
|
||||
4. Add Town_Maiden NPC to npc_manager.py
|
||||
5. Write `q_den_of_evil.py` (skeleton with placeholder paths)
|
||||
6. Integrate into bot.py (state, transitions, handler)
|
||||
7. Update params.ini
|
||||
8. **USER TESTS** -- captures templates, paths, dialogue
|
||||
9. I fill in the actual path nodes and dialogue based on your captures
|
||||
10. Full test run and iterate
|
||||
|
||||
---
|
||||
|
||||
## File tree after implementation
|
||||
|
||||
```
|
||||
my-botty/
|
||||
├── config/
|
||||
│ ├── params.ini # Modified: +run_doe in routes
|
||||
│ ├── quest_state.json # New: auto-created by QuestManager
|
||||
│ └── bnip/
|
||||
│ └── town_maiden.png # New: captured template
|
||||
├── src/
|
||||
│ ├── quest/ # New directory
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── quest_manager.py
|
||||
│ │ ├── quest_dialogue.py
|
||||
│ │ ├── quest_items.py
|
||||
│ │ ├── quest_combat.py
|
||||
│ │ └── a1/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── q_den_of_evil.py
|
||||
│ ├── npc_manager.py # Modified: +TOWN_MAIDEN
|
||||
│ ├── pather.py # Modified: +A1_ROARING_CANYON, +A1_DEN_* locations
|
||||
│ ├── bot.py # Modified: +doe state, transitions, handler
|
||||
│ └── run/__init__.py # Modified: +DenOfEvil export
|
||||
```
|
||||
@@ -0,0 +1,231 @@
|
||||
# Botty Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Botty is an autonomous bot for Diablo II Resurrected (D2R) that uses computer vision and native Windows API input to run endgame content without kernel drivers. It's built in Python 3.10 and distributed as a PyInstaller onefile executable.
|
||||
|
||||
## Entry Points
|
||||
|
||||
Two standalone executables are built from the same source:
|
||||
|
||||
| Executable | Source | Purpose |
|
||||
|---|---|---|
|
||||
| `main.exe` | `src/main.py` | Main bot controller |
|
||||
| `shop.exe` | `src/shopper.py` | Standalone vendor shopping |
|
||||
|
||||
## Threading Model
|
||||
|
||||
main.py spawns three concurrent threads:
|
||||
|
||||
```
|
||||
main.py (main thread)
|
||||
├── game_controller.py (bot loop thread)
|
||||
│ └── bot.py (state machine thread)
|
||||
├── health_manager.py (background thread)
|
||||
└── death_manager.py (background thread)
|
||||
```
|
||||
|
||||
- **Main thread**: Shows UI, registers hotkeys (F7-F12), calls `keyboard.wait()` to block
|
||||
- **Game Controller**: Orchestrates the bot loop - starts/stops the bot thread, handles game recovery, tracks stats
|
||||
- **Health Manager**: Polls health/mana/merc every ~1s, auto-potions, chickens when low
|
||||
- **Death Manager**: Polls every ~1s for "You Have Died" screen, triggers recovery
|
||||
|
||||
## Core Modules
|
||||
|
||||
### Input Layer (`src/input_layer/`)
|
||||
|
||||
Native Windows API input replacement (no kernel drivers):
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `win_input.py` | ctypes wrappers for `SendInput`, `GetAsyncKeyState`, `GetCursorPos` |
|
||||
| `mouse_impl.py` | Humanized mouse with Bezier curves, Gaussian distortion, endpoint wobble |
|
||||
| `hotkey.py` | Polling-based hotkey manager (replaces `keyboard.add_hotkey`) |
|
||||
| `__init__.py` | Drop-in API: `from input_layer import keyboard, mouse` |
|
||||
|
||||
All key presses include stealth micro-pauses and variable press duration automatically.
|
||||
|
||||
### Screen Capture (`src/screen.py`)
|
||||
|
||||
Handles D2R window detection and screenshot capture via MSS library. Converts between coordinate systems: Monitor (top-left first monitor), Screen (per-monitor), Absolute (character center), Relative (template-matched).
|
||||
|
||||
### Image Recognition (`src/template_finder.py`)
|
||||
|
||||
Template matching via OpenCV (`cv2.matchTemplate`). Searches against pre-captured asset templates in `assets/templates/`. Returns match position and validity.
|
||||
|
||||
### UI Detection (`src/ui/`)
|
||||
|
||||
| File | Detects |
|
||||
|---|---|
|
||||
| `main_menu.py` | Main menu buttons (play, save & exit) |
|
||||
| `character_select.py` | Character portraits and selection |
|
||||
| `skills.py` | Skill bar state, skill availability |
|
||||
| `meters.py` | Health/mana globes, potion charges |
|
||||
| `player_bar.py` | Player status bar, XP bar |
|
||||
| `view.py` | Minimap, inventory screen detection |
|
||||
| `waypoint.py` | Waypoint portal selection |
|
||||
| `error_screens.py` | Error dialogs, death screen |
|
||||
| `loading.py` | Loading screen detection |
|
||||
|
||||
### Pathing (`src/pather.py`)
|
||||
|
||||
Pathfinding via reference template matching with relative coordinates. Each location has numbered nodes with template references. The `Location` class defines named destinations (towns, dungeons). Path definitions map (start, end) pairs to node sequences.
|
||||
|
||||
### Character System (`src/char/`)
|
||||
|
||||
Inheritance hierarchy:
|
||||
|
||||
```
|
||||
IChar (abstract base)
|
||||
├── Basic / Basic_Ranged
|
||||
├── Paladin
|
||||
│ ├── Hammerdin
|
||||
│ └── FoHdin
|
||||
├── Sorceress (base for all sorc builds)
|
||||
│ ├── BlizzSorc
|
||||
│ ├── BlizzorbSorc
|
||||
│ ├── NovaSorc
|
||||
│ ├── LightSorc
|
||||
│ └── HydraSorc
|
||||
├── Amazon
|
||||
│ └── Javazon
|
||||
├── Trapsin
|
||||
├── Barbarian
|
||||
├── Necro
|
||||
├── Poison_Necro
|
||||
├── Bone_Necro
|
||||
└── Warlock
|
||||
├── FireLock
|
||||
├── EchoLock
|
||||
└── AbyssLock
|
||||
```
|
||||
|
||||
Each character class implements: run methods, skill usage, attack patterns, position-specific behavior.
|
||||
|
||||
### Run System (`src/run/`)
|
||||
|
||||
| File | Run |
|
||||
|---|---|
|
||||
| `pindle.py` | Pindle of Purity |
|
||||
| `shenk_eld.py` | Shenk + Eldritch |
|
||||
| `trav.py` | Travincal |
|
||||
| `nihlathak.py` | Nihlathak's Temple |
|
||||
| `arcane.py` | Arcane Sanctuary |
|
||||
| `diablo.py` | Chaos Sanctuary + Diablo |
|
||||
| `vizier.py` | Vizier (Seal Boss) |
|
||||
| `baal.py` | Baal |
|
||||
| `mephisto.py` | Mephisto |
|
||||
| `andariel.py` | Andariel |
|
||||
| `countess.py` | Countess |
|
||||
| `level.py` | General leveling runs |
|
||||
|
||||
### Town System (`src/town/`)
|
||||
|
||||
`TownManager` orchestrates act-specific town routines. Each act (A1-A5) has its own module with NPC interactions, waypoint usage, and stash handling.
|
||||
|
||||
### Inventory (`src/inventory/`)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `belt.py` | Belt slot management, potion detection |
|
||||
| `personal.py` | Personal inventory grid, item positions |
|
||||
| `vendor.py` | Vendor trade window |
|
||||
| `stash.py` | Personal stash, shared stash |
|
||||
| `cube.py` | Horadric Cube |
|
||||
| `common.py` | Shared inventory utilities |
|
||||
|
||||
### Item Recognition (`src/item/`)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `pickit.py` | Pickup decisions, item filtering |
|
||||
| `consumables.py` | Potion and consumable identification |
|
||||
|
||||
BNIP parser (`src/bnip/`) handles item filter rules from `.bnip` files.
|
||||
|
||||
### Transmute (`src/transmute/`)
|
||||
|
||||
Gem transmutation: collects gems from inventory/stash, performs cube recipes, manages stash destinations.
|
||||
|
||||
### Messages (`src/messages/`)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `messenger.py` | Message dispatcher |
|
||||
| `discord_embeds.py` | Discord webhook formatting |
|
||||
| `generic_api.py` | Generic HTTP webhook support |
|
||||
|
||||
### Configuration (`src/config.py`)
|
||||
|
||||
Singleton that merges config files in priority order:
|
||||
`custom.ini` > `params.ini` > `game.ini` > `shop.ini` > `transmute.ini`
|
||||
|
||||
Supports variable substitution via `[variables]` sections.
|
||||
|
||||
### Stealth (`src/utils/stealth.py`)
|
||||
|
||||
Three-tier stealth system:
|
||||
- **Tier 1 (Input)**: Micro-pauses, click variance, key press duration, endpoint wobble
|
||||
- **Tier 2 (Behavior)**: Wrong waypoint chance, skill mistake chance, skill hesitation
|
||||
- **Tier 3 (Session)**: AFK breaks, run skipping, personality seed per character
|
||||
|
||||
### Utilities (`src/utils/`)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `misc.py` | Window management, DPI awareness, timing utilities |
|
||||
| `restart.py` | Game launch/kill, D2R always-on-top |
|
||||
| `auto_settings.py` | D2R game settings adjustment |
|
||||
| `key_detector.py` | Auto-detect skill bindings from `.key`/`.keyo` files |
|
||||
| `graphic_debugger.py` | Visual debugging overlay |
|
||||
| `node_recorder.py` | Record new path nodes |
|
||||
| `stealth.py` | Stealth behavior randomization |
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
D2R Game Window
|
||||
│
|
||||
▼
|
||||
screen.grab() ──→ Image (numpy array)
|
||||
│
|
||||
▼
|
||||
template_finder.search() ──→ Position + Validity
|
||||
│
|
||||
▼
|
||||
ui_manager.detect_screen_object() ──→ Current UI state
|
||||
│
|
||||
▼
|
||||
bot.py (state machine) ──→ Decides next action
|
||||
│
|
||||
▼
|
||||
char.run() / pather.go_to() ──→ Movement + Interaction
|
||||
│
|
||||
▼
|
||||
input_layer.keyboard.send() / mouse.click() ──→ SendInput API
|
||||
│
|
||||
▼
|
||||
D2R Game Window (response captured on next grab)
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
- **Build script**: `build.py` (PyInstaller with `--onefile --noconsole`)
|
||||
- **Dependencies**: `pyproject.toml`
|
||||
- **Distribution**: Random exe name, no console window, no kernel drivers
|
||||
- **Assets**: `assets/` directory copied to build output
|
||||
|
||||
## Key Detection
|
||||
|
||||
At startup, `key_detector.py` reads the character's `.key`/`.keyo` file from `Saved Games/Diablo II Resurrected/` to auto-detect skill bindings and non-skill keys (inventory, show items, etc.). This eliminates manual key configuration.
|
||||
|
||||
## Coordinate Systems
|
||||
|
||||
| System | Origin | Used By |
|
||||
|---|---|---|
|
||||
| Monitor | Top-left of first monitor | `screen.grab()` |
|
||||
| Screen | Per-monitor client area | UI detection |
|
||||
| Absolute | Character at screen center | Pathing, target detection |
|
||||
| Relative | Template match position | Inventory grid, NPC interaction |
|
||||
|
||||
Conversion functions in `screen.py`: `convert_monitor_to_screen()`, `convert_screen_to_abs()`, etc.
|
||||
@@ -0,0 +1,427 @@
|
||||
# Botty D2R - Improvement Ideas
|
||||
|
||||
Generated: 2026-05-20 | Status: Partially implemented
|
||||
Based on analysis of ~100+ source files across src/, test/, config/, and assets/.
|
||||
|
||||
Character: FOH Paladin | Priority: Anti-cheat stealth > everything else
|
||||
|
||||
---
|
||||
|
||||
## DONE
|
||||
|
||||
- [x] **C8** Bug: personal.py `open()` -> `open_inventory()` (fixed)
|
||||
- [x] **C9** Bug: FoHdin missing PickIt (fixed)
|
||||
- [x] **C10** Bug: game_controller race condition - `game_stats` now in `__init__()` (fixed)
|
||||
- [x] **M15** Bug: TARGET_ASPECT_MIN duplicate removed (fixed)
|
||||
- [x] **M17** Bug: chest.py relative path -> `Path(__file__)` absolute (fixed)
|
||||
- [x] **Quick win**: All 47 bare `time.sleep()` replaced with `wait()` (15 files fixed)
|
||||
- [x] **Quick win**: Stealth fallback `Logger.warning()` added to mouse_impl.py (3 blocks)
|
||||
- [x] **Quick win**: `requirements.txt` created (120 lines, 20+ deps)
|
||||
- [x] **Quick win**: `ruff.toml` created (Python 3.10, line-length 120)
|
||||
- [x] **Tool**: `asset_manager.py` created (inventory, search, key, audit, quality, similarity, capture, crop, auto_crop, validate, cleanup, batch)
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL - DO FIRST
|
||||
|
||||
### C1. Stealth: Consolidate all timing through centralized wait()
|
||||
|
||||
Many places use bare `time.sleep()` instead of `utils.misc.wait()` (which has Gaussian jitter).
|
||||
Every direct sleep creates a predictable timing signature detectable by anti-cheat.
|
||||
|
||||
Files with bare `time.sleep()`:
|
||||
- health_manager.py line 14
|
||||
- chest.py
|
||||
- pather.py
|
||||
- game_recovery.py
|
||||
- npc_manager.py
|
||||
- bot.py
|
||||
|
||||
Fix: Replace all bare `time.sleep(n)` with `wait(n, n*1.2)` for human-like jitter.
|
||||
|
||||
### C2. Stealth: Add variable typing rhythm
|
||||
|
||||
`win_input.py` `send_text()` uses fixed `0.05` per character.
|
||||
Real humans type with 0.02-0.12 per character with variation.
|
||||
|
||||
Fix: Add per-character randomization to `send_text()`.
|
||||
|
||||
### C3. Stealth: Human curve complexity should adapt to distance
|
||||
|
||||
`mouse_impl.py` `HumanCurve` uses static parameters.
|
||||
Short movements (teleport to adjacent tile) should be simpler/faster.
|
||||
Long movements should have more complex arcs.
|
||||
|
||||
Fix: Add distance-to-complexity mapping in `HumanCurve.__init__()`.
|
||||
|
||||
### C4. Stealth: Variable pathing speed within single movement arc
|
||||
|
||||
Current implementation varies timing BETWEEN movements but not WITHIN one.
|
||||
Real humans speed up and slow down during a single mouse arc.
|
||||
|
||||
Fix: Add per-segment timing variation in `HumanCurve` execution loop.
|
||||
|
||||
### C5. Stealth: Extend endpoint wobble to ALL click sequences
|
||||
~~`mouse_impl.py` `endpoint_wobble()` only fires for `stealth_move()`.~~
|
||||
~~Most paths use regular `mouse.move()` + `mouse.click()` with no wobble.~~
|
||||
~~Fix: Make `stealth_move()` the default, or add wobble to regular click flow.~~
|
||||
(Still needed - stealth fallback logging added but wobble not yet extended to all clicks.)
|
||||
|
||||
### C6. ~~Stealth: Screen capture timing jitter~~
|
||||
~~`screen.py` uses `dxcam` with perfectly regular capture intervals (~80ms).~~
|
||||
~~Anti-cheat can detect this regular polling pattern.~~
|
||||
~~Fix: Add 5-10% jitter to grab timing intervals.~~
|
||||
(Done - all timing now routes through `wait()` with jitter.)
|
||||
|
||||
### C7. ~~Stealth: Input event spacing jitter~~
|
||||
~~`hotkey.py` `GetAsyncKeyState` polling runs at exactly 50Hz.~~
|
||||
~~Real human keyboard polling is variable.~~
|
||||
~~Fix: Add microsecond-level jitter to polling intervals.~~
|
||||
(Done - all `time.sleep()` in hotkey.py replaced with `wait()` calls.)
|
||||
|
||||
### C8. ~~Bug: Fix personal.py open() shadowing (line 60)~~ ~~(DONE)~~
|
||||
|
||||
### C9. ~~Bug: Fix FoHdin missing PickIt (bot.py line 72)~~ ~~(DONE)~~
|
||||
|
||||
### C10. ~~Bug: Replace thread killing with cooperative shutdown~~
|
||||
~~`utils.misc.kill_thread()` uses `PyThreadState_SetAsyncExc` (CPython private API).~~
|
||||
~~This can leave locks in inconsistent state, cause GIL issues, or corrupt numpy arrays.~~
|
||||
~~Fix: Replace with threading.Event flags for cooperative shutdown.~~
|
||||
(Still needs doing - this is the most dangerous remaining bug.)
|
||||
|
||||
---
|
||||
|
||||
## HIGH PRIORITY
|
||||
|
||||
### H1. Stealth: Add AFK countermeasures during ALL idle states
|
||||
|
||||
`stealth.py` `maybe_afk_break()` only runs during run transitions.
|
||||
Should also run during health monitoring idle periods and town states.
|
||||
|
||||
Fix: Add micro mouse adjustments during health/death manager idle loops.
|
||||
|
||||
### H2. Stealth: Add "thinking" pauses before major actions
|
||||
|
||||
Before executing major actions (entering waypoint, starting run), add 0.5-3s "deliberation" pause.
|
||||
Humans plan before acting.
|
||||
|
||||
Fix: Add configurable pause in `bot.py` state transitions.
|
||||
|
||||
### H3. Stealth: Randomize action order in town
|
||||
|
||||
Bot always does: belt update -> stash -> waypoint (same order every time).
|
||||
Real players vary their town routine.
|
||||
|
||||
Fix: Randomize which town tasks are performed first in `town_manager.py`.
|
||||
|
||||
### H4. Stealth: Add variable "reaction time" to potion drinking
|
||||
|
||||
When health drops, bot should NOT react instantly.
|
||||
Add 100-500ms delay to potion drinking to simulate human reaction time.
|
||||
|
||||
Fix: Add reaction delay in `health_manager.py` before invoking heal skill.
|
||||
|
||||
### H5. Stealth: Mouse trail entropy / Brownian overlay
|
||||
|
||||
Record actual mouse path during movements and add small random perturbations
|
||||
that accumulate over time. Makes movement traces look more human.
|
||||
|
||||
Fix: Add Brownian motion overlay in `mouse_impl.py` `HumanCurve`.
|
||||
|
||||
### H6. Stealth: Click position micro-jitter
|
||||
|
||||
Before and after every click, add a tiny 1-3 pixel random offset.
|
||||
Real humans don't click at exactly the same coordinates.
|
||||
|
||||
Fix: Add jitter in `mouse_impl.py` click methods.
|
||||
|
||||
### H7. Performance: Pre-load templates into memory at startup
|
||||
|
||||
`template_finder.py` loads PNGs with `cv2.imread()` on each search call.
|
||||
With 1111+ templates, this wastes CPU on every detection cycle.
|
||||
|
||||
Current: Has `@cache` on `stored_templates()` but cache is never invalidated.
|
||||
Fix: Good as-is if cache works. Verify cache isn't being bypassed in hot paths.
|
||||
|
||||
### H8. Performance: Cache screen grabs in tight pathing loops
|
||||
|
||||
`pather.py` `traverse_nodes()` calls `grab(force_new=True)` for every node.
|
||||
If retrying quickly, the frame won't have changed.
|
||||
|
||||
Fix: Cache last grab timestamp; skip if less than 16ms since last grab.
|
||||
|
||||
### H9. Performance: Convert BGR->HSV once, apply all color masks
|
||||
|
||||
`utils.misc.color_filter()` converts BGR->HSV for every filter range.
|
||||
With multiple NPC templates, this creates redundant conversions.
|
||||
|
||||
Fix: Convert once, apply all masks from single HSV image.
|
||||
|
||||
### H10. Architecture: Split pather.py (750 lines)
|
||||
|
||||
Handles node definition, path data, traversal, offset management, AND debug main.
|
||||
|
||||
Split into:
|
||||
- path_data.py (constants and route definitions)
|
||||
- path_traversal.py (traverse_nodes logic)
|
||||
- path_utils.py (node offset helpers)
|
||||
|
||||
### H11. Architecture: Split config.py (29,500 chars)
|
||||
|
||||
Loads, validates, and caches all configuration in one massive file.
|
||||
|
||||
Split into:
|
||||
- config_loader.py (read params.ini / config.yaml)
|
||||
- config_schema.py (Pydantic/dataclass validation)
|
||||
- config_defaults.py (default values per section)
|
||||
|
||||
### H12. Architecture: Encapsulate health_manager global state
|
||||
|
||||
Uses module-level globals `pause_state` and `panel_check_paused` with getter/setter functions.
|
||||
Should be encapsulated in `HealthManager` class instance for thread safety.
|
||||
|
||||
### H13. Architecture: Fix npc_manager deferred init fragility
|
||||
|
||||
`npcs` dict is populated in try/except at module level, silently staying empty on failures.
|
||||
If templates load late, `open_npc_menu()` crashes with unhelpful KeyError.
|
||||
|
||||
Fix: Add explicit initialization check with clear error message.
|
||||
|
||||
### H14. Architecture: Add thread safety to shared state
|
||||
|
||||
`health_manager.py` and `death_manager.py` share `set_pause_state` without mutex/lock.
|
||||
Bot uses `self._stash_mutex` for stashing but health/death managers don't.
|
||||
|
||||
Fix: Add threading.Lock around shared state access.
|
||||
|
||||
### H15. Configuration: Add schema validation to params.ini
|
||||
|
||||
A typo in any key name causes runtime crash deep in the call stack.
|
||||
|
||||
Fix: Use Pydantic or dataclasses with defaults and type hints.
|
||||
|
||||
### H16. Configuration: Eliminate config duplication
|
||||
|
||||
Default values are scattered across params.ini AND config.py with hardcoded fallbacks.
|
||||
|
||||
Fix: Single source of truth - either params.ini with config.py as schema only,
|
||||
or config.py with params.ini as user overrides.
|
||||
|
||||
### H17. Configuration: Add inline config documentation
|
||||
|
||||
No comments explaining what each param does, valid ranges, or D2R version compatibility.
|
||||
|
||||
Fix: Add docstrings to config sections with examples and ranges.
|
||||
|
||||
### H18. Asset management: Add startup health check
|
||||
|
||||
If a template file is missing or corrupted, the bot crashes mid-run.
|
||||
|
||||
Fix: Add startup validation that checks all required templates exist and load.
|
||||
|
||||
### H19. Asset management: Compress PNG templates with lossless optimization
|
||||
|
||||
1111+ PNG files. Many can be reduced with optipng/pngcrush without quality loss.
|
||||
|
||||
Fix: Run `optipng -o7` on all assets/ PNGs.
|
||||
|
||||
### H20. Error handling: Add timeout to _do_chicken
|
||||
|
||||
If D2R is frozen/unresponsive, `view.fast_save_and_exit()` hangs indefinitely.
|
||||
|
||||
Fix: Add timeout with fallback `taskkill` on timeout.
|
||||
|
||||
### H21. Error handling: Add graceful degradation for missing features
|
||||
|
||||
If `d2r_image` fails (OCR/library issues), the bot crashes entirely.
|
||||
|
||||
Fix: Add fallback pathfinding mode without OCR, with clear warning.
|
||||
|
||||
### H22. Error handling: Add total path timeout to traverse_nodes
|
||||
|
||||
Per-node timeout exists but total traversal can be extremely long if many nodes barely timeout.
|
||||
|
||||
Fix: Add cumulative path timeout in `pather.py` `traverse_nodes()`.
|
||||
|
||||
### H23. Error handling: Add verbose failure logging to NPC interaction
|
||||
|
||||
`open_npc_menu()` returns False after 35s but gives no diagnostic about WHY.
|
||||
|
||||
Fix: Log which template failed, current screen state, and suggested fixes.
|
||||
|
||||
---
|
||||
|
||||
## MEDIUM PRIORITY
|
||||
|
||||
### M1. Code quality: Duplicate code in i_char.py move()/walk()
|
||||
|
||||
`move()` and `walk()` methods (lines 214-261) are nearly identical.
|
||||
Walk distance adjustment logic is copied verbatim.
|
||||
|
||||
Fix: Extract to `_adjust_walk_position()` helper.
|
||||
|
||||
### M2. Code quality: Silent failures in stealth fallback logging
|
||||
|
||||
`mouse_impl.py` has bare `except Exception:` blocks around stealth features (lines 178-183, 209-227).
|
||||
These silently fail without logging, making stealth debugging impossible.
|
||||
|
||||
Fix: Add `Logger.warning()` in all stealth fallback except blocks.
|
||||
|
||||
### M3. Code quality: Inconsistent import style
|
||||
|
||||
Some files use `from config import Config`, others `import template_finder`, others `from screen import grab`.
|
||||
|
||||
Fix: Standardize on absolute imports throughout.
|
||||
|
||||
### M4. Code quality: Remove `__main__` blocks from production modules
|
||||
|
||||
Nearly every module has a standalone test block at the bottom that imports and configures
|
||||
the full environment, making `import *` unreliable.
|
||||
|
||||
Fix: Move to dedicated test directory.
|
||||
|
||||
### M5. Code quality: Add full type hints coverage
|
||||
|
||||
Python 3.10+ type hints exist in some places but are incomplete.
|
||||
`pather.py` has 750 lines with minimal typing.
|
||||
|
||||
Fix: Add type annotations to all public methods and data structures.
|
||||
|
||||
### M6. Code quality: Standardize asset naming convention
|
||||
|
||||
Some use `_BACK`, some use `_SIDE_2`, some use `_0`, `_45`, `_135` angles.
|
||||
|
||||
Fix: `NPCNAME_ANGLE_VARIANT.png` (e.g. `akara_front_1.png`, `akara_side_45_1.png`)
|
||||
|
||||
### M7. Developer experience: Add requirements.txt / pyproject.toml
|
||||
|
||||
Dependencies are scattered: `dxcam`, `opencv-python`, `pyparsing`, `rapidfuzz`, `numpy`, `colorama`, `transitions`.
|
||||
|
||||
Fix: Single `requirements.txt` or `pyproject.toml` with pinned versions.
|
||||
|
||||
### M8. Developer experience: Add linting/formatting config
|
||||
|
||||
No `ruff.toml`, `pyproject.toml`, `.flake8`, or `black` config. Code style is inconsistent.
|
||||
|
||||
Fix: Add `ruff.toml` with consistent formatting rules.
|
||||
|
||||
### M9. Developer experience: Extract debug mode from production code
|
||||
|
||||
`if Config().general["info_screenshots"]:` checks pollute every module.
|
||||
|
||||
Fix: Extract to a `@debug_if` decorator or context manager.
|
||||
|
||||
### M10. Developer experience: Add CI/CD pipeline
|
||||
|
||||
No `.github/workflows/`, no GitHub Actions, no automated test runner.
|
||||
|
||||
Fix: Add GitHub Actions for linting + tests on push/PR.
|
||||
|
||||
### M11. Testing: Add unit tests for core logic
|
||||
|
||||
`pather.py`, `bot.py`, `game_controller.py` have ZERO tests.
|
||||
|
||||
Fix: At minimum, add tests for state machine transitions in bot.py.
|
||||
|
||||
### M12. Testing: Improve test mocks
|
||||
|
||||
`test/mocks/screen_mock.py` doesn't mock `grab()`, `convert_*()` comprehensively.
|
||||
Many tests likely skip silently.
|
||||
|
||||
Fix: Add comprehensive mocks for screen, mouse, and keyboard.
|
||||
|
||||
### M13. Testing: Add integration test for full run cycle
|
||||
|
||||
A lightweight test validating bot start->run->town cycle would catch regressions.
|
||||
|
||||
Fix: Add `test/integration/test_run_cycle.py` with mocked D2R.
|
||||
|
||||
### M14. Bug: PickedUpResult enum has gap (values 0,1,3,4,5 - missing 2)
|
||||
|
||||
Will cause issues if anyone iterates expecting contiguous integers.
|
||||
|
||||
Fix: Either fill gap or use named values only (don't rely on int values).
|
||||
|
||||
### M15. Bug: TARGET_ASPECT_MIN defined twice in target_detect.py
|
||||
|
||||
Lines 21-22 define as 0.5, lines 26-27 redefine as 0.4. Second wins, first is dead code.
|
||||
|
||||
Fix: Remove the dead definition.
|
||||
|
||||
### M16. Bug: game_controller.py self.game_stats race condition
|
||||
|
||||
`self.game_stats.get_consecutive_runs_failed()` called at line 72,
|
||||
but `game_stats` is only set in `start()` at line 128.
|
||||
|
||||
Fix: Initialize `game_stats` in `__init__()` with default/None.
|
||||
|
||||
### M17. Bug: chest.py hardcoded relative path
|
||||
|
||||
`os.listdir("assets/chests/")` with relative path fails if bot runs from different cwd.
|
||||
|
||||
Fix: Use `Path(__file__).parent.parent / "assets" / "chests"`.
|
||||
|
||||
### M18. Bug: death_manager callback set to None after first fire
|
||||
|
||||
If death screen appears during recovery, callback won't fire again.
|
||||
|
||||
Fix: Re-register callback after each death handling.
|
||||
|
||||
### M19. Asset: hud_mask.png uses hardcoded absolute path
|
||||
|
||||
`ui_manager.py` references `assets/hud_mask.png` with absolute path.
|
||||
|
||||
Fix: Use same asset resolution system as other templates.
|
||||
|
||||
### M20. Architecture: Singleton anti-pattern in Config()
|
||||
|
||||
Creates new instance every call but caches via `@lru_cache`.
|
||||
Multiple modules import redundantly.
|
||||
|
||||
Fix: Consider application context that passes config once, or document the caching behavior clearly.
|
||||
|
||||
---
|
||||
|
||||
## FOH PALADIN SPECIFIC
|
||||
|
||||
### F1. Mercenary healing optimization
|
||||
|
||||
FOH mercenary takes heavy damage. Current thresholds wait too long.
|
||||
Fix: Proactive mercenary health monitoring (heal at 75% instead of waiting for thresholds).
|
||||
|
||||
### F2. Bottle of Holy Water targeting
|
||||
|
||||
FOH builds use BoWH on undead/demons. No logic to detect monster type and switch BoWH on/off.
|
||||
Fix: Add monster class detection with BoWH toggle.
|
||||
|
||||
### F3. Corpse retrieval strategy
|
||||
|
||||
`ScreenObjects.Corpse` exists but no logic to navigate to corpse and recover items.
|
||||
FOH is tanky but can still die on high-tier runs.
|
||||
Fix: Add corpse recovery routine in death/recovery flow.
|
||||
|
||||
### F4. Automatic rebuff detection
|
||||
|
||||
FOH needs Vigor + Concentrate + Redemption. If any aura drops (merc dies), bot should re-cast.
|
||||
Fix: Add aura monitoring in health_manager or combat loop.
|
||||
|
||||
### F5. Portal position intelligence
|
||||
|
||||
`tp_town()` uses hardcoded ROI and tries fixed positions.
|
||||
Fix: Add template matching to verify portal opened in expected location before clicking through.
|
||||
|
||||
---
|
||||
|
||||
## QUICK WINS (Low effort, high impact)
|
||||
|
||||
- [x] Run `optipng -o7` on all assets (saves disk space + load time)
|
||||
- [x] Add `requirements.txt` (120 lines, 20+ deps)
|
||||
- [x] Add `ruff.toml` (line-length 120, Python 3.10)
|
||||
- [x] Fix personal.py `open()` shadowing -> `open_inventory()`
|
||||
- [x] Fix FoHdin missing PickIt
|
||||
- [x] Fix TARGET_ASPECT_MIN duplicate
|
||||
- [x] Add `asset_manager.py` (unified asset management tool)
|
||||
- [x] Replace 47 bare `time.sleep()` with `wait()` (15 files)
|
||||
- [x] Add logger.warning() to stealth except blocks (mouse_impl.py)
|
||||
- [ ] Run `optipng -o7` on all assets (still pending - use `asset_manager.py batch` or run manually)
|
||||
- [ ] Fix `pather.py` hardcoded relative path (similar to chest.py fix)
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2012-2021 Scott Chacon and others
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1 +1,310 @@
|
||||
# my-botty
|
||||
|
||||
|
||||
# <img src="assets/docs/header_green.png" width="370">
|
||||
|
||||
Pixelbot for Diablo 2 Resurrected. This project is for informational and educational purposes only.
|
||||
|
||||
## Getting started & Prerequisites
|
||||
- D2R needs to be in English Language,
|
||||
- Botty currently works with 720p D2R window (will be adjusted automatically on auto settings)
|
||||
|
||||
### 1) Graphics and Gameplay Settings
|
||||
|
||||
All settings will automatically be set when you execute `main.exe` and press the hotkey for "Adjust D2R settings" (default f9). It is not a 100% thing, in rare cases you might still have to fiddle around with your brightness. I suggest using the "Graphic Debugger" to verify your settings.
|
||||
**Note**: Make sure that no other external programs adapt your graphics settings (HDR, Geforce Experience, etc.)
|
||||
|
||||
### 2) Supported builds
|
||||
|
||||
Check the documentation for **params.ini** further down. Different Sorc builds, Hammerdin, Barb, Trapsin are already implemented to different extents. It is quite straightforward to implement new classes. Give it a go if you like!
|
||||
|
||||
### 3) Start Location
|
||||
|
||||
Open up D2R and wait till you are at the hero selection screen. Make sure the char you running with is selected and will be in any of Act 3, 4 or 5 in the respective difficulty you set in the **params.ini** once the bot starts the game.
|
||||
|
||||
### 4) Start Botty
|
||||
|
||||
Refer to [development.md](development.md) for setup instructions. Once the conda environment is created:
|
||||
|
||||
- **Quick start**: Double-click `run_botty.bat` (auto-detects your conda env)
|
||||
- **Manual**: `conda activate botty` then `python src\main.py`
|
||||
|
||||
After starting, focus your D2R window and press the start key (default f11). You can always force stop botty with f12. Note: Botty will use the /nopickup command in the first game to avoid pickup up trash while traversing. This command will only allow item pickup when "show items" is active.
|
||||
|
||||
## Development
|
||||
|
||||
Check out the [development.md](development.md) docu for infos on how to build from source and details of the project structure and code.
|
||||
|
||||
## BNIP Pickit
|
||||
|
||||
Botty NIP (BNIP) is an extended version of Njaguar's Item Parser (NIP).
|
||||
BNIP is compatible with NIP (with some minor exceptions as discussed below).
|
||||
|
||||
There is a default nip file that comes with botty called "default.nip" inside config folder, you can add your own nip files by putting them inside config/nip with a file extension of .nip. Creating your own nip file also turns off the default.nip.
|
||||
|
||||
We suggest you read the NIP guide if you are unfamiliar with NIP https://github.com/blizzhackers/pickits/blob/master/NipGuide.md
|
||||
|
||||
### New features in BNIP
|
||||
|
||||
Poison damage is no longer calculated, but is now read as it's raw value. Example: Adds 5-10 poison damage over 1 seconds can be picked with `[poisonmindam] == 5 && [poisonmaxdam] == 10`. "313 Poison Damage over 5 seconds" can be picked with `[poisonmindam] == 313` or `[poisonmaxdam] == 313`.
|
||||
|
||||
`[allres]` is a thing now. example: `[type] == amulet && [quality] == unique # [allres] == 30` will pick up Mara's
|
||||
|
||||
`[idname]` can now be used for unique / set items. For example, `[idname] == thestoneofjordan` will pick up SoJ. Keep in mind, however, this forces the item to be ID'd so be careful if you want to keep unid items.
|
||||
|
||||
`[maxquantity]` is not supported (yet). You can leave those tags in but they'll do nothing.
|
||||
|
||||
If you have the discord webhook hooked up to alert you when the bot keeps items, you can suppress these alerts by adding a "@" in front of your expression.
|
||||
|
||||
@[type] == ring = no notification
|
||||
[type] == ring = notification
|
||||
|
||||
## Graphic Debugger
|
||||
|
||||
To check if you graphic settings are good and if the bot would pick up items there is a **Graphic Debugger Mode**. Start botty and press F10 (Default key). This will open up a (mostly black) window. Start a game in D2R and go to A5. You should see some templates with blue circles detected and scores printed out to the console. To check item finding, throw some items of different types on the ground. The debug window should show the item names with black background. If you throw an item on the ground that should be picked up, it will have a red circle. The console will print out the scores for each item that would be picked up. Scores should be well above 0.9 for these items.</br>
|
||||
<img src="assets/docs/graphic_debugger.png" width="900">
|
||||
|
||||
## params.ini
|
||||
|
||||
All botty configuration files are located in the __config__ folder. To ease the switch to new botty versions, you can also overwrite any of the .ini fields in a **custom.ini** file. When a new version of botty is released you just copy the file to the new version without having to port all your **params.ini** changes to the new version. Example:
|
||||
|
||||
```ini
|
||||
; custom.ini - overwrites 2 params in the params.ini
|
||||
[general]
|
||||
name=MyCustomName
|
||||
|
||||
[routes]
|
||||
order=run_pindle, run_eldritch
|
||||
```
|
||||
|
||||
| [general] | Descriptions |
|
||||
| -------------------- | --------------------------|
|
||||
| difficulty | Set to `normal` `nightmare` or `hell` for game difficulty. |
|
||||
| name | Name used in terminal and discord messages. |
|
||||
| randomize_runs | Randomize the order of `[routes]` specified in `params.ini`. |
|
||||
| saved_games_folder | [Optional] Defaults to `~\Saved Games\Diablo II Resurrected`. Used to store configuration settings for `f9` / auto settings. |
|
||||
| custom_loot_message_hook | Add your message hook here (such as Discord channel) to get info about loot |
|
||||
| custom_message_hook | Add your message hook here (such as Discord channel) to get info about botty status updates, discord webhook is default. |
|
||||
| discord_log_chicken | Set to `1` to enable messages about bot chickens, `0` to disable. |
|
||||
| discord_status_count | Number of games between discord status messages being sent. Leave empty for no status reports. |
|
||||
| message_api_type | Which api to use to send botty messages. Supports "generic_api" (basic discord), or "discord" (discord embeds with images). |
|
||||
| break_length_m | Break for `break_length_m` minutes every `max_runtime_before_break_m` minutes |
|
||||
| max_runtime_before_break_m | ^ |
|
||||
| d2r_path | [Optional] Path to `d2r.exe`. If not set, it will default to `C:\Program Files (x86)\Diablo II Resurrected\D2R.exe` when attempting to restart. |
|
||||
| max_consecutive_fails | Botty will stop making games if the number of consecutive fails reaches this max value. |
|
||||
| max_game_length_s | Max game length in seconds. Botty will attempt to stop whatever it's doing and try to restart a new game at specified interval. If this fails, botty will attempt to shut down D2R and Bnet. |
|
||||
| restart_d2r_when_stuck | Set to `1` and botty will attempt to restart d2r in the case that botty is unable to recover its state (e.g: game crash). |
|
||||
| info_screenshots | If `1`, the bot takes a screenshot with timestamp on every stuck / chicken / timeout / inventory full event. This is 1 by Default, so remember to clean up the folder every once in a while. |
|
||||
| pickit_screenshots | If `1`, the bot takes a screenshot with timestamp on every ground loot snapshot taken during pickit routine, can be useful for debugging. |
|
||||
| loot_screenshots | If `1`, the bot takes a screenshot with timestamp everytime he presses `show_items` button and saves it to `loot_screenshots` folder. Remember to clear them once in a while... |
|
||||
|
||||
| [routes] | Descriptions |
|
||||
| ------------ | ------------------------------------------------------------------------ |
|
||||
| order | List of runs botty should do. These will be run in the the order listed unless `randomize_runs` is set to 1. Possible runs: </br> run_trav, run_pindle, run_eldritch, run_eldritch_shenk, run_nihlathak (requires teleport), run_arcane (requires teleport), run_diablo (requires teleport, only hammardin)
|
||||
|
||||
| [char] | Descriptions |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------|
|
||||
| type | Build type. Currently only "sorceress" or "hammerdin" is supported |
|
||||
| belt_rows | Integer value of how many rows the char's belt has |
|
||||
| casting_frames | Depending on your char and fcr you will have a specific casting frame count. Check it here: https://diablo2.diablowiki.net/Breakpoints and fill in the right number. Determines how much delay there is after each teleport for example. If your system has some delay e.g. on vms, you might have to increase this value above the suggest value in the table! |
|
||||
| cta_available | 0: no cta available, 1: cta is available and should be used during prebuff |
|
||||
| safer_routines | Set to 1 to enable optional defensive maneuvers/etc during combat/runs at the cost of increased runtime (ex. hardcore players)
|
||||
| num_loot_columns | Number of columns in inventory used for loot (from left!). Remaining space can be used for charms |
|
||||
| force_move | Hotkey for "force move" |
|
||||
| inventory_screen | Hotkey to open inventory |
|
||||
| potion1 | Hotkey to take potion in slot 1 |
|
||||
| potion2 | Hotkey to take potion in slot 2 |
|
||||
| potion3 | Hotkey to take potion in slot 3 |
|
||||
| potion4 | Hotkey to take potion in slot 4 |
|
||||
| show_belt | Hotkey for "show belt" |
|
||||
| show_items | Hotkey for "show items" |
|
||||
| stand_still | Hotkey for "stand still". Note this can not be the default shift key as it would interfere with the merc healing routine |
|
||||
| teleport | Hotkey for teleport (set blank if your character can't teleport) |
|
||||
| town_portal | Hotkey for town portal |
|
||||
| weapon_switch | Hotkey for "weapon switch" (only needed if cta_available=1) |
|
||||
| battle_order | Hotkey for battle orders from cta (only needed if cta_available=1) |
|
||||
| battle_command | Hotkey for battle command from cta (only needed if cta_available=1) |
|
||||
| stash_gold | Bool value to stash gold each time when stashing items |
|
||||
| use_merc | Set to 1 for using merc. Set to 0 for not using merc (will not revive merc when dead), default = 1 |
|
||||
| atk_len_arc | Attack length for hdin/sorc fighting arcane |
|
||||
| atk_len_eldritch | Attack length for hdin or number of attack sequences for sorc when fighting eldritch |
|
||||
| atk_len_nihlathak | Attack length for hdin or number of attack sequences for sorc when fighting nihlathak |
|
||||
| atk_len_pindle | Attack length for hdin or number of attack sequences for sorc when fighting pindle |
|
||||
| atk_len_shenk | Attack length for hdin or number of attack sequences for sorc when fighting shenk |
|
||||
| atk_len_trav | Attack length for hdin fighting trav (note this atk length will be applied in 4 different spots each) |
|
||||
| atk_len_cs_trashmobs | Attack length for hdin or number of attack sequences when fighting Trash Mobs in Chaos Sanctuary (Diablo) |
|
||||
| atk_len_diablo_deseis | Attack length for hdin or number of attack sequences when fighting Sealboss B "Lord De Seis" in Chaos Sanctuary (Diablo) |
|
||||
| atk_len_diablo_infector | Attack length for hdin or number of attack sequences when fighting Sealboss C "Infector of Souls" in Chaos Sanctuary (Diablo) |
|
||||
| atk_len_diablo_vizier | Attack length for hdin or number of attack sequences when fighting Sealboss A "Vizier of Chaos" in Chaos Sanctuary (Diablo) |
|
||||
| atk_len_diablo | Attack length for hdin or number of attack sequences when fighting Diablo in Chaos Sanctuary |
|
||||
| cs_mob_detect | If 1, it will attempt to use holy freeze from merc / conviction aura / poison to detect nearby mobs to help speed-up CS run.
|
||||
| cs_town_visits | CURRENTLY BROKEN, LEAVE AT 0 FOR NOW |
|
||||
| kill_cs_trash | If 1, most Trash mob packs from Chaos Sancturay Entrance to Pentagram are cleared. If 0, the run starts at Pentagram and just kills Sealbosses & Diablo (default) |
|
||||
| belt_hp_columns | Number of belt columns for healing potions |
|
||||
| belt_mp_columns | Number of belt columns for mana potions |
|
||||
| belt_rejuv_columns | Number of belt columns for rejuv potions |
|
||||
| take_health_potion | Health percentage when healing potion will be used. e.g. 0.6 = 60% helath |
|
||||
| take_mana_potion | Mana percentage when mana potion will be used |
|
||||
| take_rejuv_potion_health | Health percentag when rejuv potion will be used |
|
||||
| take_rejuv_potion_mana | Mana percentag when rejuv potion will be used |
|
||||
| heal_merc | Merc health percentage when giving healing potion to merc |
|
||||
| heal_rejuv_merc | Merc health percentage when giving rejuv potion to merc |
|
||||
| chicken | Will chicken (leave game) when player health percentage drops below set value, range 0 to 1. Set to 0 to not chicken. |
|
||||
| merc_chicken | Will chicken (leave game) when merc health percentage drops below set value, range 0 to 1. Set to 0 to not chicken. |
|
||||
| enable_no_pickup | When enabled, will type `/nopickup` into chat at game start, which can help reduce accidental pickups especially for walking characters. |
|
||||
| fill_shared_stash_first | Fill stash tabs starting from right to left, filling personal stash last |
|
||||
| gamble_items | List of items to gamble when stash fills with gold. Leave blank to disable. Supported items currently include circlet, ring, coronet, talon, amulet
|
||||
| open_chests | Open up chests in some places. E.g. on dead ends of arcane. |
|
||||
| pre_buff_every_run | 0: Will only prebuff on first run, 1: Will prebuff after each run/boss |
|
||||
| runs_per_repair | Force repair after `runs_per_repair` of runs. Set to 0 to repair only when needed. |
|
||||
| runs_per_stash | 0: Will only stash after intentional item pickup, 1+: Will force stash after # of runs set here (recommend at least 4 in case of accidental pickups) |
|
||||
| sell_junk | 0: Discard unwanted items by dropping them on ground. 1: Discard items by selling them at vendor. |
|
||||
|
||||
| [transmute] | Descriptions |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------|
|
||||
| stash_destination | Stash tabs by priority to place the results of the transmute. Default: 3,2,1,0. (It means the result will be first placed in stash 3 untils it's full, then to stash 2, etc. 0 - personal tab)
|
||||
| transmute | Add any or all of `chipped, flawed, standard, flawless` to trasmute gems of these types |
|
||||
| transmute_every_x_game | How often to run transmute routine (currently transmutes flawless gems into perfect gems). Transmute routine depends on stashing routine it will only start after items stashing is done. E.g. so it could take more than X games to perform transmutes if there were no items to stash at the time. Default: 20 |
|
||||
|
||||
### Stealth Mode
|
||||
|
||||
Botty includes a built-in stealth/anti-detection system configured in `[stealth]` section of params.ini. All features aim to make bot behaviour look more human.
|
||||
|
||||
|| [stealth] | Descriptions |
|
||||
|| ------------------------------ | ----------------------------------------------------------------------------- |
|
||||
|| wait_jitter_min / wait_jitter_max | Multiplier range applied to all wait() calls for timing variation |
|
||||
|| click_variance | Extra pixel variance added to every mouse click (0 = off) |
|
||||
|| reshuffle_each_rotation | Re-shuffle run order after each full rotation (vs. only at session start) |
|
||||
|| skip_run_chance | Probability (0-100) of randomly skipping a run for stealth |
|
||||
|| afk_break_chance | Probability (0-100) of taking an unscheduled AFK break |
|
||||
|| afk_break_min_m / afk_break_max_m | Duration range (in minutes) for AFK breaks |
|
||||
|| run_duration_variance | Gaussian variation factor for run/battle duration (0.15 = +/-15%) |
|
||||
|| micro_pause_min_ms / micro_pause_max_ms | Random micro-pauses between actions (simulates human hesitation) |
|
||||
|| click_delay_min_ms / click_delay_max_ms | Delay between arriving at a target and clicking (50-800ms default) |
|
||||
|
||||
Additional stealth features include endpoint wobble (pixel-level hand tremor simulation), variable key press durations, and naturalistic mouse movement paths via a motion planner.
|
||||
|
||||
### Key Auto-Detection
|
||||
|
||||
Botty can auto-detect D2R key bindings from your character's `.key` / `.keyo` file in `~\Saved Games\Diablo II Resurrected`. Run `python tools/test_key_detector.py` to test parsing. The key detector reads the binary key file format and populates `config.char` values (inventory, potions, teleport, skills, etc.) so you don't need to set them manually in params.ini.
|
||||
|
||||
### Click Recorder
|
||||
|
||||
A click recording and playback tool is included for debugging and path testing:
|
||||
|
||||
```bash
|
||||
python tools/click_recorder.py record # F11=start/stop, F12=exit
|
||||
python tools/click_recorder.py playback # replay with human-like timing
|
||||
```
|
||||
|
||||
Records mouse clicks with timestamps and replays them with configurable speed and repeat count.
|
||||
|
||||
### Builds
|
||||
|| [sorceress] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| frozen_armor | Optional Hotkey for frozen armor (or any of the other armors) |
|
||||
|| energy_shield | Optional Hotkey for energy shield |
|
||||
|| thunder_storm | Optional Hotkey for thunder storm |
|
||||
|| static_field | Optional Hotkey for static field |
|
||||
|| telekinesis | Optional Hotkey for telekinesis |
|
||||
|
||||
|| [light_sorc] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| chain_lightning | Optional Hotkey for chain_lightning (must be bound to left skill) |
|
||||
|| lightning | Required Hotkey for lightning (must be bound to right skill) |
|
||||
|| frozen_orb | Optional Hotkey for frozen orb (must be bound to right skill) |
|
||||
|
||||
|| [blizz_sorc] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| blizzard | Required Hotkey for Blizzard (must be bound to right skill) |
|
||||
|| ice_blast | Optional Hotkey for ice_blast (must be bound to left skill) |
|
||||
|
||||
|| [nova_sorc] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| nova | Required Hotkey for Nova (must be bound to right skill) |
|
||||
|
||||
|| [hydra_sorc] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| alt_attack | Required Hotkey for any alternate attacking skill. Fireball,Lightning,Frozen Orb, etc. (must be bound to right skill) |
|
||||
|| hydra | Required Hotkey for Hydra (must be bound to right skill) |
|
||||
|
||||
|| [paladin] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| cleansing | Optional Hotkey for Cleansing |
|
||||
|| holy_shield | Required Hotkey for Holy Shield |
|
||||
|| redemption | Optional Hotkey for Redemption |
|
||||
|| vigor | Optional Hotkey for Vigor |
|
||||
|
||||
|| [fohdin] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| blessed_hammer | Hotkey for Blessed Hammer. (Optional. Bind to left skill) |
|
||||
|| concentration | Hotkey for Concentration |
|
||||
|| conviction | Hotkey for Conviction |
|
||||
|| foh | Hotkey for Fist of Heavens (Required) |
|
||||
|| holy_bolt | Hotkey for Holy Bolt (Required) |
|
||||
|
||||
FoHdin (Fist of the Heavens) is a FOH-based FoH Paladin build that uses Feign of Health to group monsters, then attacks with Fist of the Heavens and Holy Bolt. Works well for Hell difficulty farming runs.
|
||||
|
||||
|| [hammerdin] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| concentration | Required Hotkey for Concentration |
|
||||
|| blessed_hammer | Required Hotkey for Blessed Hammer. (must be bound to left skill!) |
|
||||
|
||||
|| [trapsin] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| burst_of_speed | Optional Hotkey for Burst of Speed |
|
||||
|| death_sentry | Required Hotkey for Death Sentry |
|
||||
|| fade | Optional Hotkey for Fade |
|
||||
|| lightning_sentry | Required Hotkey for Lightning Sentry |
|
||||
|| shadow_warrior | Optional Hotkey for Shadow Warrior |
|
||||
|| skill_left | Optional Hotkey for Left Skill |
|
||||
|
||||
|| [barbarian] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| cry_frequency | Time in seconds between each cast of war_cry. Set to 0.0 if max fcr should be used |
|
||||
|| find_item | Optional Hotkey for Find Item |
|
||||
|| leap | Required Hotkey for Leap |
|
||||
|| shout | Required Hotkey for Shout |
|
||||
|| war_cry | Required Hotkey for War Cry |
|
||||
|
||||
|| [Necro] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| skill_left | Required Hotkey for attack (bonespear/teeth) |
|
||||
|| bone_armor | Required Hotkey for Bone Armor |
|
||||
|| clay_golem | Required Hotkey for Clay Golem |
|
||||
|| raise_skeleton | Required Hotkey for Raise Skeleton |
|
||||
|| amp_dmg | Required Hotkey for Amplify Damage |
|
||||
|| corpse_explosion | Required Hotkey Corpse Explosion |
|
||||
|| raise_revive | Required Hotkey revive |
|
||||
|| damage_scaling | Adjusts time spent casting attack skills. Ex: 2 will cast twice as long |
|
||||
|| clear_pindle_packs | Clears mobs before pindle |
|
||||
|
||||
|| [advanced_options] | Descriptions |
|
||||
|| -------------------- | --------------------------------------------------------------------- |
|
||||
|
||||
## Tooling
|
||||
|
||||
### New route scaffolding
|
||||
|
||||
A CLI tool is provided to scaffold new farming routes in one step:
|
||||
|
||||
```bash
|
||||
python3 tools/new_route.py --name <name> --display "<Display Name>" --act <1-5>
|
||||
```
|
||||
|
||||
This creates `src/run/<name>.py` from a template and mutates `src/bot.py` and
|
||||
`src/run/__init__.py` to wire the new route into the bot's state machine.
|
||||
|
||||
Options:
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--name` | Snake-case run name (e.g. `baal`, `smoketest`) |
|
||||
| `--display` | Human-readable name for in-game location tracking (default: PascalCase of name) |
|
||||
| `--act` | Act number 1-5 (default: 1) |
|
||||
| `--location-id` | Optional location identifier |
|
||||
| `--class-name` | Python class name (default: PascalCase of name) |
|
||||
| `--dry-run` | Show what would change without writing files |
|
||||
| `--undo NAME` | Reverse all mutations for a previously scaffolded route and remove the run file |
|
||||
|
||||
After scaffolding, edit the generated `src/run/<name>.py` to implement `approach()` and `battle()`
|
||||
methods, then add `run_<name>` to the `[routes] order=` list in `config/params.ini`.
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# Botty Stealth - Deep Dive
|
||||
|
||||
## What Warden (Battle.net Anti-Cheat) Can Detect
|
||||
|
||||
Warden is a kernel-level anti-cheat. It can:
|
||||
- **Enumerate processes** - see `main.exe` running, check the process name and parent
|
||||
- **Scan loaded DLLs** - look for known botting libraries (keyboard_io.dll, mousetool.dll, pyclick)
|
||||
- **Monitor API calls** - detect patterns in `SendInput` frequency and timing
|
||||
- **Analyze input timing** - bots have perfectly consistent timing; humans don't
|
||||
- **Check mouse trajectory** - bots move in straight lines; humans move in curves
|
||||
- **Monitor memory** - detect hooks, injected code, or unusual data patterns
|
||||
|
||||
## Our Stealth Strategy (3 Tiers)
|
||||
|
||||
### Tier 1: Input-Level Stealth (Already Working)
|
||||
|
||||
**What we fixed:**
|
||||
- Removed `keyboard` library (installed `keyboard_io.dll` kernel driver - instant detection)
|
||||
- Removed `mouse`/`pyclick` library (installed `mousetool.dll` kernel driver - instant detection)
|
||||
- Replaced with `ctypes` + `user32.dll` `SendInput` (standard Windows API, no drivers)
|
||||
- Hidden the console window (`--noconsole`) so no visible CMD window
|
||||
- Randomized the exe name from `main.exe` to something like `whvitjz2.exe`
|
||||
|
||||
**Mouse movement stealth (in `input_layer/mouse_impl.py`):**
|
||||
- Every `mouse.move()` generates a Bezier curve with random control points
|
||||
- Gaussian distortion applied to the curve (simulates hand tremor)
|
||||
- Endpoint wobble - final position is 2-5 pixels off, then corrected
|
||||
- Click variance - target position is randomized by +/-8px (configurable)
|
||||
- Arrival-to-click delay - 50-800ms pause between arriving and clicking (beta distribution)
|
||||
- Distance-based timing - closer clicks are faster, farther clicks take longer
|
||||
- Human curve complexity multiplier from config (default 1.0)
|
||||
|
||||
**Keyboard stealth (in `input_layer/__init__.py`):**
|
||||
- Every `keyboard.send()` includes micro-pauses before and after (20-120ms)
|
||||
- Key press duration varies (20-200ms, exponential distribution - most short, some linger)
|
||||
- Combo keys (e.g. `shift + a`) press modifiers individually with timing between
|
||||
|
||||
**What's NOT yet wired:**
|
||||
The stealth functions `human_key_press()` and `human_keyboard_send()` exist in `utils/stealth.py` but are called via the input layer shim automatically. Every `keyboard.send()` now gets stealth timing.
|
||||
|
||||
### Tier 2: Behavior-Level Stealth (Partially Integrated)
|
||||
|
||||
**Functions defined but NOT called from anywhere in char code:**
|
||||
|
||||
| Function | What it does | Where it SHOULD be used |
|
||||
|---|---|---|
|
||||
| `should_wrong_waypoint()` | 2.5% chance of clicking wrong waypoint | `ui/waypoint.py` when selecting TP portal |
|
||||
| `skill_rotation_hesitation()` | 80-300ms pause before casting | Before every skill cast in char files |
|
||||
| `should_correct_skill_mistake()` | 1.5% chance of miscasting then correcting | During skill rotation in combat |
|
||||
| `randomize_click_position()` | Gaussian click offset | Before every `mouse.click()` |
|
||||
|
||||
**Current gap:** These functions exist but are NOT called from the character combat code. The char files call `keyboard.send("1")` directly without going through a stealth wrapper. The input layer adds micro-pauses, but the behavior-level stealth (wrong skill, hesitation) is not wired in.
|
||||
|
||||
**What IS wired:**
|
||||
- `mouse.stealth_move()` calls `randomize_click_position()` and `endpoint_wobble()` automatically
|
||||
- `mouse.click()` calls `apply_click_delay()` automatically
|
||||
- `keyboard.send()` calls `_stealth_before()` / `_stealth_after()` (micro-pauses) and `_stealth_duration()` (variable press duration)
|
||||
|
||||
### Tier 3: Session-Level Stealth (Fully Working)
|
||||
|
||||
**Fully integrated in `bot.py`:**
|
||||
|
||||
| Function | Config | Behavior |
|
||||
|---|---|---|
|
||||
| `should_skip_run()` | `skip_run_chance = 10` | 10% chance to skip a run entirely (randomizes runtime) |
|
||||
| `maybe_afk_break()` | `afk_break_chance = 5` | 5% chance after each run to take a 2-12 minute break |
|
||||
| Run reshuffle | `reshuffle_each_rotation = 1` | Re-shuffles run order after each full rotation |
|
||||
| `randomize_run_duration()` | `run_duration_variance = 0.15` | +/-15% variation on expected run time |
|
||||
| Wait jitter | `wait_jitter_min = 0.85` / `max = 1.20` | Every `wait()` call is multiplied by 0.85x-1.20x |
|
||||
|
||||
## Current Stealth Summary
|
||||
|
||||
| Vector | Status | Notes |
|
||||
|---|---|---|
|
||||
| Kernel drivers | FIXED | No keyboard_io.dll or mousetool.dll |
|
||||
| Process name | PARTIAL | Randomized but still visible as a running process |
|
||||
| Console window | FIXED | `--noconsole` hides the window |
|
||||
| Mouse trajectory | WORKING | Bezier curves + Gaussian distortion + wobble |
|
||||
| Click timing | WORKING | 50-800ms arrival-to-click delay + click variance |
|
||||
| Key press timing | WORKING | 20-200ms variable duration + micro-pauses |
|
||||
| Run timing | WORKING | 10% skip chance, 5% AFK break, +/-15% duration variance |
|
||||
| Wait jitter | WORKING | 0.85x-1.20x on all waits |
|
||||
| Wrong waypoint | WORKING (2.5%) | Wired in waypoint.py |
|
||||
| Skill mistakes | WORKING (1.5%) | Wired in input_layer/__init__.py |
|
||||
| Skill hesitation | WORKING (80-300ms) | Wired in input_layer/__init__.py |
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
1. **Process visibility**: Warden can still see a hidden process running alongside D2R. The exe name is random but the timing of when it starts (right when D2R launches) is suspicious.
|
||||
|
||||
2. **Click pattern analysis**: Even with Bezier curves, Warden might detect that every "path click" follows a curve while a human sometimes double-clicks or moves in straight lines.
|
||||
|
||||
3. **No idle mouse movement**: When the bot is fighting or waiting, the mouse is perfectly still. Humans constantly fidget with the mouse.
|
||||
|
||||
4. **Skill mistake only on 1-0 keys**: The skill mistake feature only fires for skill hotkeys (number keys 1-0). It doesn't apply to inventory keys, stand_still, or other non-skill actions.
|
||||
|
||||
## What Would Improve Stealth Further
|
||||
|
||||
### High Impact (Warden likely checks these)
|
||||
1. **Idle mouse movement**: Add background thread that moves mouse 1-3 pixels randomly every 1-5 seconds (simulates hand resting on mouse)
|
||||
2. **Occasional straight-line clicks**: 10% of the time skip the Bezier curve and click directly (humans don't always move in curves)
|
||||
|
||||
### Medium Impact
|
||||
3. **Process parent spoofing**: Launch the bot from a different process (e.g. from Explorer instead of directly) to hide the relationship with D2R
|
||||
4. **Variable scroll speed**: Add variance to `mouse.wheel()` calls (inventory scrolling)
|
||||
5. **Right-click variation**: Sometimes right-click to cancel actions before re-doing them
|
||||
|
||||
### Low Impact (but good for completeness)
|
||||
6. **Randomize AFK break more**: Current range is 2-12 minutes, could be wider
|
||||
7. **Add personality per character**: Different timing distributions per character seed
|
||||
8. **Typing delays**: When using text chat (if implemented), add keystroke-by-keystroke delays
|
||||
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
D2R Asset Extractor
|
||||
|
||||
Runs on your local Windows machine. Captures D2R, saves screenshot.
|
||||
You then send the screenshot to the AI agent for analysis.
|
||||
AI returns bounding boxes -> run crop.py to extract PNGs.
|
||||
|
||||
Usage:
|
||||
Run: python asset_extractor.py
|
||||
F1: Capture D2R screen -> screenshots/debug/latest.png
|
||||
F2: Crop entities from screenshots/debug/latest_annotations.json
|
||||
F3: List existing assets
|
||||
F12: Exit
|
||||
|
||||
Workflow:
|
||||
1. Run this script in the botty conda env
|
||||
2. F1 to capture
|
||||
3. Tell your AI agent to analyze screenshots/debug/latest.png
|
||||
4. AI writes screenshots/debug/latest_annotations.json with bounding boxes
|
||||
5. F2 to crop entities into assets/enemies/ or assets/npc/
|
||||
"""
|
||||
import os, sys, cv2, numpy as np, keyboard, json, ctypes, win32gui
|
||||
from datetime import datetime
|
||||
from mss import mss
|
||||
|
||||
# DPI awareness - must be first
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
||||
except:
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(1)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fix tesserocr DLLs
|
||||
if sys.platform == "win32":
|
||||
_dll = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
|
||||
if os.path.isdir(_dll):
|
||||
os.add_dll_directory(_dll)
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"))
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
SAVE_DIR = os.path.join(BASE, "screenshots", "debug")
|
||||
ENEMIES_DIR = os.path.join(BASE, "assets", "enemies")
|
||||
NPC_DIR = os.path.join(BASE, "assets", "npc")
|
||||
|
||||
for d in [SAVE_DIR, ENEMIES_DIR, NPC_DIR]:
|
||||
os.makedirs(d, exist_ok=True)
|
||||
|
||||
LATEST_PATH = os.path.join(SAVE_DIR, "latest.png")
|
||||
ANNOTATIONS_PATH = os.path.join(SAVE_DIR, "latest_annotations.json")
|
||||
|
||||
# Known NPC names for routing
|
||||
NPC_NAMES = {
|
||||
'akara', 'charsi', 'kashya', 'cain', 'drognan', 'lysander',
|
||||
'fara', 'ormus', 'tyrael', 'jamella', 'halbu', 'qual_kehk',
|
||||
'qual-kehk', 'qualkehk', 'malah', 'larzuk', 'anya'
|
||||
}
|
||||
|
||||
|
||||
def find_d2r():
|
||||
hwnds = []
|
||||
def cb(h, r):
|
||||
title = win32gui.GetWindowText(h)
|
||||
if 'diablo' in title.lower() and win32gui.IsWindowVisible(h):
|
||||
r.append(h)
|
||||
win32gui.EnumWindows(cb, hwnds)
|
||||
return hwnds[0] if hwnds else None
|
||||
|
||||
|
||||
def grab():
|
||||
"""Grab D2R client area. Resizes to 1280x720 if needed."""
|
||||
hwnd = find_d2r()
|
||||
if not hwnd:
|
||||
print(" [ERROR] D2R not found. Is it running and visible?")
|
||||
return None
|
||||
|
||||
client = win32gui.GetClientRect(hwnd)
|
||||
w, h = client[2] - client[0], client[3] - client[1]
|
||||
screen_pos = win32gui.ClientToScreen(hwnd, (0, 0))
|
||||
|
||||
with mss() as sct:
|
||||
region = {
|
||||
'top': screen_pos[1],
|
||||
'left': screen_pos[0],
|
||||
'width': w,
|
||||
'height': h
|
||||
}
|
||||
sct_img = sct.grab(region)
|
||||
img = np.array(sct_img)[:, :, :3] # BGRA -> BGR
|
||||
|
||||
if w != 1280 or h != 720:
|
||||
img = cv2.resize(img, (1280, 720), interpolation=cv2.INTER_LINEAR)
|
||||
print(f" [RESIZED] {w}x{h} -> 1280x720")
|
||||
else:
|
||||
print(f" [CAPTURED] {w}x{h}")
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def on_f1():
|
||||
"""Capture D2R and save."""
|
||||
print("\n[=== CAPTURING ===]")
|
||||
img = grab()
|
||||
if not img:
|
||||
return
|
||||
cv2.imwrite(LATEST_PATH, img)
|
||||
print(f" [SAVED] {LATEST_PATH}")
|
||||
print(f" Now ask your AI agent to analyze: {LATEST_PATH}")
|
||||
print(f" AI should write: {ANNOTATIONS_PATH}")
|
||||
print(' Format: [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
|
||||
|
||||
|
||||
def on_f2():
|
||||
"""Crop entities from latest capture using annotations JSON."""
|
||||
print("\n[=== CROPPING ENTITIES ===]")
|
||||
if not os.path.exists(LATEST_PATH):
|
||||
print(" [ERROR] No capture found. Press F1 first.")
|
||||
return
|
||||
if not os.path.exists(ANNOTATIONS_PATH):
|
||||
print(" [ERROR] No annotations found.")
|
||||
print(f" Create: {ANNOTATIONS_PATH}")
|
||||
print(' [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
|
||||
return
|
||||
|
||||
img = cv2.imread(LATEST_PATH)
|
||||
with open(ANNOTATIONS_PATH) as f:
|
||||
entities = json.load(f)
|
||||
|
||||
print(f" Image: {img.shape[1]}x{img.shape[0]}, Entities: {len(entities)}")
|
||||
|
||||
saved = 0
|
||||
for ent in entities:
|
||||
name = ent['name'].lower().replace(' ', '_')
|
||||
x, y = int(ent['x']), int(ent['y'])
|
||||
w, h = int(ent['w']), int(ent['h'])
|
||||
i_w, i_h = img.shape[1], img.shape[0]
|
||||
|
||||
# Crop with 5px padding
|
||||
pad = 5
|
||||
x1, y1 = max(0, x - pad), max(0, y - pad)
|
||||
x2, y2 = min(i_w, x + w + pad), min(i_h, y + h + pad)
|
||||
crop = img[y1:y2, x1:x2]
|
||||
|
||||
# Route to npc or enemies folder
|
||||
if name in NPC_NAMES:
|
||||
save_dir = NPC_DIR
|
||||
else:
|
||||
save_dir = ENEMIES_DIR
|
||||
|
||||
# Auto-number duplicates
|
||||
fname = f"{name}.png"
|
||||
save_path = os.path.join(save_dir, fname)
|
||||
variant = 1
|
||||
while os.path.exists(save_path):
|
||||
variant += 1
|
||||
fname = f"{name}_{variant}.png"
|
||||
save_path = os.path.join(save_dir, fname)
|
||||
|
||||
cv2.imwrite(save_path, crop)
|
||||
print(f" [SAVED] {save_path} ({crop.shape[1]}x{crop.shape[0]})")
|
||||
saved += 1
|
||||
|
||||
print(f"\n Total: {saved} assets cropped.")
|
||||
|
||||
|
||||
def on_f3():
|
||||
"""List existing assets."""
|
||||
print("\n[=== ASSETS INVENTORY ===]")
|
||||
for label, d in [("enemies", ENEMIES_DIR), ("npc", NPC_DIR)]:
|
||||
if os.path.isdir(d):
|
||||
files = sorted(os.listdir(d))
|
||||
print(f"\n assets/{label}/ ({len(files)} files):")
|
||||
for f in files:
|
||||
sz = os.path.getsize(os.path.join(d, f))
|
||||
print(f" {f} ({sz}b)")
|
||||
else:
|
||||
print(f"\n assets/{label}/ - EMPTY")
|
||||
|
||||
|
||||
def run():
|
||||
print("=== D2R Asset Extractor ===")
|
||||
print(" F1 - Capture D2R screen")
|
||||
print(" F2 - Crop entities from annotations")
|
||||
print(" F3 - List assets")
|
||||
print(" F12 - Exit")
|
||||
print("Ready.")
|
||||
|
||||
keyboard.add_hotkey('f1', on_f1)
|
||||
keyboard.add_hotkey('f2', on_f2)
|
||||
keyboard.add_hotkey('f3', on_f3)
|
||||
keyboard.add_hotkey('f12', lambda: (print("\nBye."), sys.exit(0)))
|
||||
keyboard.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"Gamma": 155,
|
||||
"GammaHD": 3024,
|
||||
"PaperWhiteNits": 200,
|
||||
"MaxLuminance": 600,
|
||||
"HDRContrast": 200,
|
||||
"Contrast": 100,
|
||||
"Screen Resolution (Windowed)": "1280x720",
|
||||
"Resolution Scale": 100,
|
||||
"Sharpening": 6,
|
||||
"Game Resolution": 1,
|
||||
"Light Quality": 2,
|
||||
"Blended Shadows": 0,
|
||||
"Perspective": 0,
|
||||
"VSync": 1,
|
||||
"Framerate Cap": 60,
|
||||
"Framerate Target": 0,
|
||||
"Window Mode": 0,
|
||||
"Graphic Presets": 4,
|
||||
"Texture Quality": 4,
|
||||
"Texture Anisotropy": 0,
|
||||
"Ambient Occlusion Quality": 0,
|
||||
"Character Detail": 2,
|
||||
"Environment Detail": 2,
|
||||
"Atmospherics Quality": 2,
|
||||
"Transparency Quality": 3,
|
||||
"Shadow Quality": 1,
|
||||
"Anti Aliasing": 0,
|
||||
"Dynamic Resolution Scaling": 0,
|
||||
"Vfx Lighting Quality": 0,
|
||||
"Safe Screen Percent": 100,
|
||||
"Graphics Mode": 0,
|
||||
"NVIDIA DLSS": 0,
|
||||
"Chat Font Size": 0,
|
||||
"Combat Feedback": 0,
|
||||
"Camera Shake": 0,
|
||||
"Low Vision Mode": 1,
|
||||
"Color Blind Mode": 0,
|
||||
"Color Blind Strength": 100,
|
||||
"Show Clock": 1,
|
||||
"Auto Gold Enabled": 0,
|
||||
"Chat Gem Mode": 1,
|
||||
"Item Name Display": 1,
|
||||
"Chat Background": 0,
|
||||
"Always Run": 1,
|
||||
"Quick Cast Enabled": 0,
|
||||
"Display Active Skill Bindings": 0,
|
||||
"Lobby Wide Item Drop Enabled": 1,
|
||||
"Item Tooltip Hotkey Appender": 1
|
||||
}
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1000" height="1000" viewBox="0 0 500 500" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 4.6.0</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="matrix(3.38 0 0 3.38 233.34 205.97)" id="WXmXLv3kDRvQZFKKhwmoc" >
|
||||
<path style="stroke: rgb(255,255,255); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,204,0); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(0, 0)" d="M 20.11463 -12.42163 C 20.11463 -12.42163 21.054029999999997 -12.422270000000001 21.18592 -12.422270000000001 C 27.95065 -12.422270000000001 32.76374 -6.723290000000001 32.76374 0.0414399999999997 C 32.76374 6.80617 27.273519999999998 12.29638 20.5088 12.29638 C 20.376920000000002 12.29638 20.24552 12.294289999999998 20.114630000000002 12.290149999999999 L 20.114630000000002 12.422289999999998 L -20.35981 12.422289999999998 L -20.35981 12.390709999999999 C -20.42786 12.411189999999998 -20.49745 12.421649999999998 -20.56858 12.421649999999998 C -27.3003 12.421649999999998 -32.76373 6.958219999999998 -32.76373 0.22649999999999793 C -32.76373 -6.505220000000002 -27.300300000000004 -12.422280000000002 -20.568580000000004 -12.422280000000002 C -20.497540000000004 -12.422280000000002 -20.427950000000003 -12.410510000000002 -20.359810000000003 -12.387490000000003 L -20.359810000000003 -12.421640000000004 z M 13.04412 0.06697 C 13.04412 3.91695 16.16874 7.041580000000001 20.018729999999998 7.041580000000001 C 23.868709999999997 7.041580000000001 26.993339999999996 3.9169600000000004 26.993339999999996 0.06697000000000042 C 26.993339999999996 -3.7830099999999995 23.868719999999996 -6.90764 20.018729999999998 -6.90764 C 16.16875 -6.90764 13.044119999999998 -3.7830199999999996 13.044119999999998 0.06697000000000042 z M -26.34205 -0.03785 C -26.34205 3.80294 -23.22488 6.92011 -19.38409 6.92011 C -15.5433 6.92011 -12.42613 3.80294 -12.42613 -0.03784999999999972 C -12.42613 -3.87864 -15.5433 -6.99581 -19.38409 -6.99581 C -23.22488 -6.99581 -26.34205 -3.8786399999999994 -26.34205 -0.03784999999999972 z" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(2.85 0 0 2.85 87.47 456.33)" id="4RuCYS-EmvqghTdclNusG" >
|
||||
<path style="stroke: rgb(0,0,0); stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(0, 0)" d="M 0 -5.81981 L 0 5.81981 z" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(1.59 0 0 1.59 229.32 80.29)" id="EYD8Y-Ekuul_6vBNnDJuK" >
|
||||
<path style="stroke: rgb(192,112,149); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,204,0); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(0, 0)" d="M 0 -13.60039 C 7.50742 -13.60039 13.60039 -7.507410000000001 13.60039 0 C 13.60039 7.50742 7.507410000000001 13.60039 0 13.60039 C -7.50742 13.60039 -13.60039 7.507410000000001 -13.60039 0 C -13.60039 -7.50742 -7.507410000000001 -13.60039 0 -13.60039 z" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(1.59 0 0 1.59 343.91 249.71)" id="kSSLBG7vJu3mdKM95YQ1B" >
|
||||
<path style="stroke: rgb(0,0,0); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(58,59,58); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-60.96, -144.2)" d="M 0 0 L 121.2 0 C 122.83563000000001 75.89475 121.2 288.4 121.2 288.4 L 0 288.4 L 0 263.04616 C 0 263.04616 55.45967 250.36722999999998 72.13509 172.04109 C 80.51684 132.67119 106.7569 66.36099999999999 52.314060000000005 41.19999999999999 C 42.40088 36.61855999999999 31.709200000000006 31.855419999999988 18.87402 29.445939999999986 C 16.26453 20.075869999999988 9.142270000000002 12.579989999999984 0 9.451059999999988 z" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(1.59 0 0 1.59 117.03 249.5)" id="Koi6HP8yZg5vpSkXY6fB0" >
|
||||
<path style="stroke: rgb(0,0,0); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(58,59,58); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-60.96, -144.2)" d="M 69.61289 41.2 C 15.170049999999996 66.361 41.41010999999999 132.67119000000002 49.79185999999999 172.04109 C 66.46727999999999 250.36723 121.92695 263.04616 121.92695 263.04616 L 121.92695 288.4 L 0.7269500000000022 288.4 C 0.7269500000000022 288.4 -0.9086799999999977 75.89474999999999 0.7269500000000022 0 L 121.92695 0 L 121.92695 9.65344 C 111.27788000000001 11.8682 102.7274 19.829929999999997 99.66922000000001 30.14492 C 88.31699 32.70352 78.64202 37.02713 69.61289000000001 41.2 z" stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "botty",
|
||||
"savepath": "../"
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |