Compare commits
1016 Commits
v2.9.4dev3
...
v2.20.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
855fafa94d | ||
|
|
4e10335ae7 | ||
|
|
21ea9ce579 | ||
|
|
ea07bbf4f9 | ||
|
|
8eed6fbe21 | ||
|
|
0859f2fbe9 | ||
|
|
71ba33edeb | ||
|
|
ce80d92b35 | ||
|
|
f17cf9b736 | ||
|
|
98579c774b | ||
|
|
509fa279e7 | ||
|
|
091ee87761 | ||
|
|
c0c20cc92e | ||
|
|
1341f7bca1 | ||
|
|
fe93db1d4b | ||
|
|
5db97ea773 | ||
|
|
1758e4f320 | ||
|
|
1a897c0419 | ||
|
|
32db3e3179 | ||
|
|
d830a8957a | ||
|
|
652ea48223 | ||
|
|
8c25b2b8f5 | ||
|
|
db4c56be8e | ||
|
|
f3bcffe2f9 | ||
|
|
bc5786d099 | ||
|
|
5959fe5daf | ||
|
|
649d338bb1 | ||
|
|
dcb058a718 | ||
|
|
1772bb5e7f | ||
|
|
30bd0adb06 | ||
|
|
44dfcf771c | ||
|
|
a1f8a7a930 | ||
|
|
b22887dfad | ||
|
|
28137fa3f4 | ||
|
|
9cbdc6055d | ||
|
|
fc93c61fcf | ||
|
|
3fa2e7ebd1 | ||
|
|
818628da0c | ||
|
|
adf90a8263 | ||
|
|
362923ac64 | ||
|
|
7d73838ce1 | ||
|
|
b3278ca9ec | ||
|
|
5707914ad5 | ||
|
|
9b697b24d8 | ||
|
|
098b088da6 | ||
|
|
14d62f31e1 | ||
|
|
e7b1f55d08 | ||
|
|
8f501896a1 | ||
|
|
befcb9b874 | ||
|
|
c70afa9a4c | ||
|
|
029c7dd4c2 | ||
|
|
1f83dba1ac | ||
|
|
6eae3405fd | ||
|
|
c0e1d9e4de | ||
|
|
7c0bd7aa88 | ||
|
|
394583584c | ||
|
|
4112e2aa6b | ||
|
|
aa19e0da72 | ||
|
|
bba9be1598 | ||
|
|
17998916b4 | ||
|
|
543089bcd9 | ||
|
|
7f35c78a65 | ||
|
|
b25798dd83 | ||
|
|
744b9ff78a | ||
|
|
8dc1457ebb | ||
|
|
982ad54fab | ||
|
|
912192cd7d | ||
|
|
74cd6d48da | ||
|
|
3467a7fe3f | ||
|
|
eb269a05ed | ||
|
|
c63cf4b3b0 | ||
|
|
11ed94454d | ||
|
|
8df07645da | ||
|
|
5666fdd250 | ||
|
|
0b55ae40fc | ||
|
|
3726e84697 | ||
|
|
a0c4341102 | ||
|
|
cd6b1038e8 | ||
|
|
10583fd506 | ||
|
|
713694be56 | ||
|
|
f50293cf77 | ||
|
|
7c88fa477f | ||
|
|
f8df540fad | ||
|
|
61a01805cc | ||
|
|
a93915cf04 | ||
|
|
0f74c97fbf | ||
|
|
29713b69dc | ||
|
|
cc3c2cb9c8 | ||
|
|
b36a3959da | ||
|
|
3b26bfc0e9 | ||
|
|
c59b621963 | ||
|
|
977a8fa329 | ||
|
|
a5226fee83 | ||
|
|
e4069a3988 | ||
|
|
6527f9e11e | ||
|
|
9ddfcc894f | ||
|
|
f22a4f13e5 | ||
|
|
fc9337df67 | ||
|
|
98a8332cfa | ||
|
|
a78eedad48 | ||
|
|
85e1a1bd06 | ||
|
|
25b2c04309 | ||
|
|
54a84d0e42 | ||
|
|
af0a4d1def | ||
|
|
97cdde84ce | ||
|
|
a65129b277 | ||
|
|
3b40a49918 | ||
|
|
d95345b476 | ||
|
|
2ba2d95017 | ||
|
|
4d0ffedcc8 | ||
|
|
84abde4fc5 | ||
|
|
8897f1e4b1 | ||
|
|
6f33cacb7a | ||
|
|
b1a8c0ad09 | ||
|
|
13ed635803 | ||
|
|
5a44909ebf | ||
|
|
6cbb80693d | ||
|
|
0b90d254f9 | ||
|
|
641d36205c | ||
|
|
6e38b6ea4d | ||
|
|
84b4d8fabb | ||
|
|
c0e1f7e746 | ||
|
|
d734c12168 | ||
|
|
340d94eb3e | ||
|
|
9b0b31648c | ||
|
|
8346358c37 | ||
|
|
b935700a12 | ||
|
|
2f4a5a4830 | ||
|
|
e951ce8e9d | ||
|
|
fc6d42f483 | ||
|
|
7df1431ad2 | ||
|
|
4d4680961e | ||
|
|
1cae99b812 | ||
|
|
3727d19311 | ||
|
|
c45c093f4f | ||
|
|
b613d3e312 | ||
|
|
4cff9247b0 | ||
|
|
b9a26ec28d | ||
|
|
245f81e888 | ||
|
|
204c1ba9f2 | ||
|
|
a7a4be133c | ||
|
|
14f9a46d7a | ||
|
|
003dd040dc | ||
|
|
97cf53217e | ||
|
|
7c1805826d | ||
|
|
d6a2b4dfc4 | ||
|
|
d70952520a | ||
|
|
a57867b806 | ||
|
|
17d7583cfc | ||
|
|
55ad95081b | ||
|
|
da8da1759f | ||
|
|
b4e758b9ee | ||
|
|
3ba41db699 | ||
|
|
cfc47cf483 | ||
|
|
38c8be995b | ||
|
|
492207700b | ||
|
|
b69adefbf8 | ||
|
|
361299f51e | ||
|
|
025091c3f6 | ||
|
|
a54d70036b | ||
|
|
9e67b5962c | ||
|
|
ed0c080163 | ||
|
|
21389d84fc | ||
|
|
0fe54631ad | ||
|
|
36ea5200f2 | ||
|
|
d9934160a6 | ||
|
|
b9c92c48d3 | ||
|
|
6d23df6156 | ||
|
|
a913ab72c2 | ||
|
|
f6d33a2ac1 | ||
|
|
5a3fec33a7 | ||
|
|
95734aca2c | ||
|
|
377254be9d | ||
|
|
ad9905a5e0 | ||
|
|
6fc5a9699e | ||
|
|
0ee86f4836 | ||
|
|
85593367f7 | ||
|
|
465ea61b06 | ||
|
|
2d5507e951 | ||
|
|
84e20be153 | ||
|
|
60080c02b0 | ||
|
|
2a0d4179d0 | ||
|
|
6dd55e0cd4 | ||
|
|
0adc6e0c6f | ||
|
|
e155356701 | ||
|
|
ef44b50980 | ||
|
|
6785d16c87 | ||
|
|
76c670b7d5 | ||
|
|
717303a72f | ||
|
|
e13125b174 | ||
|
|
8aa7964b81 | ||
|
|
5e5f23d296 | ||
|
|
05837f99ff | ||
|
|
70f620c2c3 | ||
|
|
10ffb987ea | ||
|
|
13ed785551 | ||
|
|
69d2e38647 | ||
|
|
c45abbdbcf | ||
|
|
2655b001a9 | ||
|
|
3c7f4edb1b | ||
|
|
663cbab401 | ||
|
|
897ca9ca43 | ||
|
|
b31a071859 | ||
|
|
8c2788fd78 | ||
|
|
dee8fa400c | ||
|
|
2339327473 | ||
|
|
f6f9239dd5 | ||
|
|
cd4c8c8c10 | ||
|
|
c25eda8b64 | ||
|
|
b6edf0e034 | ||
|
|
af4277fc7e | ||
|
|
acd774abe5 | ||
|
|
9081353634 | ||
|
|
9c7fa37a72 | ||
|
|
d92e11893a | ||
|
|
f55ab00bf5 | ||
|
|
36265aa2a3 | ||
|
|
bfedcdbffd | ||
|
|
a5d10c4a76 | ||
|
|
2962ce1945 | ||
|
|
0063d2955e | ||
|
|
9787a18666 | ||
|
|
d8cd3197b5 | ||
|
|
e07c4f65ab | ||
|
|
c3108f773a | ||
|
|
a400821268 | ||
|
|
ca4bac07da | ||
|
|
0038eacc3f | ||
|
|
4bd633a0d4 | ||
|
|
ee837f0b04 | ||
|
|
016e18cc89 | ||
|
|
1d6ac53183 | ||
|
|
1e32dfa463 | ||
|
|
4431753570 | ||
|
|
6fdb57318c | ||
|
|
649db019de | ||
|
|
1d528be0ef | ||
|
|
2d6f6df83d | ||
|
|
7fa998f276 | ||
|
|
6a3157a4c8 | ||
|
|
74efa50576 | ||
|
|
e99be78c54 | ||
|
|
e386232de1 | ||
|
|
a15896044d | ||
|
|
72c74513ce | ||
|
|
34d6d13cb2 | ||
|
|
323bb8e064 | ||
|
|
b628f8ef9b | ||
|
|
1900216d36 | ||
|
|
c045ed81c1 | ||
|
|
39edec60e3 | ||
|
|
259214e907 | ||
|
|
2e1c53392d | ||
|
|
7a77d12686 | ||
|
|
763a7714ed | ||
|
|
e89f35d654 | ||
|
|
3196751b7a | ||
|
|
f221024974 | ||
|
|
66cab8a0d4 | ||
|
|
daa49a8cd4 | ||
|
|
b8601ff240 | ||
|
|
5df3eeca64 | ||
|
|
274e4ac2ca | ||
|
|
f6485251ca | ||
|
|
c03fb70def | ||
|
|
8652a2891b | ||
|
|
a09af0a9eb | ||
|
|
c7d4b93fba | ||
|
|
745c0db530 | ||
|
|
aa56ab8d6c | ||
|
|
043c430221 | ||
|
|
dc5cc5855e | ||
|
|
59d6266e2b | ||
|
|
7495ba67f8 | ||
|
|
e649683a4d | ||
|
|
bec58a5772 | ||
|
|
e8f9ae8a9c | ||
|
|
679382e220 | ||
|
|
7f86782f54 | ||
|
|
f80b7d972f | ||
|
|
c0bd489c1b | ||
|
|
41e4c2107d | ||
|
|
cfc95c272a | ||
|
|
f778f9ceae | ||
|
|
7fb6170bcb | ||
|
|
fa37428cd3 | ||
|
|
2a2d9d3456 | ||
|
|
a91efb681f | ||
|
|
386e05be8f | ||
|
|
65e7bf609d | ||
|
|
7a58d97652 | ||
|
|
c5118da417 | ||
|
|
13b505525d | ||
|
|
b682dec363 | ||
|
|
6aa98e2214 | ||
|
|
8fba988222 | ||
|
|
24a82efe50 | ||
|
|
8054fa9267 | ||
|
|
a0e39a3725 | ||
|
|
185d6d0c51 | ||
|
|
1975e96848 | ||
|
|
ab37d228ea | ||
|
|
f48483d754 | ||
|
|
e6cfd33435 | ||
|
|
c29126ce1d | ||
|
|
c52170b731 | ||
|
|
6607dd31bf | ||
|
|
c6c74be38d | ||
|
|
41c6062ff9 | ||
|
|
bbb96a73b7 | ||
|
|
9eb3b9e017 | ||
|
|
6b3e94729c | ||
|
|
fb48f2b5d4 | ||
|
|
cfffc77777 | ||
|
|
f7089f358d | ||
|
|
06c4f2ce46 | ||
|
|
83eb0abd92 | ||
|
|
4199b33c47 | ||
|
|
23cd4bff5a | ||
|
|
b65f95fe77 | ||
|
|
32160c94e1 | ||
|
|
ac02fba98b | ||
|
|
cde0108cba | ||
|
|
39dc7e4a46 | ||
|
|
9943f784a8 | ||
|
|
88ce45f29e | ||
|
|
7157e876ca | ||
|
|
0cf88cf7ca | ||
|
|
10dfdc3627 | ||
|
|
76bdefcda6 | ||
|
|
1c2c8cc5f9 | ||
|
|
58f853de5b | ||
|
|
c052297bf7 | ||
|
|
9e78cd1076 | ||
|
|
79f4deacea | ||
|
|
ff42c4c711 | ||
|
|
02d31d49d8 | ||
|
|
64f47fcc24 | ||
|
|
0ceb8acd64 | ||
|
|
78579e2e13 | ||
|
|
cf4e1d3935 | ||
|
|
d1be0bb680 | ||
|
|
e70ebad3f7 | ||
|
|
4dfe5c3abf | ||
|
|
384d9f4614 | ||
|
|
47434c68f9 | ||
|
|
af88afb6b5 | ||
|
|
536eb1efa5 | ||
|
|
c4c763089e | ||
|
|
cdfd4c0d8e | ||
|
|
f9bb8836e5 | ||
|
|
58b2634c8c | ||
|
|
093ae008ce | ||
|
|
5f62fc0cdc | ||
|
|
e7a4b4ac26 | ||
|
|
66e9944cb5 | ||
|
|
ad8528c248 | ||
|
|
07d22cd8e4 | ||
|
|
a6d5922d77 | ||
|
|
958d7bff99 | ||
|
|
7819b80be4 | ||
|
|
2ca50a4658 | ||
|
|
09ff4fd128 | ||
|
|
3e53863f9e | ||
|
|
63d2289f97 | ||
|
|
2663ef2e66 | ||
|
|
d4bdf47d62 | ||
|
|
660ee7c4bf | ||
|
|
1db1f3070b | ||
|
|
3dba82c497 | ||
|
|
25e7b7a9f7 | ||
|
|
9582212ae0 | ||
|
|
7d2b60c327 | ||
|
|
0121a0064e | ||
|
|
2aa96fc819 | ||
|
|
8d81db0a3a | ||
|
|
e5ba35fde9 | ||
|
|
885cd32cb0 | ||
|
|
18d8ed6558 | ||
|
|
9618ece4b4 | ||
|
|
a80a77a422 | ||
|
|
3806be3ddd | ||
|
|
3e803fef30 | ||
|
|
12956d435a | ||
|
|
a3381007f3 | ||
|
|
1efe4ee5e5 | ||
|
|
ec21f93d3c | ||
|
|
f384b32ed6 | ||
|
|
22d8f34c75 | ||
|
|
6128cd8322 | ||
|
|
386f403430 | ||
|
|
5f7d9aea89 | ||
|
|
b367c449a9 | ||
|
|
26b3dff9d4 | ||
|
|
873a62e3f0 | ||
|
|
d967ab375e | ||
|
|
fcf2d6a72c | ||
|
|
843ced15bf | ||
|
|
813db9340f | ||
|
|
acbd8a3298 | ||
|
|
561e22e894 | ||
|
|
05ac0a528a | ||
|
|
c040353f6e | ||
|
|
f23a8fa0c8 | ||
|
|
ba93467646 | ||
|
|
00d480860f | ||
|
|
c94384acb8 | ||
|
|
0c2c0ac6ef | ||
|
|
61a33a331e | ||
|
|
e374a6f2c6 | ||
|
|
dbd84dce28 | ||
|
|
9d554f9c68 | ||
|
|
576cf56735 | ||
|
|
e2aaabbc16 | ||
|
|
ef226898c0 | ||
|
|
a0db235e5a | ||
|
|
5bf05ba775 | ||
|
|
c073b1fa2a | ||
|
|
5f58307bf3 | ||
|
|
8741b17a5e | ||
|
|
4c1fa09795 | ||
|
|
ce7df2d01f | ||
|
|
b433b0ea7c | ||
|
|
20868d6b44 | ||
|
|
33103dbee9 | ||
|
|
2a05ac5a85 | ||
|
|
a013828128 | ||
|
|
e19510b3d4 | ||
|
|
390f2048f2 | ||
|
|
0bb732300e | ||
|
|
fd017df561 | ||
|
|
0ed16b9a6f | ||
|
|
865978fcc1 | ||
|
|
a43f9930de | ||
|
|
c13cd23d54 | ||
|
|
ed1f52a114 | ||
|
|
7dd063f04e | ||
|
|
6e9fc1d1d9 | ||
|
|
cae0172e48 | ||
|
|
e2b492ee8d | ||
|
|
545ddc7492 | ||
|
|
d0b7c58a1d | ||
|
|
a9ad094422 | ||
|
|
68154333c2 | ||
|
|
5df2db5879 | ||
|
|
5a34db0d2f | ||
|
|
6f50be1e7e | ||
|
|
d15fefcf1b | ||
|
|
07bf1b400c | ||
|
|
9f975a958e | ||
|
|
c2a240bab0 | ||
|
|
40c3bf723f | ||
|
|
7a92ace2db | ||
|
|
500f5b8310 | ||
|
|
44830a4de6 | ||
|
|
f3f13e7ba8 | ||
|
|
0269a64ae1 | ||
|
|
5d6cdcbd23 | ||
|
|
81906a7bd2 | ||
|
|
b25b038934 | ||
|
|
b469fa520e | ||
|
|
4f865896c7 | ||
|
|
3b50dddef2 | ||
|
|
380e9c2e87 | ||
|
|
1c1443c862 | ||
|
|
0529baac4a | ||
|
|
7dab220009 | ||
|
|
0ea0f8cdf2 | ||
|
|
eebd59413b | ||
|
|
f4a635eb43 | ||
|
|
0e57258cc5 | ||
|
|
67462c3278 | ||
|
|
fce8129fa2 | ||
|
|
1d76f3ec31 | ||
|
|
707dbeecf8 | ||
|
|
56672f5830 | ||
|
|
13a0bf9d42 | ||
|
|
1bff10c973 | ||
|
|
1d4aece7cc | ||
|
|
cdb79f499a | ||
|
|
837dbb3677 | ||
|
|
3d3bf4ce2c | ||
|
|
46ae2a006e | ||
|
|
0062206f87 | ||
|
|
b6642aa76c | ||
|
|
522f1c8314 | ||
|
|
6fceda5f27 | ||
|
|
668a947543 | ||
|
|
f3eadc9ef1 | ||
|
|
7693483720 | ||
|
|
219e1c11dc | ||
|
|
99c6614d86 | ||
|
|
93af60c3d0 | ||
|
|
b4789bbebf | ||
|
|
160c2f0942 | ||
|
|
9605efe643 | ||
|
|
56e09b8528 | ||
|
|
7dc17543df | ||
|
|
3793721dc3 | ||
|
|
54aa284fd9 | ||
|
|
207818537b | ||
|
|
d32ff668e1 | ||
|
|
1b2cd62629 | ||
|
|
2d96af9fc8 | ||
|
|
20422d3046 | ||
|
|
0e2ae0e0f0 | ||
|
|
9572a51f28 | ||
|
|
cf6f884b3b | ||
|
|
4ca737281d | ||
|
|
847f4e343e | ||
|
|
e0db6eb2ad | ||
|
|
c7f625456e | ||
|
|
332eb048a1 | ||
|
|
e2c7f169fd | ||
|
|
4eb68c7c13 | ||
|
|
c1228b95fe | ||
|
|
2ab61f2b9e | ||
|
|
d266aa796e | ||
|
|
e488497a42 | ||
|
|
e8766817f8 | ||
|
|
d577b1d1c6 | ||
|
|
7984b57494 | ||
|
|
567e8df174 | ||
|
|
65e7607221 | ||
|
|
6e3b536d83 | ||
|
|
348c4d71df | ||
|
|
9494885f45 | ||
|
|
01bda70fef | ||
|
|
8b2f9ce59d | ||
|
|
60a8e905b8 | ||
|
|
d9a4b0a359 | ||
|
|
2d43a6ade5 | ||
|
|
d0a56e3ee8 | ||
|
|
9b15f1942d | ||
|
|
edd261c677 | ||
|
|
3c967ba9eb | ||
|
|
8332ccaa7a | ||
|
|
26b1610ca5 | ||
|
|
ae1a5f4e44 | ||
|
|
4594f57961 | ||
|
|
c1d0849f87 | ||
|
|
313264a49f | ||
|
|
a724347236 | ||
|
|
df7ad187f5 | ||
|
|
c142a011a0 | ||
|
|
477c43884a | ||
|
|
f3551ce570 | ||
|
|
6baa9dd322 | ||
|
|
302975c243 | ||
|
|
73f75fb44e | ||
|
|
7f651b144f | ||
|
|
ff98658491 | ||
|
|
53db5943b1 | ||
|
|
33b7ab0d98 | ||
|
|
f0af93f8b9 | ||
|
|
379fd2353a | ||
|
|
98f0766425 | ||
|
|
be07a4735c | ||
|
|
d736a10dc9 | ||
|
|
c5c3b9cba1 | ||
|
|
bfa9ad4d96 | ||
|
|
2f8ece9080 | ||
|
|
c7e769e42e | ||
|
|
3ab06d0832 | ||
|
|
251bc71f86 | ||
|
|
3f3870bb30 | ||
|
|
eb9612b9a3 | ||
|
|
1d7efce197 | ||
|
|
ff60cf313e | ||
|
|
a275878ba0 | ||
|
|
09fb4c1d35 | ||
|
|
483c1c35fd | ||
|
|
810b8be92a | ||
|
|
679ed7b806 | ||
|
|
e1896c0216 | ||
|
|
01310c166e | ||
|
|
3bc93899fe | ||
|
|
69bd988174 | ||
|
|
20bee1196a | ||
|
|
9803da1825 | ||
|
|
bc61e32ee7 | ||
|
|
4f784e2eea | ||
|
|
931d8d355f | ||
|
|
6912b6eb39 | ||
|
|
163d2c9b10 | ||
|
|
cc8def1cf5 | ||
|
|
bcdefdc4ac | ||
|
|
4f2e1be9ac | ||
|
|
663623dec6 | ||
|
|
cecf5d7e31 | ||
|
|
8a4caeaa2d | ||
|
|
f8062ba39f | ||
|
|
3e6e3b0743 | ||
|
|
1e35eaf62a | ||
|
|
8a3dc2f3dc | ||
|
|
34a6fdc07e | ||
|
|
fc65cb6000 | ||
|
|
247add778d | ||
|
|
ab1071f1d7 | ||
|
|
f5cb5c3993 | ||
|
|
ea7f122030 | ||
|
|
2160cc4aaa | ||
|
|
817e99a05d | ||
|
|
53f5656478 | ||
|
|
42d11bd3f1 | ||
|
|
a028ebe198 | ||
|
|
c315adf987 | ||
|
|
01371f227c | ||
|
|
3174deed99 | ||
|
|
6db178e4d2 | ||
|
|
eb07e03e93 | ||
|
|
a881cd2bcc | ||
|
|
676b09720a | ||
|
|
ac7b6d9ecd | ||
|
|
49cb81b516 | ||
|
|
27e361dc5b | ||
|
|
4c330bfb16 | ||
|
|
fcb85c85a3 | ||
|
|
9be1b96226 | ||
|
|
11598f9a09 | ||
|
|
39e23237a5 | ||
|
|
22507673aa | ||
|
|
7fdcd4aa15 | ||
|
|
c559508175 | ||
|
|
95621b6aab | ||
|
|
fb93aa1ad5 | ||
|
|
035c69c60a | ||
|
|
788bbb5d25 | ||
|
|
04178ca824 | ||
|
|
647bdb78df | ||
|
|
ce9099a25b | ||
|
|
4e715750a5 | ||
|
|
b330f72326 | ||
|
|
dd5c95d2f2 | ||
|
|
ae5e0cc71a | ||
|
|
cfe0e36e48 | ||
|
|
63a362286e | ||
|
|
2155aa0d21 | ||
|
|
f52fda3f03 | ||
|
|
f315f8b85a | ||
|
|
72e56246f4 | ||
|
|
5102cb35c8 | ||
|
|
170853f0f4 | ||
|
|
7bc0d88898 | ||
|
|
4834cfe8ca | ||
|
|
2c2065119b | ||
|
|
636672fdce | ||
|
|
bc8c70fa9c | ||
|
|
e3ac9a7722 | ||
|
|
e14d3d7214 | ||
|
|
5b898a678b | ||
|
|
e2d6baaeb1 | ||
|
|
eb87ba1d89 | ||
|
|
0257e70c29 | ||
|
|
70d1a3534b | ||
|
|
16f4903eba | ||
|
|
6b77d72f06 | ||
|
|
ac5768e666 | ||
|
|
f3bd47f347 | ||
|
|
1fbb47d64b | ||
|
|
3797887abc | ||
|
|
8870eef79b | ||
|
|
eefcd9e738 | ||
|
|
5f296bbe30 | ||
|
|
d88fa1131d | ||
|
|
3c6071ad88 | ||
|
|
deb772f0a7 | ||
|
|
858719aad8 | ||
|
|
14debfd25c | ||
|
|
510c9cafec | ||
|
|
6434902f86 | ||
|
|
16be84420b | ||
|
|
8f2283f9aa | ||
|
|
920b84886c | ||
|
|
cb2f0e40ba | ||
|
|
14a9c9910c | ||
|
|
c8d0ae8659 | ||
|
|
fd541ead6c | ||
|
|
71e7ea0230 | ||
|
|
cab2d41269 | ||
|
|
f5b1c79029 | ||
|
|
885a3f1ac9 | ||
|
|
e821b2d09c | ||
|
|
1b2bff8a77 | ||
|
|
d213e94860 | ||
|
|
d2b71d97d2 | ||
|
|
1ff7bdf1a7 | ||
|
|
f221f2df4f | ||
|
|
46f365c42d | ||
|
|
a53c00aeda | ||
|
|
044818aa65 | ||
|
|
a55084dbae | ||
|
|
c2c9528e80 | ||
|
|
9c7ad95f6e | ||
|
|
fe9dc0a3e5 | ||
|
|
25712ef778 | ||
|
|
a63b543e0c | ||
|
|
8ebec1f957 | ||
|
|
0733fee878 | ||
|
|
d52dd535a3 | ||
|
|
cbc6475875 | ||
|
|
c6de92592c | ||
|
|
5f97734881 | ||
|
|
62fbb7c9c8 | ||
|
|
4ddbdebae4 | ||
|
|
542b79fa00 | ||
|
|
9f6f5c8a76 | ||
|
|
8591f649d1 | ||
|
|
3bbd51614d | ||
|
|
cb20c8588f | ||
|
|
10b1c6ebfb | ||
|
|
3c6739b83a | ||
|
|
57426f783e | ||
|
|
0788ff050d | ||
|
|
18d59c119c | ||
|
|
ae34cd5422 | ||
|
|
50807e9381 | ||
|
|
fdb4d4d443 | ||
|
|
ff22f12a56 | ||
|
|
15dc2a325a | ||
|
|
ee9c1db000 | ||
|
|
b3b134ea45 | ||
|
|
2e9b024390 | ||
|
|
ee2193e1bb | ||
|
|
a582cf93bd | ||
|
|
fc2d7cf7b8 | ||
|
|
61836dbb83 | ||
|
|
154122388e | ||
|
|
0114417018 | ||
|
|
e662edc2cc | ||
|
|
9e6cdb2f4f | ||
|
|
98b1fdb476 | ||
|
|
0f0e544f54 | ||
|
|
6d50f03396 | ||
|
|
7ec9d3f122 | ||
|
|
592adb36f1 | ||
|
|
d571191ec2 | ||
|
|
1f5fe47580 | ||
|
|
1e3783c21d | ||
|
|
7190d91d31 | ||
|
|
5f697c166a | ||
|
|
68b5fd9893 | ||
|
|
3e1a91d073 | ||
|
|
c68451228a | ||
|
|
77ae235385 | ||
|
|
c4009bdbd7 | ||
|
|
1eb48b00e1 | ||
|
|
67cef93dd8 | ||
|
|
2aa274f56f | ||
|
|
cd20164d7a | ||
|
|
5a0ca503c1 | ||
|
|
4c1c15e69e | ||
|
|
0320a16ba4 | ||
|
|
fcc8f3c5a7 | ||
|
|
fae3e8568a | ||
|
|
4d2bb5ba87 | ||
|
|
d71cf64564 | ||
|
|
1bb30499c2 | ||
|
|
d8deb98d7b | ||
|
|
e1078ef6da | ||
|
|
a1d807bd45 | ||
|
|
4f3228388c | ||
|
|
81e3edc041 | ||
|
|
077db2ecd6 | ||
|
|
6d2746ad75 | ||
|
|
0ffdae97fd | ||
|
|
70fd1ac6de | ||
|
|
78d056c6ff | ||
|
|
43ba63233d | ||
|
|
adf750fe44 | ||
|
|
250996e8ac | ||
|
|
56639a0812 | ||
|
|
c12e450648 | ||
|
|
4fce6f7b99 | ||
|
|
27b8c12639 | ||
|
|
5d5d9ff153 | ||
|
|
d803c8374f | ||
|
|
a5b22aa112 | ||
|
|
ae8fb25d3f | ||
|
|
530dd1c03b | ||
|
|
473b65850d | ||
|
|
2c49bde5bf | ||
|
|
97b32b33d3 | ||
|
|
1382e87133 | ||
|
|
19d03591b1 | ||
|
|
6f1321aa13 | ||
|
|
365a3798c2 | ||
|
|
8d3981e1a4 | ||
|
|
e34fcb2f9c | ||
|
|
71f1c69f23 | ||
|
|
072ad028a3 | ||
|
|
a652e12fa4 | ||
|
|
58f3618350 | ||
|
|
1b26cee9c1 | ||
|
|
4752e5a20f | ||
|
|
d8e277593d | ||
|
|
edbc341909 | ||
|
|
5f20f249f7 | ||
|
|
a1de3b9225 | ||
|
|
5110e63809 | ||
|
|
26d4cfa2de | ||
|
|
ae1a9950bc | ||
|
|
1c120f2fd6 | ||
|
|
d7e45b0f76 | ||
|
|
e796b748b6 | ||
|
|
a74984d37b | ||
|
|
e342f96fbe | ||
|
|
e262aa7daa | ||
|
|
c64d09ca54 | ||
|
|
8def076175 | ||
|
|
a64fbd8976 | ||
|
|
eda869fe0d | ||
|
|
04a74e278b | ||
|
|
6786cc7eff | ||
|
|
6984bd435f | ||
|
|
ec8b771a24 | ||
|
|
6ce72e4fb3 | ||
|
|
cda9ba5978 | ||
|
|
e2ae89f6b9 | ||
|
|
3bc3705c42 | ||
|
|
522de5ca5a | ||
|
|
efd8a6964e | ||
|
|
1cd10d2109 | ||
|
|
3a09f4b45c | ||
|
|
64bc2c34c2 | ||
|
|
845630437e | ||
|
|
c4484d735a | ||
|
|
5b74c6c5e1 | ||
|
|
3e410540c9 | ||
|
|
5bba1dc88b | ||
|
|
8c0cae8bc3 | ||
|
|
71e55a000b | ||
|
|
7bcdf95f5c | ||
|
|
4402addcb0 | ||
|
|
3d57861481 | ||
|
|
5d1d2b87df | ||
|
|
389b5d57aa | ||
|
|
53de46bab7 | ||
|
|
e6dce726b7 | ||
|
|
63ca8dc559 | ||
|
|
6e083a5af8 | ||
|
|
ac93c5487c | ||
|
|
1f94b28b87 | ||
|
|
78b6eb4283 | ||
|
|
417e478d27 | ||
|
|
78d2dff0d8 | ||
|
|
63c45c5060 | ||
|
|
c4f225003a | ||
|
|
185cf4f625 | ||
|
|
d2b838e9d5 | ||
|
|
15b6a848e8 | ||
|
|
193fcc60d8 | ||
|
|
ae110371fe | ||
|
|
5857413285 | ||
|
|
4448d7e62f | ||
|
|
d3ca0a961e | ||
|
|
405492d9d7 | ||
|
|
d27d7656d5 | ||
|
|
120bd9aa0c | ||
|
|
6ab79ab5c0 | ||
|
|
b8d189c0ad | ||
|
|
86e04321c8 | ||
|
|
6bcc906c4a | ||
|
|
c3becec822 | ||
|
|
aae2e7c531 | ||
|
|
52490144d3 | ||
|
|
c04c672f11 | ||
|
|
f51979b69a | ||
|
|
ab6b9759b0 | ||
|
|
b3027532ff | ||
|
|
494c9b08cb | ||
|
|
c595195519 | ||
|
|
c3efa819f4 | ||
|
|
4e7580b277 | ||
|
|
af642a4259 | ||
|
|
c365efb67e | ||
|
|
fc7613451e | ||
|
|
62b7b44120 | ||
|
|
744fce2e82 | ||
|
|
dd55493b4e | ||
|
|
7e7b49d2e4 | ||
|
|
24494e9b29 | ||
|
|
eff0510092 | ||
|
|
988688939b | ||
|
|
d448116e91 | ||
|
|
75ce6ffbcf | ||
|
|
60933a309f | ||
|
|
428cb5c888 | ||
|
|
d195ec7e68 | ||
|
|
c2017f3cb9 | ||
|
|
66ff4d827c | ||
|
|
745914bf9e | ||
|
|
421146eb54 | ||
|
|
ef81f9c830 | ||
|
|
1e760b2111 | ||
|
|
fe50372b12 | ||
|
|
7ef79eaa79 | ||
|
|
9b282587b2 | ||
|
|
4af36514bc | ||
|
|
5320e99276 | ||
|
|
b733205541 | ||
|
|
b125c62930 | ||
|
|
7895e4076d | ||
|
|
9ec192de7d | ||
|
|
8e41a31d1d | ||
|
|
fa4a2436aa | ||
|
|
22ca78cb68 | ||
|
|
ee4a1f936b | ||
|
|
15a8c5750a | ||
|
|
9f261f5b80 | ||
|
|
b6a58b4ba6 | ||
|
|
09ca85ca81 | ||
|
|
3aa69a6eaf | ||
|
|
2a645b1b04 | ||
|
|
0420f399ad | ||
|
|
509a45dcee | ||
|
|
52724d790b | ||
|
|
4b960af9ab | ||
|
|
022f0c06ee | ||
|
|
5ffd644ad9 | ||
|
|
03183827a6 | ||
|
|
5e7fcc32b6 | ||
|
|
3c0d87940b | ||
|
|
4cf07c4b76 | ||
|
|
28db388fa0 | ||
|
|
2c1905f041 | ||
|
|
30d03f0ab5 | ||
|
|
4ca3f10bc9 | ||
|
|
9cc228cfff | ||
|
|
3359d8cb88 | ||
|
|
738d7f687d | ||
|
|
b224196b05 | ||
|
|
bbcc32c8cf | ||
|
|
3c0b8643f6 | ||
|
|
c85b6e4a36 | ||
|
|
6003302e10 | ||
|
|
e7dd045979 | ||
|
|
6ca7a22c3e | ||
|
|
e8f09514ab | ||
|
|
1a3a656879 | ||
|
|
e77ada4e8c | ||
|
|
41b72c2789 | ||
|
|
a4be7c5e9a | ||
|
|
fb3c183b3e | ||
|
|
3e7dbef659 | ||
|
|
89260d1d36 | ||
|
|
d451bda7ed | ||
|
|
933c84466f | ||
|
|
d4c9100f77 | ||
|
|
c6aa72a3e3 | ||
|
|
7cf6ff04b6 | ||
|
|
5b575fdfe3 | ||
|
|
a8a5fabce7 | ||
|
|
f41d6dd2c1 | ||
|
|
09727c102a | ||
|
|
6580734dc7 | ||
|
|
ff34865067 | ||
|
|
bdd400fd51 | ||
|
|
1e8184a80b | ||
|
|
6a20f04c7f | ||
|
|
d81acc1f9c | ||
|
|
bc84c20cb2 | ||
|
|
d5c5e2698e | ||
|
|
16a78e689e | ||
|
|
066f29660d | ||
|
|
bba0df5f50 | ||
|
|
45452ca680 | ||
|
|
4fbbc18f9f | ||
|
|
22ec280ec2 | ||
|
|
89c06b5201 | ||
|
|
86d5f72988 | ||
|
|
c3e055a4c9 | ||
|
|
e48631956d | ||
|
|
2964f3b009 | ||
|
|
527c66dca4 | ||
|
|
24909f0523 | ||
|
|
ed7494b3a4 | ||
|
|
8fae275e5a | ||
|
|
a09a2a5f4b | ||
|
|
2adc150811 | ||
|
|
f5cad33b6c | ||
|
|
338cf45f65 | ||
|
|
359c60bafb | ||
|
|
f808b73a5d | ||
|
|
8dd87cde58 | ||
|
|
1ec78d9beb | ||
|
|
9c710285f2 | ||
|
|
90f745a18f | ||
|
|
af446579ab | ||
|
|
bcc11bd172 | ||
|
|
0c31f756a8 | ||
|
|
16fdd5a5e6 | ||
|
|
ec1a2c66ee | ||
|
|
c3f41d68e6 | ||
|
|
2f8701b4b2 | ||
|
|
9b2d5410d6 | ||
|
|
1d8c9d2c40 | ||
|
|
8c0817245f | ||
|
|
2a04e60ae0 | ||
|
|
fb5eb220fd | ||
|
|
512f48ebdd | ||
|
|
09db7d26a7 | ||
|
|
690cf5eca1 | ||
|
|
1aee4c59c4 | ||
|
|
f1384074b5 | ||
|
|
203bed06d6 | ||
|
|
0b00e28863 | ||
|
|
d74d331642 | ||
|
|
f075fbdc63 | ||
|
|
d59b6696ca | ||
|
|
eedf6f9a39 | ||
|
|
1cca5729fc | ||
|
|
a7b01ece22 | ||
|
|
d17e6d08d8 | ||
|
|
b29aaa9e20 | ||
|
|
b2c718d614 |
123
.appveyor.yml
123
.appveyor.yml
@@ -1,124 +1,78 @@
|
||||
image: Visual Studio 2019
|
||||
clone_depth: 400
|
||||
|
||||
environment:
|
||||
global:
|
||||
# SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the
|
||||
# /E:ON and /V:ON options are not enabled in the batch script intepreter
|
||||
# See: http://stackoverflow.com/a/13751649/163740
|
||||
CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_env.cmd"
|
||||
|
||||
matrix:
|
||||
|
||||
- PYTHON: "C:\\Python36"
|
||||
PYTHON_VERSION: "3.6.x"
|
||||
PYTHON_ARCH: "32"
|
||||
init:
|
||||
- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
||||
- PYTHON: "C:\\Python37-x64"
|
||||
# Should be enabled only for build process debugging
|
||||
# init:
|
||||
# - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
||||
install:
|
||||
# If there is a newer build queued for the same PR, cancel this one.
|
||||
# The AppVeyor 'rollout builds' option is supposed to serve the same
|
||||
# purpose but it is problematic because it tends to cancel builds pushed
|
||||
# directly to master instead of just PR builds (or the converse).
|
||||
# credits: JuliaLang developers.
|
||||
- ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod `
|
||||
https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | `
|
||||
Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { `
|
||||
throw "There are newer queued builds for this pull request, failing early." }
|
||||
- ps: echo("OS version:")
|
||||
- ps: "[System.Environment]::OSVersion.Version"
|
||||
|
||||
- ECHO "Filesystem root:"
|
||||
- ps: "ls \"C:/\""
|
||||
- ps: echo("Filesystem - root:")
|
||||
- ps: "ls \"C:\\\""
|
||||
|
||||
- ECHO "Filesystem projects root:"
|
||||
- ps: echo("Filesystem - projects root:")
|
||||
- ps: "ls \"C:\\projects\\\""
|
||||
|
||||
- ECHO "Filesystem pyfa root:"
|
||||
- ps: "ls \"C:\\projects\\$env:APPVEYOR_PROJECT_SLUG\""
|
||||
- ps: echo("Filesystem - pyfa root:")
|
||||
- ps: "ls \"C:\\projects\\$env:APPVEYOR_PROJECT_SLUG\\\""
|
||||
|
||||
- ECHO "Installed SDKs:"
|
||||
- ps: "ls \"C:/Program Files/Microsoft SDKs/Windows\""
|
||||
- ps: echo("Filesystem - installed SDKs:")
|
||||
- ps: "ls \"C:\\Program Files (x86)\\Windows Kits\\\""
|
||||
|
||||
# Prepend newly installed Python to the PATH of this build (this cannot be
|
||||
# done from inside the powershell script as it would require to restart
|
||||
# the parent CMD process).
|
||||
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
|
||||
- cmd: "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
|
||||
|
||||
- "python --version"
|
||||
- "python -c \"import struct; print(struct.calcsize('P') * 8)\""
|
||||
- cmd: "python --version"
|
||||
- cmd: "python -c \"import struct; print(struct.calcsize('P') * 8)\""
|
||||
|
||||
# Upgrade to the latest version of pip to avoid it displaying warnings
|
||||
# about it being out of date.
|
||||
- "pip install --disable-pip-version-check --user --upgrade pip"
|
||||
- cmd: "python -m pip install --upgrade pip"
|
||||
|
||||
# Install the build dependencies of the project. If some dependencies contain
|
||||
# compiled extensions and are not provided as pre-built wheel packages,
|
||||
# pip will build them from source using the MSVC compiler matching the
|
||||
# target Python version and architecture
|
||||
- ECHO "Install pip requirements:"
|
||||
- "pip install -r requirements.txt"
|
||||
- "pip install PyInstaller"
|
||||
- ps: echo("Install pip requirements:")
|
||||
# This one is needed to build wxpython 4.0.6 on windows
|
||||
- cmd: "python -m pip install pathlib2"
|
||||
- cmd: "python -m pip install -r requirements.txt"
|
||||
- cmd: "python -m pip install PyInstaller==3.6"
|
||||
|
||||
before_build:
|
||||
# directory that will contain the built files
|
||||
- ps: $env:PYFA_DIST_DIR = "c:\projects\$env:APPVEYOR_PROJECT_SLUG\dist"
|
||||
- ps: $env:PYFA_VERSION = (python ./scripts/dump_version.py)
|
||||
- ps: echo("pyfa version ")
|
||||
- ps: echo ($env:PYFA_VERSION)
|
||||
- ps: echo("pyfa version $env:PYFA_VERSION")
|
||||
|
||||
build_script:
|
||||
- ECHO "Build pyfa:"
|
||||
|
||||
##########
|
||||
# PyInstaller - create binaries for pyfa
|
||||
##########
|
||||
- ps: echo("Build pyfa:")
|
||||
# Build gamedata DB
|
||||
- cmd: "python db_update.py"
|
||||
# Build command for PyInstaller
|
||||
- "python -m PyInstaller --noupx --clean --windowed --noconsole -y pyfa.spec"
|
||||
- cmd: "python -m PyInstaller --noupx --clean --windowed --noconsole -y pyfa.spec"
|
||||
# Copy over manifest (See pyfa-org/pyfa#1622)
|
||||
- ps: xcopy /y dist_assets\win\pyfa.exe.manifest $env:PYFA_DIST_DIR\pyfa\
|
||||
# Not really sure if this is needed, but why not
|
||||
- ps: xcopy /y dist_assets\win\Microsoft.VC90.CRT.manifest $env:PYFA_DIST_DIR\pyfa\
|
||||
|
||||
##########
|
||||
# InnoScript EXE building
|
||||
# This is in a separate script because I don't feel like copying over the logic to AppVeyor script right now...
|
||||
##########
|
||||
- "python dist_assets/win/dist.py"
|
||||
# InnoScript EXE building. This is in a separate script because I don't feel like copying over the logic to AppVeyor script right now...
|
||||
- cmd: "python dist_assets/win/dist.py"
|
||||
- ps: dir $env:PYFA_DIST_DIR/
|
||||
#- ECHO "Build pyfa (Debug):"
|
||||
#- copy C:\projects\pyfa\dist_assets\win\pyfa_debug.spec C:\projects\pyfa\pyfa_debug.spec
|
||||
#- "pyinstaller.exe --clean --noconfirm --windowed --upx-dir=C:\\projects\\pyfa\\scripts\\upx.exe C:\\projects\\pyfa\\pyfa_debug.spec"
|
||||
|
||||
build: on
|
||||
|
||||
after_build:
|
||||
- ps: "ls \"./\""
|
||||
#- ps: "ls \"C:\\projects\\pyfa\\build\\pyfa\\\""
|
||||
# - ps: "ls \"C:\\projects\\$env:APPVEYOR_PROJECT_SLUG\\build\\exe.win32-2.7\\\""
|
||||
# Zip
|
||||
# APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER
|
||||
#- 7z a build.zip -r C:\projects\pyfa\build\pyfa\*.*
|
||||
- ps: 7z a "pyfa-$env:PYFA_VERSION-win.zip" -r "$env:PYFA_DIST_DIR\pyfa\*.*"
|
||||
#- 7z a pyfa_debug.zip -r C:\projects\pyfa\dist\pyfa_debug\*.*
|
||||
|
||||
on_success:
|
||||
# Do nothing right now
|
||||
- ps: 7z a "pyfa-$env:PYFA_VERSION-win.zip" -r "$env:PYFA_DIST_DIR\pyfa\*"
|
||||
|
||||
test_script:
|
||||
#- tox
|
||||
#- "py.test --cov=./"
|
||||
# Run the project tests
|
||||
# - "%CMD_IN_ENV% python C:/projects/eve-gnosis/setup.py nosetests"
|
||||
|
||||
after_test:
|
||||
# If tests are successful, create binary packages for the project.
|
||||
# - "%CMD_IN_ENV% python setup.py bdist_wheel"
|
||||
# - "%CMD_IN_ENV% python setup.py bdist_wininst"
|
||||
# - "%CMD_IN_ENV% python setup.py bdist_msi"
|
||||
# - ps: "ls dist"
|
||||
# Ha... we're just building
|
||||
|
||||
artifacts:
|
||||
# Archive the generated packages in the ci.appveyor.com build report.
|
||||
- path: pyfa*-win.zip
|
||||
- path: pyfa*-win.exe
|
||||
#- path: pyfa_debug.zip
|
||||
# name: Pyfa_debug
|
||||
|
||||
deploy:
|
||||
tag: $(pyfa_version)
|
||||
@@ -126,10 +80,9 @@ deploy:
|
||||
description: 'Release description'
|
||||
provider: GitHub
|
||||
auth_token:
|
||||
secure: BfNHO66ff5hVx2O2ORbl49X0U/5h2V2T0IuRZDwm7fd1HvsVluF0wRCbl29oRp1M
|
||||
secure: X+U3hOAMTt7HGXCR/LXaGNF6qyhUXetrjz5+xlWiNJQ3XEdzhZZmHK75m0Hm6qre
|
||||
draft: true
|
||||
force_update: false
|
||||
# deploy on tag push only
|
||||
on:
|
||||
APPVEYOR_REPO_TAG: true # deploy on tag push only
|
||||
#on_success:
|
||||
# - TODO: upload the content of dist/*.whl to a public wheelhouse
|
||||
#
|
||||
APPVEYOR_REPO_TAG: true
|
||||
|
||||
10
.travis.yml
10
.travis.yml
@@ -1,16 +1,18 @@
|
||||
os: linux
|
||||
language: python
|
||||
git:
|
||||
depth: 400
|
||||
python:
|
||||
- 3.6
|
||||
- 3.8
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode7.3
|
||||
osx_image: xcode11.3
|
||||
language: generic
|
||||
env: PYTHON=3.6.1
|
||||
before_install:
|
||||
- bash scripts/setup-osx.sh
|
||||
install:
|
||||
- python3 db_update.py
|
||||
- export PYFA_VERSION="$(python3 scripts/dump_version.py)"
|
||||
- bash scripts/package-osx.sh
|
||||
before_deploy:
|
||||
@@ -19,7 +21,7 @@ before_deploy:
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: Xfu0xApoB0zUPLXl29aYUulVC3iA4/3bXQwwADKCfAKZwxgNon4dLbO7Rie5/7Ukf2POL0KwmRaQGN3kOr+XSoIVTE4M5sXxnhiaaLGKQ+48hDizLE6JuXcZGJvkxUaghaTzIdCwHsG7VGBsPfQgfGsjJcfBp8tFNLmRyM/Jpsr8T6BR2MxtBIEUVy8zrOWFNZqnmWrY2pWMsB9fYt3JFNdpqeIgRAYqbBsBcZQ1MngLTi3ztuYS5IaF+lk06RrnBlHmUsJu/5nCvIpvPvD0i2BLZ3Uu0+Fn+8QWUgjJEL9MNseXZMXynu05xd8YRk7Ajc9CUrzQIIbAktyteYp85kE3pUJHmrMLcXhh7nqkwttR5/47Zwa3OLJLJFKBxMx6wY5jFkJjkV08850B7aWrmTFl/Eqc3Q5nZMuiEt3wFRbjxHi9h1mTN/fkxfRRHg8u3ENGPR+ZPiFC3J18qtks/B/hsKjjHvZP1i79OYlET4V/zyLyyQkCbpDaARQANuotLYJyZ7tH+KWEyRsvTi0M9Yev9mNNw6aI4vzh4HfkEhvcvnWnYwckPj1dnjQ573Qpw0Z9wsconoWfHAn+hBDt3+YLMrrFZl++mCRskHH1mZChX3aGMDi49zD0kfxBUkYPOAhguc6PwudBxHUZP+O6T/SoHylff6EizCE/k5dGeAk=
|
||||
secure: D8tBW0kyHlKf/sXS69aIuexsYTx9auY2DzudKFlfcvdzqat4N2XZqZbZCTVd7YVvptQ8Dj0oZ/p3KUxEGpnJZmlTeJL142rpM/qaNd6wOIMy2yUde/aZl+W9JLFNQp7KHutM+MxObYLzJGihx/8YsupmFx6lxgdngGDXtXYZe/ruDIWDs92ShoKJ4vlce9Csm7eGKv7wv6Z6V9sD5FS3E9J8xdWStHxsbrkPBOflmG+uHU09dpEqzUm+ZYROIoTwig1Xbw3fw+gfjmNrfdSU4fAJcVZI1hrgoenZyJbMfhI2Ej/nZdbZgaXcZNF/eUpqOGgbPe1JljqFnHTbexcE+LPBVyAToScsGMpByHhig67DrZ0nk9gSZoC6CPNl5YS6xub+5dncMJ3P5L03DOGYRu4SL9NczbeuQyKuea7+JPP/8VLwfFDSEqbNEAmgzABAzrdfano+VXtuBuE/Tiy5eE7le9hJu6aSQoKW1SA3cUhMsmr2amzdO96sh+PN8FA1oNr45Yuy0pqOj4SUIkb8JUy4th7vgdhljEkSxrHDK1UcHpxUTp+IIUZkZVVk50aH68dQZxGwSTVOeRxpjrTcEf7VCGaM98qxi/ZK4RW6Ewiq0eo0AxwEeB2Zm841lycGPR/406vM9/ZBzv5IhELIdDdVWTk+dGjJBXB8z5hPJOg=
|
||||
file_glob: true
|
||||
file: "dist/pyfa-*.zip"
|
||||
skip_cleanup: true
|
||||
|
||||
102
CONTRIBUTING.md
Normal file
102
CONTRIBUTING.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Contribution
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.6
|
||||
- Git CLI installed
|
||||
- Python, pip and git are all available as command-line commands (add to the path if needed)
|
||||
|
||||
Virtual environment will be created in *PyfaEnv* folder. Project will be cloned and run from the *PyfaDEV* folder. Separate virtual environment will be created so required libraries won't clutter the main python installation.
|
||||
|
||||
> Commands and screens were created on Windows 10. Please, update all the paths according to your OS.
|
||||
|
||||
## Setting up the project manually
|
||||
|
||||
Clone the repository
|
||||
```
|
||||
git clone <repo> PyfaDEV
|
||||
```
|
||||
|
||||
Create the virtual environment
|
||||
```
|
||||
python -m venv PyfaEnv
|
||||
```
|
||||
|
||||
Activate the virtual environment
|
||||
|
||||
```
|
||||
For cmd.exe: PyfaEnv\scripts\activate.bat
|
||||
For bash: source <venv>/Scripts/activate
|
||||
```
|
||||
> For other OS check [Python documentation](https://docs.python.org/3/library/venv.html)
|
||||
|
||||
Install requirements for the project from *requirements.txt*
|
||||
```
|
||||
pip install -r PyfaDEV\requirements.txt
|
||||
```
|
||||
> For some Linux distributions, you may need to install separate wxPython bindings, such as `python-matplotlib-wx`
|
||||
|
||||
Check that the libs from *requirements.txt* are installed
|
||||
```
|
||||
pip list
|
||||
```
|
||||
|
||||
Test that the project is starting properly
|
||||
```
|
||||
python PyfaDEV\pyfa.py
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Setting up the project with PyCharm/IntelliJ
|
||||
|
||||
Install PyCharm / Other IntelliJ product with Python plugin
|
||||
|
||||
After launching - select *Check out from Version Control* -> *GIt*
|
||||
|
||||

|
||||
|
||||
Login to GitHub, paste the repo URL and select the folder to which to clone the project into, press *Clone*.
|
||||
|
||||

|
||||
|
||||
After process is complete, open *File* -> *Settings* -> *Project* -> *Project Interpreter*.
|
||||
|
||||

|
||||
|
||||
Press on options and add new virtual environment.
|
||||
|
||||

|
||||
|
||||
Open project tree view and double-click on the *requirements.txt*. Press *Install requirements*. Install all requirements.
|
||||
|
||||

|
||||
|
||||
Create new *Run Configuration*. Set correct *Script path* and *Python interpreter*.
|
||||
|
||||

|
||||
|
||||
Check that the project is starting properly.
|
||||
|
||||
## Running tests
|
||||
|
||||
Switch to the proper virtual environment
|
||||
```
|
||||
For cmd.exe: PyfaEnv\scripts\activate.bat
|
||||
For bash: source <venv>/Scripts/activate
|
||||
```
|
||||
|
||||
Install pytest
|
||||
```
|
||||
pip install pytest
|
||||
```
|
||||
|
||||
Switch to pyfa directory.
|
||||
|
||||
Run tests (any will do)
|
||||
```
|
||||
python -m pytest
|
||||
py.test
|
||||
```
|
||||
|
||||
More information on tests can be found on appropriate [Wiki page](https://github.com/pyfa-org/Pyfa/wiki/Developers:-Writing-Tests-for-Pyfa).
|
||||
38
README.md
38
README.md
@@ -2,20 +2,19 @@
|
||||
|
||||
[](https://pyfainvite.azurewebsites.net/) [](https://travis-ci.org/pyfa-org/Pyfa)
|
||||
|
||||

|
||||

|
||||
|
||||
## What is it?
|
||||
|
||||
pyfa, short for **py**thon **f**itting **a**ssistant, allows you to create, experiment with, and save ship fittings without being in game. Open source and written in Python, it is available on any platform where Python 2.x and wxWidgets are available, including Windows, Mac OS X, and Linux.
|
||||
Pyfa, short for **py**thon **f**itting **a**ssistant, allows you to create, experiment with, and save ship fittings without being in game. Open source and written in Python, it is available on any platform where Python 3 and wxWidgets are available, including Windows, Mac OS X, and Linux.
|
||||
|
||||
## Latest Version and Changelogs
|
||||
The latest version along with release notes can always be found on the project's [Releases](https://github.com/DarkFenX/Pyfa/releases) page. pyfa will notify you if you are running an outdated version.
|
||||
The latest version along with release notes can always be found on the project's [releases](https://github.com/pyfa-org/Pyfa/releases) page. Pyfa will notify you if you are running an outdated version.
|
||||
|
||||
## Installation
|
||||
Windows and OS X users are supplied self-contained builds of pyfa on the [latest releases](https://github.com/pyfa-org/Pyfa/releases/latest) page. An `.exe` installer is also available for Windows builds. Linux users can run pyfa using their distribution's Python interpreter. There is no official self-contained package for Linux, however, there are a number of third-party packages available through distribution-specific repositories.
|
||||
|
||||
#### OS X
|
||||
|
||||
### OS X
|
||||
Apart from the official release, there is also a [Homebrew](http://brew.sh) option for installing pyfa on OS X. Please note this is maintained by a third-party and is not tested by pyfa developers. Simply fire up in terminal:
|
||||
```
|
||||
$ brew install Caskroom/cask/pyfa
|
||||
@@ -24,39 +23,34 @@ $ brew install Caskroom/cask/pyfa
|
||||
### Linux Distro-specific Packages
|
||||
The following is a list of pyfa packages available for certain distributions. Please note that these packages are maintained by third-parties and are not evaluated by the pyfa developers.
|
||||
|
||||
* Debian/Ubuntu/derivatives: https://github.com/AdamMajer/Pyfa/releases
|
||||
* Arch: https://aur.archlinux.org/packages/pyfa/
|
||||
* openSUSE: https://build.opensuse.org/package/show/home:rmk2/pyfa
|
||||
* FreeBSD: http://www.freshports.org/games/pyfa/ (see [#484](https://github.com/pyfa-org/Pyfa/issues/484) for instructions)
|
||||
* Gentoo: https://github.com/ZeroPointEnergy/gentoo-pyfa-overlay
|
||||
|
||||
### Dependencies
|
||||
If you wish to help with development or simply need to run pyfa through a Python interpreter, the following software is required:
|
||||
|
||||
* Python 3.6
|
||||
* Requirements as listed in `requirements.txt`
|
||||
## Contribution
|
||||
If you wish to help with development or you need to run pyfa through a Python interpreter, check out [the instructions](https://github.com/pyfa-org/Pyfa/blob/master/CONTRIBUTING.md).
|
||||
|
||||
## Bug Reporting
|
||||
The preferred method of reporting bugs is through the project's [GitHub Issues interface](https://github.com/pyfa-org/Pyfa/issues). Alternatively, posting a report in the [pyfa thread](http://forums.eveonline.com/default.aspx?g=posts&t=247609) on the official EVE Online forums is acceptable. Guidelines for bug reporting can be found on [this wiki page](https://github.com/DarkFenX/Pyfa/wiki/Bug-Reporting).
|
||||
The preferred method of reporting bugs is through the project's [GitHub Issues interface](https://github.com/pyfa-org/Pyfa/issues). Alternatively, posting a report in the [pyfa thread](https://forums.eveonline.com/t/27156) on the official EVE Online forums is acceptable. Guidelines for bug reporting can be found on [this wiki page](https://github.com/pyfa-org/Pyfa/wiki/Bug-Reporting).
|
||||
|
||||
## License
|
||||
pyfa is licensed under the GNU GPL v3.0, see LICENSE
|
||||
Pyfa is licensed under the GNU GPL v3.0, see LICENSE
|
||||
|
||||
## Resources
|
||||
* Development repository: [https://github.com/pyfa-org/Pyfa](https://github.com/pyfa-org/Pyfa)
|
||||
* [Development repository](https://github.com/pyfa-org/Pyfa)
|
||||
* [EVE forum thread](https://forums.eveonline.com/t/27156)
|
||||
* [EVE University guide using pyfa](http://wiki.eveuniversity.org/Guide_to_using_PYFA)
|
||||
* [EVE University guide using pyfa](https://wiki.eveuniversity.org/PYFA)
|
||||
* [EVE Online website](http://www.eveonline.com/)
|
||||
|
||||
## Contacts:
|
||||
* Sable Blitzmann
|
||||
* GitHub: @blitzmann
|
||||
* [TweetFleet Slack](https://www.fuzzwork.co.uk/tweetfleet-slack-invites/): @blitzmann
|
||||
* [Gitter chat](https://gitter.im/pyfa-org/Pyfa): @ blitzmann
|
||||
* Email: sable.blitzmann@gmail.com
|
||||
* Kadesh / DarkPhoenix
|
||||
* GitHub: @DarkFenX
|
||||
* EVE: Kadesh Priestess
|
||||
* Email: phoenix@mail.ru
|
||||
* Sable Blitzmann
|
||||
* GitHub: @blitzmann
|
||||
* [TweetFleet Slack](https://www.fuzzwork.co.uk/tweetfleet-slack-invites/): @blitzmann
|
||||
* [Gitter chat](https://gitter.im/pyfa-org/Pyfa): @blitzmann
|
||||
* Email: sable.blitzmann@gmail.com
|
||||
|
||||
## CCP Copyright Notice
|
||||
EVE Online, the EVE logo, EVE and all associated logos and designs are the intellectual property of CCP hf. All artwork, screenshots, characters, vehicles, storylines, world facts or other recognizable features of the intellectual property relating to these trademarks are likewise the intellectual property of CCP hf. EVE Online and the EVE logo are the registered trademarks of CCP hf. All rights are reserved worldwide. All other trademarks are the property of their respective owners. CCP hf. has granted permission to pyfa to use EVE Online and all associated logos and designs for promotional and information purposes on its website but does not endorse, and is not in any way affiliated with, pyfa. CCP is in no way responsible for the content on or functioning of this program, nor can it be liable for any damage arising from the use of this program.
|
||||
|
||||
@@ -49,6 +49,8 @@ def DBInMemory_test():
|
||||
gamedata_version = gamedata_session.execute(
|
||||
"SELECT `field_value` FROM `metadata` WHERE `field_name` LIKE 'client_build'"
|
||||
).fetchone()[0]
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception as e:
|
||||
print("Missing gamedata version.")
|
||||
gamedata_version = None
|
||||
@@ -72,7 +74,7 @@ def DBInMemory_test():
|
||||
# noinspection PyPep8
|
||||
#from eos.db.gamedata import alphaClones, attribute, category, effect, group, icon, item, marketGroup, metaData, metaGroup, queries, traits, unit
|
||||
# noinspection PyPep8
|
||||
#from eos.db.saveddata import booster, cargo, character, crest, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, loadDefaultDatabaseValues, miscData, module, override, price, queries, skill, targetResists, user
|
||||
#from eos.db.saveddata import booster, cargo, character, crest, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, miscData, module, override, price, queries, skill, targetProfile, user
|
||||
|
||||
# If using in memory saveddata, you'll want to reflect it so the data structure is good.
|
||||
if saveddata_connectionstring == "sqlite:///:memory:":
|
||||
|
||||
@@ -62,4 +62,4 @@ def HeronFit(DB, Gamedata, Saveddata):
|
||||
for _ in range(4):
|
||||
fit.modules.append(mod)
|
||||
|
||||
return fit
|
||||
return fit
|
||||
|
||||
17
config.py
17
config.py
@@ -33,11 +33,14 @@ pyfaPath = None
|
||||
savePath = None
|
||||
saveDB = None
|
||||
gameDB = None
|
||||
imgsZIP = None
|
||||
logPath = None
|
||||
loggingLevel = None
|
||||
logging_setup = None
|
||||
cipher = None
|
||||
clientHash = None
|
||||
experimentalFeatures = None
|
||||
version = None
|
||||
|
||||
ESI_CACHE = 'esi_cache'
|
||||
|
||||
@@ -96,11 +99,13 @@ def defPaths(customSavePath=None):
|
||||
global savePath
|
||||
global saveDB
|
||||
global gameDB
|
||||
global imgsZIP
|
||||
global saveInRoot
|
||||
global logPath
|
||||
global cipher
|
||||
global clientHash
|
||||
global version
|
||||
global experimentalFeatures
|
||||
|
||||
pyfalog.debug("Configuring Pyfa")
|
||||
|
||||
@@ -155,6 +160,10 @@ def defPaths(customSavePath=None):
|
||||
if not gameDB:
|
||||
gameDB = os.path.join(pyfaPath, "eve.db")
|
||||
|
||||
imgsZIP = getattr(configforced, "imgsZIP", imgsZIP)
|
||||
if not imgsZIP:
|
||||
imgsZIP = os.path.join(pyfaPath, "imgs.zip")
|
||||
|
||||
if debug:
|
||||
logFile = "pyfa_debug.log"
|
||||
else:
|
||||
@@ -162,6 +171,10 @@ def defPaths(customSavePath=None):
|
||||
|
||||
logPath = os.path.join(savePath, logFile)
|
||||
|
||||
experimentalFeatures = getattr(configforced, "experimentalFeatures", experimentalFeatures)
|
||||
if experimentalFeatures is None:
|
||||
experimentalFeatures = False
|
||||
|
||||
# DON'T MODIFY ANYTHING BELOW
|
||||
import eos.config
|
||||
|
||||
@@ -220,6 +233,8 @@ def defLogging():
|
||||
# reset=False,
|
||||
)
|
||||
])
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
print("Critical error attempting to setup logging. Falling back to console only.")
|
||||
logging_setup = NestedSetup([
|
||||
@@ -233,7 +248,7 @@ def defLogging():
|
||||
])
|
||||
|
||||
|
||||
class LoggerWriter(object):
|
||||
class LoggerWriter:
|
||||
def __init__(self, level):
|
||||
# self.level is really like using log.debug(message)
|
||||
# at least in my case
|
||||
|
||||
556
db_update.py
Normal file
556
db_update.py
Normal file
@@ -0,0 +1,556 @@
|
||||
#!/usr/bin/env python3
|
||||
#======================================================================
|
||||
# Copyright (C) 2012 Diego Duclos
|
||||
#
|
||||
# This file is part of eos.
|
||||
#
|
||||
# eos is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of
|
||||
# the License, or (at your option) any later version.
|
||||
#
|
||||
# eos is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
#======================================================================
|
||||
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
|
||||
ROOT_DIR = os.path.realpath(os.path.dirname(__file__))
|
||||
DB_PATH = os.path.join(ROOT_DIR, 'eve.db')
|
||||
JSON_DIR = os.path.join(ROOT_DIR, 'staticdata')
|
||||
if ROOT_DIR not in sys.path:
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
GAMEDATA_SCHEMA_VERSION = 3
|
||||
|
||||
|
||||
def db_needs_update():
|
||||
"""True if needs, false if it does not, none if we cannot check it."""
|
||||
try:
|
||||
with open(os.path.join(JSON_DIR, 'phobos', 'metadata.json')) as f:
|
||||
data_version = next((r['field_value'] for r in json.load(f) if r['field_name'] == 'client_build'))
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
# If we have no source data - return None; should not update in this case
|
||||
except:
|
||||
return None
|
||||
if not os.path.isfile(DB_PATH):
|
||||
print('Gamedata DB not found')
|
||||
return True
|
||||
db_data_version = None
|
||||
db_schema_version = None
|
||||
try:
|
||||
db = sqlite3.connect(DB_PATH)
|
||||
cursor = db.cursor()
|
||||
cursor.execute('SELECT field_value FROM metadata WHERE field_name = \'client_build\'')
|
||||
for row in cursor:
|
||||
db_data_version = int(row[0])
|
||||
cursor.execute('SELECT field_value FROM metadata WHERE field_name = \'schema_version\'')
|
||||
for row in cursor:
|
||||
db_schema_version = int(row[0])
|
||||
cursor.close()
|
||||
db.close()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
print('Error when fetching gamedata DB metadata')
|
||||
return True
|
||||
if data_version != db_data_version:
|
||||
print('Gamedata DB data version mismatch: needed {}, DB has {}'.format(data_version, db_data_version))
|
||||
return True
|
||||
if GAMEDATA_SCHEMA_VERSION != db_schema_version:
|
||||
print('Gamedata DB schema version mismatch: needed {}, DB has {}'.format(GAMEDATA_SCHEMA_VERSION, db_schema_version))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def update_db():
|
||||
|
||||
print('Building gamedata DB...')
|
||||
|
||||
if os.path.isfile(DB_PATH):
|
||||
os.remove(DB_PATH)
|
||||
|
||||
import eos.db
|
||||
import eos.gamedata
|
||||
|
||||
# Create the database tables
|
||||
eos.db.gamedata_meta.create_all()
|
||||
|
||||
def _readData(minerName, jsonName, keyIdName=None):
|
||||
with open(os.path.join(JSON_DIR, minerName, '{}.json'.format(jsonName)), encoding='utf-8') as f:
|
||||
rawData = json.load(f)
|
||||
if not keyIdName:
|
||||
return rawData
|
||||
# IDs in keys, rows in values
|
||||
data = []
|
||||
for k, v in rawData.items():
|
||||
row = {}
|
||||
row.update(v)
|
||||
if keyIdName not in row:
|
||||
row[keyIdName] = int(k)
|
||||
data.append(row)
|
||||
return data
|
||||
|
||||
def _addRows(data, cls, fieldMap=None):
|
||||
if fieldMap is None:
|
||||
fieldMap = {}
|
||||
for row in data:
|
||||
instance = cls()
|
||||
for k, v in row.items():
|
||||
if isinstance(v, str):
|
||||
v = v.strip()
|
||||
setattr(instance, fieldMap.get(k, k), v)
|
||||
eos.db.gamedata_session.add(instance)
|
||||
|
||||
def processEveTypes():
|
||||
print('processing evetypes')
|
||||
data = _readData('fsd_lite', 'evetypes', keyIdName='typeID')
|
||||
for row in data:
|
||||
if (
|
||||
# Apparently people really want Civilian modules available
|
||||
(row['typeName'].startswith('Civilian') and "Shuttle" not in row['typeName']) or
|
||||
row['typeName'] in ('Capsule', 'Dark Blood Tracking Disruptor')
|
||||
):
|
||||
row['published'] = True
|
||||
elif row['typeName'].startswith('Limited Synth '):
|
||||
row['published'] = False
|
||||
|
||||
newData = []
|
||||
for row in data:
|
||||
if (
|
||||
row['published'] or
|
||||
# group Ship Modifiers, for items like tactical t3 ship modes
|
||||
row['groupID'] == 1306 or
|
||||
# Micro Bombs (Fighters)
|
||||
row['typeID'] in (41549, 41548, 41551, 41550) or
|
||||
# Abyssal weather (environment)
|
||||
row['groupID'] in (
|
||||
1882,
|
||||
1975,
|
||||
1971,
|
||||
# the "container" for the abyssal environments
|
||||
1983)
|
||||
):
|
||||
newData.append(row)
|
||||
|
||||
_addRows(newData, eos.gamedata.Item)
|
||||
return newData
|
||||
|
||||
def processEveGroups():
|
||||
print('processing evegroups')
|
||||
data = _readData('fsd_lite', 'evegroups', keyIdName='groupID')
|
||||
_addRows(data, eos.gamedata.Group)
|
||||
return data
|
||||
|
||||
def processEveCategories():
|
||||
print('processing evecategories')
|
||||
data = _readData('fsd_lite', 'evecategories', keyIdName='categoryID')
|
||||
_addRows(data, eos.gamedata.Category)
|
||||
|
||||
def processDogmaAttributes():
|
||||
print('processing dogmaattributes')
|
||||
data = _readData('fsd_binary', 'dogmaattributes', keyIdName='attributeID')
|
||||
_addRows(data, eos.gamedata.AttributeInfo)
|
||||
|
||||
def processDogmaTypeAttributes(eveTypesData):
|
||||
print('processing dogmatypeattributes')
|
||||
data = _readData('fsd_binary', 'typedogma', keyIdName='typeID')
|
||||
eveTypeIds = set(r['typeID'] for r in eveTypesData)
|
||||
newData = []
|
||||
seenKeys = set()
|
||||
|
||||
def checkKey(key):
|
||||
if key in seenKeys:
|
||||
return False
|
||||
seenKeys.add(key)
|
||||
return True
|
||||
|
||||
for typeData in data:
|
||||
if typeData['typeID'] not in eveTypeIds:
|
||||
continue
|
||||
for row in typeData.get('dogmaAttributes', ()):
|
||||
row['typeID'] = typeData['typeID']
|
||||
if checkKey((row['typeID'], row['attributeID'])):
|
||||
newData.append(row)
|
||||
for row in eveTypesData:
|
||||
for attrId, attrName in {4: 'mass', 38: 'capacity', 161: 'volume', 162: 'radius'}.items():
|
||||
if attrName in row and checkKey((row['typeID'], attrId)):
|
||||
newData.append({'typeID': row['typeID'], 'attributeID': attrId, 'value': row[attrName]})
|
||||
|
||||
_addRows(newData, eos.gamedata.Attribute)
|
||||
return newData
|
||||
|
||||
def processDynamicItemAttributes():
|
||||
print('processing dynamicitemattributes')
|
||||
data = _readData('fsd_binary', 'dynamicitemattributes')
|
||||
for mutaID, mutaData in data.items():
|
||||
muta = eos.gamedata.DynamicItem()
|
||||
muta.typeID = mutaID
|
||||
muta.resultingTypeID = mutaData['inputOutputMapping'][0]['resultingType']
|
||||
eos.db.gamedata_session.add(muta)
|
||||
|
||||
for x in mutaData['inputOutputMapping'][0]['applicableTypes']:
|
||||
item = eos.gamedata.DynamicItemItem()
|
||||
item.typeID = mutaID
|
||||
item.applicableTypeID = x
|
||||
eos.db.gamedata_session.add(item)
|
||||
|
||||
for attrID, attrData in mutaData['attributeIDs'].items():
|
||||
attr = eos.gamedata.DynamicItemAttribute()
|
||||
attr.typeID = mutaID
|
||||
attr.attributeID = attrID
|
||||
attr.min = attrData['min']
|
||||
attr.max = attrData['max']
|
||||
eos.db.gamedata_session.add(attr)
|
||||
|
||||
def processDogmaEffects():
|
||||
print('processing dogmaeffects')
|
||||
data = _readData('fsd_binary', 'dogmaeffects', keyIdName='effectID')
|
||||
_addRows(data, eos.gamedata.Effect, fieldMap={'resistanceAttributeID': 'resistanceID'})
|
||||
|
||||
def processDogmaTypeEffects(eveTypesData):
|
||||
print('processing dogmatypeeffects')
|
||||
data = _readData('fsd_binary', 'typedogma', keyIdName='typeID')
|
||||
eveTypeIds = set(r['typeID'] for r in eveTypesData)
|
||||
newData = []
|
||||
for typeData in data:
|
||||
if typeData['typeID'] not in eveTypeIds:
|
||||
continue
|
||||
for row in typeData.get('dogmaEffects', ()):
|
||||
row['typeID'] = typeData['typeID']
|
||||
newData.append(row)
|
||||
_addRows(newData, eos.gamedata.ItemEffect)
|
||||
return newData
|
||||
|
||||
def processDogmaUnits():
|
||||
print('processing dogmaunits')
|
||||
data = _readData('fsd_binary', 'dogmaunits', keyIdName='unitID')
|
||||
_addRows(data, eos.gamedata.Unit, fieldMap={'name': 'unitName'})
|
||||
|
||||
def processMarketGroups():
|
||||
print('processing marketgroups')
|
||||
data = _readData('fsd_binary', 'marketgroups', keyIdName='marketGroupID')
|
||||
_addRows(data, eos.gamedata.MarketGroup, fieldMap={'name': 'marketGroupName'})
|
||||
|
||||
def processMetaGroups():
|
||||
print('processing metagroups')
|
||||
data = _readData('fsd_binary', 'metagroups', keyIdName='metaGroupID')
|
||||
_addRows(data, eos.gamedata.MetaGroup)
|
||||
|
||||
def processCloneGrades():
|
||||
print('processing clonegrades')
|
||||
data = _readData('fsd_lite', 'clonegrades')
|
||||
|
||||
newData = []
|
||||
# December, 2017 - CCP decided to use only one set of skill levels for alpha clones. However, this is still
|
||||
# represented in the data as a skillset per race. To ensure that all skills are the same, we store them in a way
|
||||
# that we can check to make sure all races have the same skills, as well as skill levels
|
||||
check = {}
|
||||
for ID in data:
|
||||
for skill in data[ID]['skills']:
|
||||
newData.append({
|
||||
'alphaCloneID': int(ID),
|
||||
'alphaCloneName': 'Alpha Clone',
|
||||
'typeID': skill['typeID'],
|
||||
'level': skill['level']})
|
||||
if ID not in check:
|
||||
check[ID] = {}
|
||||
check[ID][int(skill['typeID'])] = int(skill['level'])
|
||||
if not functools.reduce(lambda a, b: a if a == b else False, [v for _, v in check.items()]):
|
||||
raise Exception('Alpha Clones not all equal')
|
||||
newData = [x for x in newData if x['alphaCloneID'] == 1]
|
||||
if len(newData) == 0:
|
||||
raise Exception('Alpha Clone processing failed')
|
||||
|
||||
tmp = []
|
||||
for row in newData:
|
||||
if row['alphaCloneID'] not in tmp:
|
||||
cloneParent = eos.gamedata.AlphaClone()
|
||||
setattr(cloneParent, 'alphaCloneID', row['alphaCloneID'])
|
||||
setattr(cloneParent, 'alphaCloneName', row['alphaCloneName'])
|
||||
eos.db.gamedata_session.add(cloneParent)
|
||||
tmp.append(row['alphaCloneID'])
|
||||
_addRows(newData, eos.gamedata.AlphaCloneSkill)
|
||||
|
||||
def processTraits():
|
||||
print('processing traits')
|
||||
data = _readData('phobos', 'traits')
|
||||
|
||||
def convertSection(sectionData):
|
||||
sectionLines = []
|
||||
headerText = '<b>{}</b>'.format(sectionData['header'])
|
||||
sectionLines.append(headerText)
|
||||
for bonusData in sectionData['bonuses']:
|
||||
prefix = '{} '.format(bonusData['number']) if 'number' in bonusData else ''
|
||||
bonusText = '{}{}'.format(prefix, bonusData['text'].replace('\u00B7', '\u2022 '))
|
||||
sectionLines.append(bonusText)
|
||||
sectionLine = '<br />\n'.join(sectionLines)
|
||||
return sectionLine
|
||||
|
||||
newData = []
|
||||
for row in data:
|
||||
typeLines = []
|
||||
typeId = row['typeID']
|
||||
traitData = row['traits']
|
||||
for skillData in sorted(traitData.get('skills', ()), key=lambda i: i['header']):
|
||||
typeLines.append(convertSection(skillData))
|
||||
if 'role' in traitData:
|
||||
typeLines.append(convertSection(traitData['role']))
|
||||
if 'misc' in traitData:
|
||||
typeLines.append(convertSection(traitData['misc']))
|
||||
traitLine = '<br />\n<br />\n'.join(typeLines)
|
||||
newRow = {'typeID': typeId, 'traitText': traitLine}
|
||||
newData.append(newRow)
|
||||
|
||||
_addRows(newData, eos.gamedata.Traits)
|
||||
|
||||
def processMetadata():
|
||||
print('processing metadata')
|
||||
data = _readData('phobos', 'metadata')
|
||||
_addRows(data, eos.gamedata.MetaData)
|
||||
|
||||
def processReqSkills(eveTypesData):
|
||||
print('processing requiredskillsfortypes')
|
||||
|
||||
def composeReqSkills(raw):
|
||||
reqSkills = {}
|
||||
for skillTypeID, skillLevels in raw.items():
|
||||
reqSkills[int(skillTypeID)] = skillLevels[0]
|
||||
return reqSkills
|
||||
|
||||
eveTypeIds = set(r['typeID'] for r in eveTypesData)
|
||||
data = _readData('fsd_binary', 'requiredskillsfortypes')
|
||||
reqsByItem = {}
|
||||
itemsByReq = {}
|
||||
for typeID, skillreqData in data.items():
|
||||
typeID = int(typeID)
|
||||
if typeID not in eveTypeIds:
|
||||
continue
|
||||
for skillTypeID, skillLevel in composeReqSkills(skillreqData).items():
|
||||
reqsByItem.setdefault(typeID, {})[skillTypeID] = skillLevel
|
||||
itemsByReq.setdefault(skillTypeID, {})[typeID] = skillLevel
|
||||
for item in eos.db.gamedata_session.query(eos.gamedata.Item).all():
|
||||
if item.typeID in reqsByItem:
|
||||
item.reqskills = json.dumps(reqsByItem[item.typeID])
|
||||
if item.typeID in itemsByReq:
|
||||
item.requiredfor = json.dumps(itemsByReq[item.typeID])
|
||||
|
||||
def processReplacements(eveTypesData, eveGroupsData, dogmaTypeAttributesData, dogmaTypeEffectsData):
|
||||
print('finding item replacements')
|
||||
|
||||
def compareAttrs(attrs1, attrs2):
|
||||
# Consider items as different if they have no attrs
|
||||
if len(attrs1) == 0 and len(attrs2) == 0:
|
||||
return False
|
||||
if set(attrs1) != set(attrs2):
|
||||
return False
|
||||
if all(attrs1[aid] == attrs2[aid] for aid in attrs1):
|
||||
return True
|
||||
return False
|
||||
|
||||
skillReqAttribs = {
|
||||
182: 277,
|
||||
183: 278,
|
||||
184: 279,
|
||||
1285: 1286,
|
||||
1289: 1287,
|
||||
1290: 1288}
|
||||
skillReqAttribsFlat = set(skillReqAttribs.keys()).union(skillReqAttribs.values())
|
||||
# Get data on type groups
|
||||
# Format: {type ID: group ID}
|
||||
typesGroups = {}
|
||||
for row in eveTypesData:
|
||||
typesGroups[row['typeID']] = row['groupID']
|
||||
# Get data on item effects
|
||||
# Format: {type ID: set(effect, IDs)}
|
||||
typesEffects = {}
|
||||
for row in dogmaTypeEffectsData:
|
||||
typesEffects.setdefault(row['typeID'], set()).add(row['effectID'])
|
||||
# Get data on type attributes
|
||||
# Format: {type ID: {attribute ID: attribute value}}
|
||||
typesNormalAttribs = {}
|
||||
typesSkillAttribs = {}
|
||||
for row in dogmaTypeAttributesData:
|
||||
attributeID = row['attributeID']
|
||||
if attributeID in skillReqAttribsFlat:
|
||||
typeSkillAttribs = typesSkillAttribs.setdefault(row['typeID'], {})
|
||||
typeSkillAttribs[row['attributeID']] = row['value']
|
||||
# Ignore these attributes for comparison purposes
|
||||
elif attributeID in (
|
||||
# We do not need mass as it affects final ship stats only when carried by ship itself
|
||||
# (and we're not going to replace ships), but it's wildly inconsistent for other items,
|
||||
# which otherwise would be the same
|
||||
4, # mass
|
||||
124, # mainColor
|
||||
162, # radius
|
||||
422, # techLevel
|
||||
633, # metaLevel
|
||||
1692, # metaGroupID
|
||||
1768 # typeColorScheme
|
||||
):
|
||||
continue
|
||||
else:
|
||||
typeNormalAttribs = typesNormalAttribs.setdefault(row['typeID'], {})
|
||||
typeNormalAttribs[row['attributeID']] = row['value']
|
||||
# Get data on skill requirements
|
||||
# Format: {type ID: {skill type ID: skill level}}
|
||||
typesSkillReqs = {}
|
||||
for typeID, typeAttribs in typesSkillAttribs.items():
|
||||
typeSkillAttribs = typesSkillAttribs.get(typeID, {})
|
||||
if not typeSkillAttribs:
|
||||
continue
|
||||
typeSkillReqs = typesSkillReqs.setdefault(typeID, {})
|
||||
for skillreqTypeAttr, skillreqLevelAttr in skillReqAttribs.items():
|
||||
try:
|
||||
skillType = int(typeSkillAttribs[skillreqTypeAttr])
|
||||
skillLevel = int(typeSkillAttribs[skillreqLevelAttr])
|
||||
except (KeyError, ValueError):
|
||||
continue
|
||||
typeSkillReqs[skillType] = skillLevel
|
||||
# Format: {group ID: category ID}
|
||||
groupCategories = {}
|
||||
for row in eveGroupsData:
|
||||
groupCategories[row['groupID']] = row['categoryID']
|
||||
# As EVE affects various types mostly depending on their group or skill requirements,
|
||||
# we're going to group various types up this way
|
||||
# Format: {(group ID, frozenset(skillreq, type, IDs), frozenset(type, effect, IDs): [type ID, {attribute ID: attribute value}]}
|
||||
groupedData = {}
|
||||
for row in eveTypesData:
|
||||
typeID = row['typeID']
|
||||
# Ignore items outside of categories we need
|
||||
if groupCategories[typesGroups[typeID]] not in (
|
||||
6, # Ship
|
||||
7, # Module
|
||||
8, # Charge
|
||||
18, # Drone
|
||||
20, # Implant
|
||||
22, # Deployable
|
||||
23, # Starbase
|
||||
32, # Subsystem
|
||||
35, # Decryptors
|
||||
65, # Structure
|
||||
66, # Structure Module
|
||||
87, # Fighter
|
||||
):
|
||||
continue
|
||||
typeAttribs = typesNormalAttribs.get(typeID, {})
|
||||
# Ignore items w/o attributes
|
||||
if not typeAttribs:
|
||||
continue
|
||||
# We need only skill types, not levels for keys
|
||||
typeSkillreqs = frozenset(typesSkillReqs.get(typeID, {}))
|
||||
typeGroup = typesGroups[typeID]
|
||||
typeEffects = frozenset(typesEffects.get(typeID, ()))
|
||||
groupData = groupedData.setdefault((typeGroup, typeSkillreqs, typeEffects), [])
|
||||
groupData.append((typeID, typeAttribs))
|
||||
# Format: {type ID: set(type IDs)}
|
||||
replacements = {}
|
||||
# Now, go through composed groups and for every item within it
|
||||
# find items which are the same
|
||||
for groupData in groupedData.values():
|
||||
for type1, type2 in itertools.combinations(groupData, 2):
|
||||
if compareAttrs(type1[1], type2[1]):
|
||||
replacements.setdefault(type1[0], set()).add(type2[0])
|
||||
replacements.setdefault(type2[0], set()).add(type1[0])
|
||||
# Update DB session with data we generated
|
||||
for item in eos.db.gamedata_session.query(eos.gamedata.Item).all():
|
||||
itemReplacements = replacements.get(item.typeID)
|
||||
if itemReplacements is not None:
|
||||
item.replacements = ','.join('{}'.format(tid) for tid in sorted(itemReplacements))
|
||||
|
||||
def processImplantSets(eveTypesData):
|
||||
print('composing implant sets')
|
||||
# Includes only implants which can be considered part of sets, not all implants
|
||||
implant_groups = (300, 1730)
|
||||
specials = {'Genolution': ('Genolution Core Augmentation', r'CA-\d+')}
|
||||
implantSets = {}
|
||||
for row in eveTypesData:
|
||||
if not row.get('published'):
|
||||
continue
|
||||
if row.get('groupID') not in implant_groups:
|
||||
continue
|
||||
typeName = row.get('typeName', '')
|
||||
# Regular sets matching
|
||||
m = re.match('(?P<grade>(High|Mid|Low)-grade) (?P<set>\w+) (?P<implant>(Alpha|Beta|Gamma|Delta|Epsilon|Omega))', typeName, re.IGNORECASE)
|
||||
if m:
|
||||
implantSets.setdefault((m.group('grade'), m.group('set')), set()).add(row['typeID'])
|
||||
# Special set matching
|
||||
for setHandle, (setName, implantPattern) in specials.items():
|
||||
pattern = '(?P<set>{}) (?P<implant>{})'.format(setName, implantPattern)
|
||||
m = re.match(pattern, typeName)
|
||||
if m:
|
||||
implantSets.setdefault((None, setHandle), set()).add(row['typeID'])
|
||||
break
|
||||
data = []
|
||||
for (gradeName, setName), implants in implantSets.items():
|
||||
if len(implants) < 2:
|
||||
continue
|
||||
implants = ','.join('{}'.format(tid) for tid in sorted(implants))
|
||||
row = {'setName': setName, 'gradeName': gradeName, 'implants': implants}
|
||||
data.append(row)
|
||||
_addRows(data, eos.gamedata.ImplantSet)
|
||||
|
||||
eveTypesData = processEveTypes()
|
||||
eveGroupsData = processEveGroups()
|
||||
processEveCategories()
|
||||
processDogmaAttributes()
|
||||
dogmaTypeAttributesData = processDogmaTypeAttributes(eveTypesData)
|
||||
processDynamicItemAttributes()
|
||||
processDogmaEffects()
|
||||
dogmaTypeEffectsData = processDogmaTypeEffects(eveTypesData)
|
||||
processDogmaUnits()
|
||||
processMarketGroups()
|
||||
processMetaGroups()
|
||||
processCloneGrades()
|
||||
processTraits()
|
||||
processMetadata()
|
||||
|
||||
eos.db.gamedata_session.flush()
|
||||
processReqSkills(eveTypesData)
|
||||
processReplacements(eveTypesData, eveGroupsData, dogmaTypeAttributesData, dogmaTypeEffectsData)
|
||||
processImplantSets(eveTypesData)
|
||||
|
||||
# Add schema version to prevent further updates
|
||||
metadata_schema_version = eos.gamedata.MetaData()
|
||||
metadata_schema_version.field_name = 'schema_version'
|
||||
metadata_schema_version.field_value = GAMEDATA_SCHEMA_VERSION
|
||||
eos.db.gamedata_session.add(metadata_schema_version)
|
||||
|
||||
eos.db.gamedata_session.flush()
|
||||
|
||||
# CCP still has 5 subsystems assigned to T3Cs, even though only 4 are available / usable. They probably have some
|
||||
# old legacy requirement or assumption that makes it difficult for them to change this value in the data. But for
|
||||
# pyfa, we can do it here as a post-processing step
|
||||
for attr in eos.db.gamedata_session.query(eos.gamedata.Attribute).filter(eos.gamedata.Attribute.ID == 1367).all():
|
||||
attr.value = 4.0
|
||||
for item in eos.db.gamedata_session.query(eos.gamedata.Item).filter(eos.gamedata.Item.name.like('%abyssal%')).all():
|
||||
item.published = False
|
||||
|
||||
for x in [
|
||||
30 # Apparel
|
||||
]:
|
||||
cat = eos.db.gamedata_session.query(eos.gamedata.Category).filter(eos.gamedata.Category.ID == x).first()
|
||||
print ('Removing Category: {}'.format(cat.name))
|
||||
eos.db.gamedata_session.delete(cat)
|
||||
|
||||
eos.db.gamedata_session.commit()
|
||||
eos.db.gamedata_engine.execute('VACUUM')
|
||||
|
||||
print('done')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
update_db()
|
||||
@@ -30,7 +30,8 @@ added_files = [
|
||||
|
||||
import_these = [
|
||||
'numpy.core._dtype_ctypes', # https://github.com/pyinstaller/pyinstaller/issues/3982
|
||||
'sqlalchemy.ext.baked' # windows build doesn't launch without if when using sqlalchemy 1.3.x
|
||||
'sqlalchemy.ext.baked', # windows build doesn't launch without if when using sqlalchemy 1.3.x
|
||||
'pkg_resources.py2_warn' # issue 2156
|
||||
]
|
||||
|
||||
icon = os.path.join(os.getcwd(), "dist_assets", "mac", "pyfa.icns")
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
# This apes hook-matplotlib.backends.py, but REMOVES backends, all but
|
||||
# the ones in the list below.
|
||||
# Courtesy of https://github.com/bpteague/cytoflow/blob/70f9291/packaging/hook-matplotlib.backends.py
|
||||
|
||||
KEEP = ["WXAgg", "WX", "agg"]
|
||||
|
||||
from PyInstaller.compat import is_darwin
|
||||
from PyInstaller.utils.hooks import (
|
||||
eval_statement, exec_statement, logger)
|
||||
|
||||
|
||||
def get_matplotlib_backend_module_names():
|
||||
"""
|
||||
List the names of all matplotlib backend modules importable under the
|
||||
current Python installation.
|
||||
Returns
|
||||
----------
|
||||
list
|
||||
List of the fully-qualified names of all such modules.
|
||||
"""
|
||||
# Statement safely importing a single backend module.
|
||||
import_statement = """
|
||||
import os, sys
|
||||
# Preserve stdout.
|
||||
sys_stdout = sys.stdout
|
||||
try:
|
||||
# Redirect output printed by this importation to "/dev/null", preventing
|
||||
# such output from being erroneously interpreted as an error.
|
||||
with open(os.devnull, 'w') as dev_null:
|
||||
sys.stdout = dev_null
|
||||
__import__('%s')
|
||||
# If this is an ImportError, print this exception's message without a traceback.
|
||||
# ImportError messages are human-readable and require no additional context.
|
||||
except ImportError as exc:
|
||||
sys.stdout = sys_stdout
|
||||
print(exc)
|
||||
# Else, print this exception preceded by a traceback. traceback.print_exc()
|
||||
# prints to stderr rather than stdout and must not be called here!
|
||||
except Exception:
|
||||
sys.stdout = sys_stdout
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
"""
|
||||
|
||||
# List of the human-readable names of all available backends.
|
||||
backend_names = eval_statement(
|
||||
'import matplotlib; print(matplotlib.rcsetup.all_backends)')
|
||||
|
||||
# List of the fully-qualified names of all importable backend modules.
|
||||
module_names = []
|
||||
|
||||
# If the current system is not OS X and the "CocoaAgg" backend is available,
|
||||
# remove this backend from consideration. Attempting to import this backend
|
||||
# on non-OS X systems halts the current subprocess without printing output
|
||||
# or raising exceptions, preventing its reliable detection.
|
||||
if not is_darwin and 'CocoaAgg' in backend_names:
|
||||
backend_names.remove('CocoaAgg')
|
||||
|
||||
# For safety, attempt to import each backend in a unique subprocess.
|
||||
for backend_name in backend_names:
|
||||
if backend_name in KEEP:
|
||||
continue
|
||||
|
||||
module_name = 'matplotlib.backends.backend_%s' % backend_name.lower()
|
||||
stdout = exec_statement(import_statement % module_name)
|
||||
|
||||
# If no output was printed, this backend is importable.
|
||||
if not stdout:
|
||||
module_names.append(module_name)
|
||||
logger.info(' Matplotlib backend "%s": removed' % backend_name)
|
||||
|
||||
return module_names
|
||||
|
||||
# Freeze all importable backends, as PyInstaller is unable to determine exactly
|
||||
# which backends are required by the current program.
|
||||
e=get_matplotlib_backend_module_names()
|
||||
print(e)
|
||||
excludedimports = e
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<noInheritable/>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.VC90.CRT"
|
||||
version="9.0.21022.8"
|
||||
processorArchitecture="x86"
|
||||
publicKeyToken="1fc8b3b9a1e18e3b"/>
|
||||
<file name="MSVCR90.DLL"/>
|
||||
<file name="MSVCM90.DLL"/>
|
||||
<file name="MSVCP90.DLL"/>
|
||||
</assembly>
|
||||
@@ -14,7 +14,7 @@ with open("version.yml", 'r') as file:
|
||||
os.environ["PYFA_DIST_DIR"] = os.path.join(os.getcwd(), 'dist')
|
||||
|
||||
os.environ["PYFA_VERSION"] = version
|
||||
iscc = "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" # inno script location via wine
|
||||
iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
|
||||
|
||||
source = os.path.join(os.environ["PYFA_DIST_DIR"], "pyfa")
|
||||
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
#define MyAppURL "https://github.com/pyfa-org/Pyfa/"
|
||||
#define MyAppExeName "pyfa.exe"
|
||||
|
||||
; What version starts with the new structure (1.x.0). This is used to determine if we run directory structure cleanup
|
||||
#define MajorVersionFlag 2
|
||||
#define MinorVersionFlag 0
|
||||
|
||||
#ifndef MyOutputFile
|
||||
#define MyOutputFile LowerCase(StringChange(MyAppName+'-'+MyAppVersion+'-win', " ", "-"))
|
||||
#endif
|
||||
@@ -30,7 +26,6 @@
|
||||
#endif
|
||||
|
||||
[Setup]
|
||||
|
||||
; NOTE: The value of AppId uniquely identifies this application.
|
||||
; Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
@@ -41,6 +36,9 @@ AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
AppSupportURL={#MyAppURL}
|
||||
AppUpdatesURL={#MyAppURL}
|
||||
ArchitecturesAllowed=x64
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
CloseApplications=yes
|
||||
DefaultDirName={pf}\{#MyAppName}
|
||||
DefaultGroupName={#MyAppName}
|
||||
AllowNoIcons=yes
|
||||
@@ -49,7 +47,6 @@ OutputDir={#MyOutputDir}
|
||||
OutputBaseFilename={#MyOutputFile}
|
||||
SetupIconFile={#MyAppDir}\pyfa.ico
|
||||
SolidCompression=yes
|
||||
CloseApplications=yes
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
@@ -83,6 +80,7 @@ Type: files; Name: "{app}\*.pyc"
|
||||
|
||||
[Code]
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
function IsAppRunning(const FileName : string): Boolean;
|
||||
var
|
||||
FSWbemLocator: Variant;
|
||||
@@ -99,6 +97,7 @@ begin
|
||||
FSWbemLocator := Unassigned;
|
||||
end;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
procedure RemoveFromVirtualStore;
|
||||
var
|
||||
VirtualStore,FileName,FilePath:String;
|
||||
@@ -115,6 +114,7 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
begin
|
||||
if(IsAppRunning( 'pyfa.exe' )) then
|
||||
@@ -127,54 +127,61 @@ begin
|
||||
end
|
||||
end;
|
||||
|
||||
function GetUninstallString: string;
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
function GetUninstallString(): String;
|
||||
var
|
||||
sUnInstPath: string;
|
||||
sUnInstPath: String;
|
||||
sUnInstallString: String;
|
||||
begin
|
||||
Result := '';
|
||||
sUnInstPath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1'); //Your App GUID/ID
|
||||
sUnInstallString := '';
|
||||
if not RegQueryStringValue(HKLM, sUnInstPath, 'UninstallString', sUnInstallString) then
|
||||
RegQueryStringValue(HKCU, sUnInstPath, 'UninstallString', sUnInstallString);
|
||||
if not RegQueryStringValue(HKCU, sUnInstPath, 'UninstallString', sUnInstallString) then
|
||||
if not RegQueryStringValue(HKLM32, sUnInstPath, 'UninstallString', sUnInstallString) then
|
||||
RegQueryStringValue(HKCU32, sUnInstPath, 'UninstallString', sUnInstallString);
|
||||
Result := sUnInstallString;
|
||||
end;
|
||||
|
||||
function IsUpgrade: Boolean;
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
function UnInstallOldVersion(): Integer;
|
||||
var
|
||||
sUnInstallString: String;
|
||||
iResultCode: Integer;
|
||||
begin
|
||||
// Return Values:
|
||||
// 1 - uninstall string is empty
|
||||
// 2 - error executing the UnInstallString
|
||||
// 3 - successfully executed the UnInstallString
|
||||
|
||||
// default return value
|
||||
Result := 0;
|
||||
|
||||
// get the uninstall string of the old app
|
||||
sUnInstallString := GetUninstallString();
|
||||
if sUnInstallString <> '' then begin
|
||||
sUnInstallString := RemoveQuotes(sUnInstallString);
|
||||
if Exec(sUnInstallString, '/SILENT /NORESTART /SUPPRESSMSGBOXES','', SW_HIDE, ewWaitUntilTerminated, iResultCode) then
|
||||
Result := 3
|
||||
else
|
||||
Result := 2;
|
||||
end else
|
||||
Result := 1;
|
||||
end;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
function IsUpgrade(): Boolean;
|
||||
begin
|
||||
Result := (GetUninstallString() <> '');
|
||||
end;
|
||||
|
||||
function InitializeSetup: Boolean;
|
||||
var
|
||||
V: Integer;
|
||||
iResultCode: Integer;
|
||||
sUnInstallString: string;
|
||||
iOldVersionMajor: Cardinal;
|
||||
iOldVersionMinor: Cardinal;
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
begin
|
||||
Result := True; // in case when no previous version is found
|
||||
if RegValueExists(HKEY_LOCAL_MACHINE,'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1', 'UninstallString') then //Your App GUID/ID
|
||||
if (CurStep=ssInstall) then
|
||||
begin
|
||||
RegQueryDWordValue(HKEY_LOCAL_MACHINE,
|
||||
'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1',
|
||||
'MajorVersion', iOldVersionMajor);
|
||||
RegQueryDWordValue(HKEY_LOCAL_MACHINE,
|
||||
'Software\Microsoft\Windows\CurrentVersion\Uninstall\{3DA39096-C08D-49CD-90E0-1D177F32C8AA}_is1',
|
||||
'MinorVersion', iOldVersionMinor);
|
||||
if (iOldVersionMajor < {#MajorVersionFlag}) or ((iOldVersionMajor = {#MajorVersionFlag}) and (iOldVersionMinor < {#MinorVersionFlag})) then // If old version with old structure is installed.
|
||||
if (IsUpgrade()) then
|
||||
begin
|
||||
V := MsgBox(ExpandConstant('An old version of pyfa was detected. Due to recent changes in the application structure, you must uninstall the previous version first. This will not affect your user data (saved fittings, characters, etc.). Do you want to uninstall now?'), mbInformation, MB_YESNO); //Custom Message if App installed
|
||||
if V = IDYES then
|
||||
begin
|
||||
sUnInstallString := GetUninstallString();
|
||||
sUnInstallString := RemoveQuotes(sUnInstallString);
|
||||
Exec(ExpandConstant(sUnInstallString), '', '', SW_SHOW, ewWaitUntilTerminated, iResultCode);
|
||||
Result := True; //if you want to proceed after uninstall
|
||||
//Exit; //if you want to quit after uninstall
|
||||
end
|
||||
else
|
||||
Result := False; //when older version present and not uninstalled
|
||||
UnInstallOldVersion();
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
@@ -8,11 +8,6 @@
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.VC90.CRT" version="9.0.21022.8" processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b"></assemblyIdentity>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
|
||||
|
||||
@@ -20,7 +20,6 @@ added_files = [
|
||||
('../../service/jargon/*.yaml', 'service/jargon'),
|
||||
('../../dist_assets/win/pyfa.ico', '.'),
|
||||
('../../dist_assets/win/pyfa.exe.manifest', '.'),
|
||||
('../../dist_assets/win/Microsoft.VC90.CRT.manifest', '.'),
|
||||
(requests.certs.where(), '.'), # is this needed anymore?
|
||||
('../../eve.db', '.'),
|
||||
('../../README.md', '.'),
|
||||
@@ -30,7 +29,8 @@ added_files = [
|
||||
|
||||
import_these = [
|
||||
'numpy.core._dtype_ctypes', # https://github.com/pyinstaller/pyinstaller/issues/3982
|
||||
'sqlalchemy.ext.baked' # windows build doesn't launch without if when using sqlalchemy 1.3.x
|
||||
'sqlalchemy.ext.baked', # windows build doesn't launch without if when using sqlalchemy 1.3.x
|
||||
'pkg_resources.py2_warn' # issue 2156
|
||||
]
|
||||
|
||||
# Walk directories that do dynamic importing
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
# -*- mode: python -*-
|
||||
|
||||
# Note: This script is provided AS-IS for those that may be interested.
|
||||
# pyfa does not currently support pyInstaller (or any other build process) 100% at the moment
|
||||
|
||||
# Command line to build:
|
||||
# (Run from directory where pyfa.py and pyfa.spec lives.)
|
||||
# c:\Python27\scripts\pyinstaller.exe --clean --noconfirm --windowed --upx-dir=.\scripts\upx.exe pyfa.spec
|
||||
|
||||
# Don't forget to change the path to where your pyfa.py and pyfa.spec lives
|
||||
# pathex=['C:\\Users\\Ebag333\\Documents\\GitHub\\Ebag333\\Pyfa'],
|
||||
|
||||
import os
|
||||
|
||||
block_cipher = None
|
||||
|
||||
added_files = [
|
||||
( 'imgs/gui/*.png', 'imgs/gui' ),
|
||||
( 'imgs/gui/*.gif', 'imgs/gui' ),
|
||||
( 'imgs/icons/*.png', 'imgs/icons' ),
|
||||
( 'imgs/renders/*.png', 'imgs/renders' ),
|
||||
( 'dist_assets/win/pyfa.ico', '.' ),
|
||||
( 'dist_assets/cacert.pem', '.' ),
|
||||
( 'eve.db', '.' ),
|
||||
( 'README.md', '.' ),
|
||||
( 'LICENSE', '.' ),
|
||||
]
|
||||
|
||||
import_these = []
|
||||
|
||||
# Walk eos.effects and add all effects so we can import them properly
|
||||
for root, folders, files in os.walk("eos/effects"):
|
||||
for file_ in files:
|
||||
if file_.endswith(".py") and not file_.startswith("_"):
|
||||
mod_name = "{}.{}".format(
|
||||
root.replace("/", "."),
|
||||
file_.split(".py")[0],
|
||||
)
|
||||
import_these.append(mod_name)
|
||||
|
||||
a = Analysis(
|
||||
['pyfa.py'],
|
||||
pathex=['C:\\projects\\pyfa\\'],
|
||||
binaries=[],
|
||||
datas=added_files,
|
||||
hiddenimports=import_these,
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
)
|
||||
|
||||
pyz = PYZ(
|
||||
a.pure,
|
||||
a.zipped_data,
|
||||
cipher=block_cipher,
|
||||
)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
exclude_binaries=True,
|
||||
debug=True,
|
||||
console=True,
|
||||
strip=False,
|
||||
upx=True,
|
||||
name='pyfa_debug',
|
||||
icon='dist_assets/win/pyfa.ico',
|
||||
onefile=False,
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
onefile=False,
|
||||
name='pyfa_debug',
|
||||
icon='dist_assets/win/pyfa.ico',
|
||||
)
|
||||
71
eos/calc.py
Normal file
71
eos/calc.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2019 Ryan Holmes
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
|
||||
|
||||
# Just copy-paste penalization chain calculation code (with some modifications,
|
||||
# as multipliers arrive in different form) in here to not make actual attribute
|
||||
# calculations slower than they already are due to extra function calls
|
||||
def calculateMultiplier(multipliers):
|
||||
"""
|
||||
multipliers: dictionary in format:
|
||||
{stacking group name: [(mult, resist attr ID), (mult, resist attr ID)]}
|
||||
"""
|
||||
val = 1
|
||||
for penalizedMultipliers in multipliers.values():
|
||||
# A quick explanation of how this works:
|
||||
# 1: Bonuses and penalties are calculated seperately, so we'll have to filter each of them
|
||||
l1 = [v[0] for v in penalizedMultipliers if v[0] > 1]
|
||||
l2 = [v[0] for v in penalizedMultipliers if v[0] < 1]
|
||||
# 2: The most significant bonuses take the smallest penalty,
|
||||
# This means we'll have to sort
|
||||
abssort = lambda _val: -abs(_val - 1)
|
||||
l1.sort(key=abssort)
|
||||
l2.sort(key=abssort)
|
||||
# 3: The first module doesn't get penalized at all
|
||||
# Any module after the first takes penalties according to:
|
||||
# 1 + (multiplier - 1) * math.exp(- math.pow(i, 2) / 7.1289)
|
||||
for l in (l1, l2):
|
||||
for i in range(len(l)):
|
||||
bonus = l[i]
|
||||
val *= 1 + (bonus - 1) * math.exp(- i ** 2 / 7.1289)
|
||||
return val
|
||||
|
||||
|
||||
def calculateRangeFactor(srcOptimalRange, srcFalloffRange, distance, restrictedRange=True):
|
||||
"""Range strength/chance factor, applicable to guns, ewar, RRs, etc."""
|
||||
if distance is None:
|
||||
return 1
|
||||
if srcFalloffRange > 0:
|
||||
# Most modules cannot be activated when at 3x falloff range, with few exceptions like guns
|
||||
if restrictedRange and distance > srcOptimalRange + 3 * srcFalloffRange:
|
||||
return 0
|
||||
return 0.5 ** ((max(0, distance - srcOptimalRange) / srcFalloffRange) ** 2)
|
||||
elif distance <= srcOptimalRange:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def calculateLockTime(srcScanRes, tgtSigRadius):
|
||||
if not srcScanRes or not tgtSigRadius:
|
||||
return None
|
||||
return min(40000 / srcScanRes / math.asinh(tgtSigRadius) ** 2, 30 * 60)
|
||||
198
eos/capSim.py
198
eos/capSim.py
@@ -1,7 +1,7 @@
|
||||
import heapq
|
||||
import time
|
||||
from math import sqrt, exp
|
||||
from functools import reduce
|
||||
from collections import Counter
|
||||
|
||||
DAY = 24 * 60 * 60 * 1000
|
||||
|
||||
@@ -13,7 +13,7 @@ def lcm(a, b):
|
||||
return n / a
|
||||
|
||||
|
||||
class CapSimulator(object):
|
||||
class CapSimulator:
|
||||
"""Entity's EVE Capacitor Simulator"""
|
||||
|
||||
def __init__(self):
|
||||
@@ -21,6 +21,7 @@ class CapSimulator(object):
|
||||
|
||||
self.capacitorCapacity = 100
|
||||
self.capacitorRecharge = 1000
|
||||
self.startingCapacity = 1000
|
||||
|
||||
# max simulated time.
|
||||
self.t_max = DAY
|
||||
@@ -41,6 +42,14 @@ class CapSimulator(object):
|
||||
# relevant decimal digits of capacitor for LCM period optimization
|
||||
self.stability_precision = 1
|
||||
|
||||
# Stores how cap sim changed cap values outside of cap regen time
|
||||
self.saved_changes = ()
|
||||
self.saved_changes_internal = None
|
||||
|
||||
# Reports if sim was stopped due to detecting stability early
|
||||
self.optimize_repeats = True
|
||||
self.result_optimized_repeats = None
|
||||
|
||||
def scale_activation(self, duration, capNeed):
|
||||
for res in self.scale_resolutions:
|
||||
mod = duration % res
|
||||
@@ -59,7 +68,7 @@ class CapSimulator(object):
|
||||
return duration, capNeed
|
||||
|
||||
def init(self, modules):
|
||||
"""prepare modules. a list of (duration, capNeed, clipSize, disableStagger) tuples is
|
||||
"""prepare modules. a list of (duration, capNeed, clipSize, disableStagger, reloadTime, isInjector) tuples is
|
||||
expected, with clipSize 0 if the module has infinite ammo.
|
||||
"""
|
||||
self.modules = modules
|
||||
@@ -67,48 +76,57 @@ class CapSimulator(object):
|
||||
def reset(self):
|
||||
"""Reset the simulator state"""
|
||||
self.state = []
|
||||
self.saved_changes_internal = {}
|
||||
self.result_optimized_repeats = False
|
||||
mods = {}
|
||||
period = 1
|
||||
disable_period = False
|
||||
|
||||
# Loop over modules, clearing clipSize if applicable, and group modules based on attributes
|
||||
for (duration, capNeed, clipSize, disableStagger, reloadTime) in self.modules:
|
||||
for (duration, capNeed, clipSize, disableStagger, reloadTime, isInjector) in self.modules:
|
||||
if self.scale:
|
||||
duration, capNeed = self.scale_activation(duration, capNeed)
|
||||
|
||||
# set clipSize to infinite if reloads are disabled unless it's
|
||||
# a cap booster module.
|
||||
if not self.reload and capNeed > 0:
|
||||
# a cap booster module
|
||||
if not self.reload and not isInjector:
|
||||
clipSize = 0
|
||||
reloadTime = 0
|
||||
|
||||
# Group modules based on their properties
|
||||
if (duration, capNeed, clipSize, disableStagger, reloadTime) in mods:
|
||||
mods[(duration, capNeed, clipSize, disableStagger, reloadTime)] += 1
|
||||
key = (duration, capNeed, clipSize, disableStagger, reloadTime, isInjector)
|
||||
if key in mods:
|
||||
mods[key] += 1
|
||||
else:
|
||||
mods[(duration, capNeed, clipSize, disableStagger, reloadTime)] = 1
|
||||
mods[key] = 1
|
||||
|
||||
# Loop over grouped modules, configure staggering and push to the simulation state
|
||||
for (duration, capNeed, clipSize, disableStagger, reloadTime), amount in mods.items():
|
||||
for (duration, capNeed, clipSize, disableStagger, reloadTime, isInjector), amount in mods.items():
|
||||
# period optimization doesn't work when reloads are active.
|
||||
if clipSize:
|
||||
disable_period = True
|
||||
# Just push multiple instances if item is injector. We do not want to stagger them as we will
|
||||
# use them as needed and want them to be available right away
|
||||
if isInjector:
|
||||
for i in range(amount):
|
||||
heapq.heappush(self.state, [0, duration, capNeed, 0, clipSize, reloadTime, isInjector])
|
||||
continue
|
||||
if self.stagger and not disableStagger:
|
||||
# Stagger all mods if they do not need to be reloaded
|
||||
if clipSize == 0:
|
||||
duration = int(duration / amount)
|
||||
# Stagger mods after first
|
||||
else:
|
||||
stagger_amount = (duration * clipSize + reloadTime) / (amount * clipSize)
|
||||
for i in range(1, amount):
|
||||
heapq.heappush(self.state,
|
||||
[i * stagger_amount, duration,
|
||||
capNeed, 0, clipSize, reloadTime])
|
||||
heapq.heappush(self.state, [i * stagger_amount, duration, capNeed, 0, clipSize, reloadTime, isInjector])
|
||||
# If mods are not staggered - just multiply cap use
|
||||
else:
|
||||
capNeed *= amount
|
||||
|
||||
period = lcm(period, duration)
|
||||
|
||||
# period optimization doesn't work when reloads are active.
|
||||
if clipSize:
|
||||
disable_period = True
|
||||
|
||||
heapq.heappush(self.state, [0, duration, capNeed, 0, clipSize, reloadTime])
|
||||
heapq.heappush(self.state, [0, duration, capNeed, 0, clipSize, reloadTime, isInjector])
|
||||
|
||||
if disable_period:
|
||||
self.period = self.t_max
|
||||
@@ -119,7 +137,8 @@ class CapSimulator(object):
|
||||
"""Run the simulation"""
|
||||
|
||||
start = time.time()
|
||||
|
||||
awaitingInjectors = []
|
||||
awaitingInjectorsCounterWrap = Counter()
|
||||
self.reset()
|
||||
|
||||
push = heapq.heappush
|
||||
@@ -129,27 +148,36 @@ class CapSimulator(object):
|
||||
stability_precision = self.stability_precision
|
||||
period = self.period
|
||||
|
||||
activation = None
|
||||
iterations = 0
|
||||
|
||||
capCapacity = self.capacitorCapacity
|
||||
tau = self.capacitorRecharge / 5.0
|
||||
|
||||
cap_wrap = capCapacity # cap value at last period
|
||||
cap_lowest = capCapacity # lowest cap value encountered
|
||||
cap_lowest_pre = capCapacity # lowest cap value before activations
|
||||
cap = capCapacity # current cap value
|
||||
cap_wrap = self.startingCapacity # cap value at last period
|
||||
cap_lowest = self.startingCapacity # lowest cap value encountered
|
||||
cap_lowest_pre = self.startingCapacity # lowest cap value before activations
|
||||
cap = self.startingCapacity # current cap value
|
||||
t_wrap = self.period # point in time of next period
|
||||
|
||||
t_last = 0
|
||||
t_max = self.t_max
|
||||
|
||||
while 1:
|
||||
activation = pop(state)
|
||||
t_now, duration, capNeed, shot, clipSize, reloadTime = activation
|
||||
# Nothing to pop - might happen when no mods are activated, or when
|
||||
# only cap injectors are active (and are postponed by code below)
|
||||
try:
|
||||
activation = pop(state)
|
||||
except IndexError:
|
||||
break
|
||||
t_now, duration, capNeed, shot, clipSize, reloadTime, isInjector = activation
|
||||
|
||||
# Max time reached, stop simulation - we're stable
|
||||
if t_now >= t_max:
|
||||
break
|
||||
|
||||
cap = ((1.0 + (sqrt(cap / capCapacity) - 1.0) * exp((t_last - t_now) / tau)) ** 2) * capCapacity
|
||||
# Regenerate cap from last time point
|
||||
if t_now > t_last:
|
||||
cap = ((1.0 + (sqrt(cap / capCapacity) - 1.0) * exp((t_last - t_now) / tau)) ** 2) * capCapacity
|
||||
|
||||
if t_now != t_last:
|
||||
if cap < cap_lowest_pre:
|
||||
@@ -157,36 +185,104 @@ class CapSimulator(object):
|
||||
if t_now == t_wrap:
|
||||
# history is repeating itself, so if we have more cap now than last
|
||||
# time this happened, it is a stable setup.
|
||||
if cap >= cap_wrap:
|
||||
awaitingInjectorsCounterNow = Counter(awaitingInjectors)
|
||||
if self.optimize_repeats and cap >= cap_wrap and awaitingInjectorsCounterNow == awaitingInjectorsCounterWrap:
|
||||
self.result_optimized_repeats = True
|
||||
break
|
||||
cap_wrap = round(cap, stability_precision)
|
||||
awaitingInjectorsCounterWrap = awaitingInjectorsCounterNow
|
||||
t_wrap += period
|
||||
|
||||
cap -= capNeed
|
||||
if cap > capCapacity:
|
||||
cap = capCapacity
|
||||
|
||||
t_last = t_now
|
||||
iterations += 1
|
||||
|
||||
t_last = t_now
|
||||
# If injecting cap will "overshoot" max cap, postpone it
|
||||
if isInjector and cap - capNeed > capCapacity:
|
||||
awaitingInjectors.append((duration, capNeed, shot, clipSize, reloadTime, isInjector))
|
||||
|
||||
if cap < cap_lowest:
|
||||
if cap < 0.0:
|
||||
break
|
||||
cap_lowest = cap
|
||||
else:
|
||||
# If we will need more cap than we have, but we are not at 100% -
|
||||
# use awaiting cap injectors to top us up until we have enough or
|
||||
# until we're full
|
||||
if capNeed > cap and cap < capCapacity:
|
||||
while awaitingInjectors and capNeed > cap and capCapacity > cap:
|
||||
neededInjection = min(capNeed - cap, capCapacity - cap)
|
||||
# Find injectors which have just enough cap or more
|
||||
goodInjectors = [i for i in awaitingInjectors if -i[1] >= neededInjection]
|
||||
if goodInjectors:
|
||||
# Pick injector which overshoots the least
|
||||
bestInjector = min(goodInjectors, key=lambda i: -i[1])
|
||||
else:
|
||||
# Take the one which provides the most cap
|
||||
bestInjector = max(goodInjectors, key=lambda i: -i[1])
|
||||
# Use injector
|
||||
awaitingInjectors.remove(bestInjector)
|
||||
inj_duration, inj_capNeed, inj_shot, inj_clipSize, inj_reloadTime, inj_isInjector = bestInjector
|
||||
cap -= inj_capNeed
|
||||
if cap > capCapacity:
|
||||
cap = capCapacity
|
||||
self.saved_changes_internal[t_now] = cap
|
||||
# Add injector to regular state tracker
|
||||
inj_t_now = t_now
|
||||
inj_t_now += inj_duration
|
||||
inj_shot += 1
|
||||
if inj_clipSize:
|
||||
if inj_shot % inj_clipSize == 0:
|
||||
inj_shot = 0
|
||||
inj_t_now += inj_reloadTime
|
||||
push(state, [inj_t_now, inj_duration, inj_capNeed, inj_shot, inj_clipSize, inj_reloadTime, inj_isInjector])
|
||||
|
||||
# queue the next activation of this module
|
||||
t_now += duration
|
||||
shot += 1
|
||||
if clipSize:
|
||||
if shot % clipSize == 0:
|
||||
shot = 0
|
||||
t_now += reloadTime # include reload time
|
||||
activation[0] = t_now
|
||||
activation[3] = shot
|
||||
# Apply cap modification
|
||||
cap -= capNeed
|
||||
if cap > capCapacity:
|
||||
cap = capCapacity
|
||||
self.saved_changes_internal[t_now] = cap
|
||||
|
||||
if cap < cap_lowest:
|
||||
# Negative cap - we're unstable, simulation is over
|
||||
if cap < 0.0:
|
||||
break
|
||||
cap_lowest = cap
|
||||
|
||||
# Try using awaiting injectors to top up the cap after spending some
|
||||
while awaitingInjectors and cap < capCapacity:
|
||||
neededInjection = capCapacity - cap
|
||||
# Find injectors which do not overshoot max cap
|
||||
goodInjectors = [i for i in awaitingInjectors if -i[1] <= neededInjection]
|
||||
if not goodInjectors:
|
||||
break
|
||||
# Take the one which provides the most cap
|
||||
bestInjector = max(goodInjectors, key=lambda i: -i[1])
|
||||
# Use injector
|
||||
awaitingInjectors.remove(bestInjector)
|
||||
inj_duration, inj_capNeed, inj_shot, inj_clipSize, inj_reloadTime, inj_isInjector = bestInjector
|
||||
cap -= inj_capNeed
|
||||
if cap > capCapacity:
|
||||
cap = capCapacity
|
||||
self.saved_changes_internal[t_now] = cap
|
||||
# Add injector to regular state tracker
|
||||
inj_t_now = t_now
|
||||
inj_t_now += inj_duration
|
||||
inj_shot += 1
|
||||
if inj_clipSize:
|
||||
if inj_shot % inj_clipSize == 0:
|
||||
inj_shot = 0
|
||||
inj_t_now += inj_reloadTime
|
||||
push(state, [inj_t_now, inj_duration, inj_capNeed, inj_shot, inj_clipSize, inj_reloadTime, inj_isInjector])
|
||||
|
||||
# queue the next activation of this module
|
||||
t_now += duration
|
||||
shot += 1
|
||||
if clipSize:
|
||||
if shot % clipSize == 0:
|
||||
shot = 0
|
||||
t_now += reloadTime # include reload time
|
||||
activation[0] = t_now
|
||||
activation[3] = shot
|
||||
|
||||
push(state, activation)
|
||||
if activation is not None:
|
||||
push(state, activation)
|
||||
push(state, activation)
|
||||
|
||||
# update instance with relevant results.
|
||||
self.t = t_last
|
||||
@@ -194,7 +290,7 @@ class CapSimulator(object):
|
||||
|
||||
# calculate EVE's stability value
|
||||
try:
|
||||
avgDrain = reduce(float.__add__, [x[2] / x[1] for x in self.state], 0.0)
|
||||
avgDrain = sum(x[2] / x[1] for x in self.state)
|
||||
self.cap_stable_eve = 0.25 * (1.0 + sqrt(-(2.0 * avgDrain * tau - capCapacity) / capCapacity)) ** 2
|
||||
except ValueError:
|
||||
self.cap_stable_eve = 0.0
|
||||
@@ -204,7 +300,9 @@ class CapSimulator(object):
|
||||
self.cap_stable_low = cap_lowest
|
||||
self.cap_stable_high = cap_lowest_pre
|
||||
else:
|
||||
self.cap_stable_low = \
|
||||
self.cap_stable_high = 0.0
|
||||
self.cap_stable_low = self.cap_stable_high = 0.0
|
||||
|
||||
self.saved_changes = tuple((k / 1000, max(0, self.saved_changes_internal[k])) for k in sorted(self.saved_changes_internal))
|
||||
self.saved_changes_internal = None
|
||||
|
||||
self.runtime = time.time() - start
|
||||
|
||||
18
eos/const.py
18
eos/const.py
@@ -90,9 +90,12 @@ class FittingHardpoint(IntEnum):
|
||||
|
||||
@unique
|
||||
class SpoolType(IntEnum):
|
||||
SCALE = 0 # [0..1]
|
||||
TIME = 1 # Expressed via time in seconds since spool up started
|
||||
CYCLES = 2 # Expressed in amount of cycles since spool up started
|
||||
# Spool and cycle scale are different in case if max spool amount cannot
|
||||
# be divided by spool step without remainder
|
||||
SPOOL_SCALE = 0 # [0..1]
|
||||
CYCLE_SCALE = 1 # [0..1]
|
||||
TIME = 2 # Expressed via time in seconds since spool up started
|
||||
CYCLES = 3 # Expressed in amount of cycles since spool up started
|
||||
|
||||
|
||||
@unique
|
||||
@@ -101,3 +104,12 @@ class FitSystemSecurity(IntEnum):
|
||||
LOWSEC = 1
|
||||
NULLSEC = 2
|
||||
WSPACE = 3
|
||||
|
||||
|
||||
@unique
|
||||
class Operator(IntEnum):
|
||||
PREASSIGN = 0
|
||||
PREINCREASE = 1
|
||||
MULTIPLY = 2
|
||||
POSTINCREASE = 3
|
||||
FORCE = 4
|
||||
|
||||
@@ -17,34 +17,66 @@
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
import re
|
||||
import threading
|
||||
|
||||
from sqlalchemy import MetaData, create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import MetaData, create_engine, event
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
|
||||
from . import migration
|
||||
from eos import config
|
||||
from logbook import Logger
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
pyfalog.info("Initializing database")
|
||||
pyfalog.info("Gamedata connection: {0}", config.gamedata_connectionstring)
|
||||
pyfalog.info("Saveddata connection: {0}", config.saveddata_connectionstring)
|
||||
|
||||
|
||||
class ReadOnlyException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def re_fn(expr, item):
|
||||
try:
|
||||
reg = re.compile(expr, re.IGNORECASE)
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
return reg.search(item) is not None
|
||||
|
||||
|
||||
pyfalog.debug('Initializing gamedata')
|
||||
gamedata_connectionstring = config.gamedata_connectionstring
|
||||
if callable(gamedata_connectionstring):
|
||||
gamedata_engine = create_engine("sqlite://", creator=gamedata_connectionstring, echo=config.debug)
|
||||
else:
|
||||
gamedata_engine = create_engine(gamedata_connectionstring, echo=config.debug)
|
||||
|
||||
|
||||
@event.listens_for(gamedata_engine, 'connect')
|
||||
def create_functions(dbapi_connection, connection_record):
|
||||
dbapi_connection.create_function('regexp', 2, re_fn)
|
||||
|
||||
|
||||
gamedata_meta = MetaData()
|
||||
gamedata_meta.bind = gamedata_engine
|
||||
gamedata_session = sessionmaker(bind=gamedata_engine, autoflush=False, expire_on_commit=False)()
|
||||
GamedataSession = scoped_session(sessionmaker(bind=gamedata_engine, autoflush=False, expire_on_commit=False))
|
||||
gamedata_session = GamedataSession()
|
||||
|
||||
gamedata_sessions = {threading.get_ident(): gamedata_session}
|
||||
|
||||
|
||||
def get_gamedata_session():
|
||||
thread_id = threading.get_ident()
|
||||
if thread_id not in gamedata_sessions:
|
||||
gamedata_sessions[thread_id] = GamedataSession()
|
||||
return gamedata_sessions[thread_id]
|
||||
|
||||
|
||||
pyfalog.debug('Getting gamedata version')
|
||||
# This should be moved elsewhere, maybe as an actual query. Current, without try-except, it breaks when making a new
|
||||
# game db because we haven't reached gamedata_meta.create_all()
|
||||
try:
|
||||
@@ -54,12 +86,15 @@ try:
|
||||
config.gamedata_date = gamedata_session.execute(
|
||||
"SELECT `field_value` FROM `metadata` WHERE `field_name` LIKE 'dump_time'"
|
||||
).fetchone()[0]
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception as e:
|
||||
pyfalog.warning("Missing gamedata version.")
|
||||
pyfalog.critical(e)
|
||||
config.gamedata_version = None
|
||||
config.gamedata_date = None
|
||||
|
||||
pyfalog.debug('Initializing saveddata')
|
||||
saveddata_connectionstring = config.saveddata_connectionstring
|
||||
if saveddata_connectionstring is not None:
|
||||
if callable(saveddata_connectionstring):
|
||||
@@ -76,16 +111,19 @@ else:
|
||||
# Lock controlling any changes introduced to session
|
||||
sd_lock = threading.RLock()
|
||||
|
||||
pyfalog.debug('Importing gamedata DB scheme')
|
||||
# Import all the definitions for all our database stuff
|
||||
# noinspection PyPep8
|
||||
from eos.db.gamedata import alphaClones, attribute, category, effect, group, item, marketGroup, metaData, metaGroup, queries, traits, unit, dynamicAttributes
|
||||
from eos.db.gamedata import alphaClones, attribute, category, effect, group, item, marketGroup, metaData, metaGroup, queries, traits, unit, dynamicAttributes, implantSet
|
||||
pyfalog.debug('Importing saveddata DB scheme')
|
||||
# noinspection PyPep8
|
||||
from eos.db.saveddata import booster, cargo, character, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, loadDefaultDatabaseValues, \
|
||||
miscData, mutator, module, override, price, queries, skill, targetResists, user
|
||||
from eos.db.saveddata import booster, cargo, character, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, \
|
||||
miscData, mutator, module, override, price, queries, skill, targetProfile, user
|
||||
|
||||
# Import queries
|
||||
pyfalog.debug('Importing gamedata queries')
|
||||
# noinspection PyPep8
|
||||
from eos.db.gamedata.queries import *
|
||||
pyfalog.debug('Importing saveddata queries')
|
||||
# noinspection PyPep8
|
||||
from eos.db.saveddata.queries import *
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__all__ = ["attribute", "category", "effect", "group", "metaData", "dynamicAttributes",
|
||||
"item", "marketGroup", "metaGroup", "unit", "alphaClones"]
|
||||
"item", "marketGroup", "metaGroup", "unit", "alphaClones", "implantSet"]
|
||||
|
||||
33
eos/db/gamedata/implantSet.py
Normal file
33
eos/db/gamedata/implantSet.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# ===============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of eos.
|
||||
#
|
||||
# eos is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# eos is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
from sqlalchemy import Column, String, Integer, Table
|
||||
from sqlalchemy.orm import mapper, synonym
|
||||
|
||||
from eos.db import gamedata_meta
|
||||
from eos.gamedata import ImplantSet
|
||||
|
||||
implant_set_table = Table("implantsets", gamedata_meta,
|
||||
Column("setID", Integer, primary_key=True),
|
||||
Column("setName", String),
|
||||
Column("gradeName", String),
|
||||
Column("implants", String))
|
||||
|
||||
mapper(ImplantSet, implant_set_table,
|
||||
properties={"ID": synonym("setID")})
|
||||
@@ -25,7 +25,7 @@ from sqlalchemy.orm.collections import attribute_mapped_collection
|
||||
from eos.db import gamedata_meta
|
||||
from eos.db.gamedata.dynamicAttributes import dynamicApplicable_table
|
||||
from eos.db.gamedata.effect import typeeffects_table
|
||||
from eos.gamedata import Attribute, DynamicItem, Effect, Group, Item, MetaType, Traits
|
||||
from eos.gamedata import Attribute, DynamicItem, Effect, Group, Item, Traits, MetaGroup
|
||||
|
||||
items_table = Table("invtypes", gamedata_meta,
|
||||
Column("typeID", Integer, primary_key=True),
|
||||
@@ -33,17 +33,18 @@ items_table = Table("invtypes", gamedata_meta,
|
||||
Column("description", String),
|
||||
Column("raceID", Integer),
|
||||
Column("factionID", Integer),
|
||||
Column("volume", Float),
|
||||
Column("mass", Float),
|
||||
Column("capacity", Float),
|
||||
Column("published", Boolean),
|
||||
Column("marketGroupID", Integer, ForeignKey("invmarketgroups.marketGroupID")),
|
||||
Column("iconID", Integer),
|
||||
Column("graphicID", Integer),
|
||||
Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True),
|
||||
Column("replacements", String))
|
||||
Column("metaLevel", Integer),
|
||||
Column("metaGroupID", Integer, ForeignKey("invmetagroups.metaGroupID"), index=True),
|
||||
Column("variationParentTypeID", Integer, ForeignKey("invtypes.typeID"), index=True),
|
||||
Column("replacements", String),
|
||||
Column("reqskills", String),
|
||||
Column("requiredfor", String))
|
||||
|
||||
from .metaGroup import metatypes_table # noqa
|
||||
from .traits import traits_table # noqa
|
||||
|
||||
mapper(Item, items_table,
|
||||
@@ -51,9 +52,8 @@ mapper(Item, items_table,
|
||||
"group" : relation(Group, backref=backref("items", cascade="all,delete")),
|
||||
"_Item__attributes": relation(Attribute, cascade='all, delete, delete-orphan', collection_class=attribute_mapped_collection('name')),
|
||||
"effects": relation(Effect, secondary=typeeffects_table, collection_class=attribute_mapped_collection('name')),
|
||||
"metaGroup" : relation(MetaType,
|
||||
primaryjoin=metatypes_table.c.typeID == items_table.c.typeID,
|
||||
uselist=False),
|
||||
"metaGroup" : relation(MetaGroup, backref=backref("items", cascade="all,delete")),
|
||||
"varParent" : relation(Item, backref=backref("varChildren", cascade="all,delete"), remote_side=items_table.c.typeID),
|
||||
"ID" : synonym("typeID"),
|
||||
"name" : synonym("typeName"),
|
||||
"description" : deferred(items_table.c.description),
|
||||
@@ -64,7 +64,6 @@ mapper(Item, items_table,
|
||||
primaryjoin=dynamicApplicable_table.c.applicableTypeID == items_table.c.typeID,
|
||||
secondaryjoin=dynamicApplicable_table.c.typeID == DynamicItem.typeID,
|
||||
secondary=dynamicApplicable_table,
|
||||
backref="applicableItems")
|
||||
})
|
||||
backref="applicableItems")})
|
||||
|
||||
Item.category = association_proxy("group", "category")
|
||||
|
||||
@@ -17,35 +17,17 @@
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
from sqlalchemy import Table, Column, Integer, ForeignKey, String
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.orm import relation, mapper, synonym
|
||||
from sqlalchemy import Table, Column, Integer, String
|
||||
from sqlalchemy.orm import mapper, synonym
|
||||
|
||||
from eos.db import gamedata_meta
|
||||
from eos.db.gamedata.item import items_table
|
||||
from eos.gamedata import Item, MetaGroup, MetaType
|
||||
from eos.gamedata import MetaGroup
|
||||
|
||||
metagroups_table = Table("invmetagroups", gamedata_meta,
|
||||
Column("metaGroupID", Integer, primary_key=True),
|
||||
Column("metaGroupName", String))
|
||||
|
||||
metatypes_table = Table("invmetatypes", gamedata_meta,
|
||||
Column("typeID", Integer, ForeignKey("invtypes.typeID"), primary_key=True),
|
||||
Column("parentTypeID", Integer, ForeignKey("invtypes.typeID")),
|
||||
Column("metaGroupID", Integer, ForeignKey("invmetagroups.metaGroupID")))
|
||||
|
||||
mapper(MetaGroup, metagroups_table,
|
||||
properties={
|
||||
"ID" : synonym("metaGroupID"),
|
||||
"name": synonym("metaGroupName")
|
||||
})
|
||||
|
||||
mapper(MetaType, metatypes_table,
|
||||
properties={
|
||||
"ID" : synonym("metaGroupID"),
|
||||
"parent": relation(Item, primaryjoin=metatypes_table.c.parentTypeID == items_table.c.typeID),
|
||||
"items" : relation(Item, primaryjoin=metatypes_table.c.typeID == items_table.c.typeID),
|
||||
"info" : relation(MetaGroup, lazy=False)
|
||||
})
|
||||
|
||||
MetaType.name = association_proxy("info", "name")
|
||||
"name": synonym("metaGroupName")})
|
||||
|
||||
@@ -22,11 +22,11 @@ from sqlalchemy.orm import aliased, exc, join
|
||||
from sqlalchemy.sql import and_, or_, select
|
||||
|
||||
import eos.config
|
||||
from eos.db import gamedata_session
|
||||
from eos.db import get_gamedata_session
|
||||
from eos.db.gamedata.item import items_table
|
||||
from eos.db.gamedata.group import groups_table
|
||||
from eos.db.gamedata.metaGroup import items_table, metatypes_table
|
||||
from eos.db.util import processEager, processWhere
|
||||
from eos.gamedata import AlphaClone, Attribute, AttributeInfo, Category, DynamicItem, Group, Item, MarketGroup, MetaData, MetaGroup
|
||||
from eos.gamedata import AlphaClone, Attribute, AttributeInfo, Category, DynamicItem, Group, Item, MarketGroup, MetaData, MetaGroup, ImplantSet
|
||||
|
||||
cache = {}
|
||||
configVal = getattr(eos.config, "gamedataCache", None)
|
||||
@@ -64,7 +64,7 @@ else:
|
||||
return deco
|
||||
|
||||
|
||||
def sqlizeString(line):
|
||||
def sqlizeNormalString(line):
|
||||
# Escape backslashes first, as they will be as escape symbol in queries
|
||||
# Then escape percent and underscore signs
|
||||
# Finally, replace generic wildcards with sql-style wildcards
|
||||
@@ -79,28 +79,39 @@ itemNameMap = {}
|
||||
def getItem(lookfor, eager=None):
|
||||
if isinstance(lookfor, int):
|
||||
if eager is None:
|
||||
item = gamedata_session.query(Item).get(lookfor)
|
||||
item = get_gamedata_session().query(Item).get(lookfor)
|
||||
else:
|
||||
item = gamedata_session.query(Item).options(*processEager(eager)).filter(Item.ID == lookfor).first()
|
||||
item = get_gamedata_session().query(Item).options(*processEager(eager)).filter(Item.ID == lookfor).first()
|
||||
elif isinstance(lookfor, str):
|
||||
if lookfor in itemNameMap:
|
||||
id = itemNameMap[lookfor]
|
||||
if eager is None:
|
||||
item = gamedata_session.query(Item).get(id)
|
||||
item = get_gamedata_session().query(Item).get(id)
|
||||
else:
|
||||
item = gamedata_session.query(Item).options(*processEager(eager)).filter(Item.ID == id).first()
|
||||
item = get_gamedata_session().query(Item).options(*processEager(eager)).filter(Item.ID == id).first()
|
||||
else:
|
||||
# Item names are unique, so we can use first() instead of one()
|
||||
item = gamedata_session.query(Item).options(*processEager(eager)).filter(Item.name == lookfor).first()
|
||||
itemNameMap[lookfor] = item.ID
|
||||
item = get_gamedata_session().query(Item).options(*processEager(eager)).filter(Item.name == lookfor).first()
|
||||
if item is not None:
|
||||
itemNameMap[lookfor] = item.ID
|
||||
else:
|
||||
raise TypeError("Need integer or string as argument")
|
||||
return item
|
||||
|
||||
@cachedQuery(1, "itemIDs")
|
||||
def getItems(itemIDs, eager=None):
|
||||
if not isinstance(itemIDs, (tuple, list, set)) or not all(isinstance(t, int) for t in itemIDs):
|
||||
raise TypeError("Need iterable of integers as argument")
|
||||
if eager is None:
|
||||
items = get_gamedata_session().query(Item).filter(Item.ID.in_(itemIDs)).all()
|
||||
else:
|
||||
items = get_gamedata_session().query(Item).options(*processEager(eager)).filter(Item.ID.in_(itemIDs)).all()
|
||||
return items
|
||||
|
||||
|
||||
def getMutaplasmid(lookfor, eager=None):
|
||||
if isinstance(lookfor, int):
|
||||
item = gamedata_session.query(DynamicItem).filter(DynamicItem.ID == lookfor).first()
|
||||
item = get_gamedata_session().query(DynamicItem).filter(DynamicItem.ID == lookfor).first()
|
||||
else:
|
||||
raise TypeError("Need integer as argument")
|
||||
return item
|
||||
@@ -108,7 +119,7 @@ def getMutaplasmid(lookfor, eager=None):
|
||||
|
||||
def getItemWithBaseItemAttribute(lookfor, baseItemID, eager=None):
|
||||
# A lot of this is described in more detail in #1597
|
||||
item = gamedata_session.query(Item).get(lookfor)
|
||||
item = get_gamedata_session().query(Item).get(lookfor)
|
||||
base = getItem(baseItemID)
|
||||
|
||||
# we have to load all attributes for this object, otherwise we'll lose access to them when we expunge.
|
||||
@@ -124,7 +135,7 @@ def getItemWithBaseItemAttribute(lookfor, baseItemID, eager=None):
|
||||
# Expunge the item form the session. This is required to have different Abyssal / Base combinations loaded in memory.
|
||||
# Without expunging it, once one Abyssal Web is created, SQLAlchmey will use it for all others. We don't want this,
|
||||
# we want to generate a completely new object to work with
|
||||
gamedata_session.expunge(item)
|
||||
get_gamedata_session().expunge(item)
|
||||
return item
|
||||
|
||||
|
||||
@@ -147,7 +158,7 @@ def getItems(lookfor, eager=None):
|
||||
|
||||
if len(toGet) > 0:
|
||||
# Get items that aren't currently cached, and store them in the cache
|
||||
items = gamedata_session.query(Item).filter(Item.ID.in_(toGet)).all()
|
||||
items = get_gamedata_session().query(Item).filter(Item.ID.in_(toGet)).all()
|
||||
for item in items:
|
||||
cache[(item.ID, None)] = item
|
||||
results += items
|
||||
@@ -161,9 +172,9 @@ def getItems(lookfor, eager=None):
|
||||
def getAlphaClone(lookfor, eager=None):
|
||||
if isinstance(lookfor, int):
|
||||
if eager is None:
|
||||
item = gamedata_session.query(AlphaClone).get(lookfor)
|
||||
item = get_gamedata_session().query(AlphaClone).get(lookfor)
|
||||
else:
|
||||
item = gamedata_session.query(AlphaClone).options(*processEager(eager)).filter(AlphaClone.ID == lookfor).first()
|
||||
item = get_gamedata_session().query(AlphaClone).options(*processEager(eager)).filter(AlphaClone.ID == lookfor).first()
|
||||
else:
|
||||
raise TypeError("Need integer as argument")
|
||||
return item
|
||||
@@ -171,7 +182,7 @@ def getAlphaClone(lookfor, eager=None):
|
||||
|
||||
def getAlphaCloneList(eager=None):
|
||||
eager = processEager(eager)
|
||||
clones = gamedata_session.query(AlphaClone).options(*eager).all()
|
||||
clones = get_gamedata_session().query(AlphaClone).options(*eager).all()
|
||||
return clones
|
||||
|
||||
|
||||
@@ -182,20 +193,21 @@ groupNameMap = {}
|
||||
def getGroup(lookfor, eager=None):
|
||||
if isinstance(lookfor, int):
|
||||
if eager is None:
|
||||
group = gamedata_session.query(Group).get(lookfor)
|
||||
group = get_gamedata_session().query(Group).get(lookfor)
|
||||
else:
|
||||
group = gamedata_session.query(Group).options(*processEager(eager)).filter(Group.ID == lookfor).first()
|
||||
group = get_gamedata_session().query(Group).options(*processEager(eager)).filter(Group.ID == lookfor).first()
|
||||
elif isinstance(lookfor, str):
|
||||
if lookfor in groupNameMap:
|
||||
id = groupNameMap[lookfor]
|
||||
if eager is None:
|
||||
group = gamedata_session.query(Group).get(id)
|
||||
group = get_gamedata_session().query(Group).get(id)
|
||||
else:
|
||||
group = gamedata_session.query(Group).options(*processEager(eager)).filter(Group.ID == id).first()
|
||||
group = get_gamedata_session().query(Group).options(*processEager(eager)).filter(Group.ID == id).first()
|
||||
else:
|
||||
# Group names are unique, so we can use first() instead of one()
|
||||
group = gamedata_session.query(Group).options(*processEager(eager)).filter(Group.name == lookfor).first()
|
||||
groupNameMap[lookfor] = group.ID
|
||||
group = get_gamedata_session().query(Group).options(*processEager(eager)).filter(Group.name == lookfor).first()
|
||||
if group is not None:
|
||||
groupNameMap[lookfor] = group.ID
|
||||
else:
|
||||
raise TypeError("Need integer or string as argument")
|
||||
return group
|
||||
@@ -208,23 +220,24 @@ categoryNameMap = {}
|
||||
def getCategory(lookfor, eager=None):
|
||||
if isinstance(lookfor, int):
|
||||
if eager is None:
|
||||
category = gamedata_session.query(Category).get(lookfor)
|
||||
category = get_gamedata_session().query(Category).get(lookfor)
|
||||
else:
|
||||
category = gamedata_session.query(Category).options(*processEager(eager)).filter(
|
||||
category = get_gamedata_session().query(Category).options(*processEager(eager)).filter(
|
||||
Category.ID == lookfor).first()
|
||||
elif isinstance(lookfor, str):
|
||||
if lookfor in categoryNameMap:
|
||||
id = categoryNameMap[lookfor]
|
||||
if eager is None:
|
||||
category = gamedata_session.query(Category).get(id)
|
||||
category = get_gamedata_session().query(Category).get(id)
|
||||
else:
|
||||
category = gamedata_session.query(Category).options(*processEager(eager)).filter(
|
||||
category = get_gamedata_session().query(Category).options(*processEager(eager)).filter(
|
||||
Category.ID == id).first()
|
||||
else:
|
||||
# Category names are unique, so we can use first() instead of one()
|
||||
category = gamedata_session.query(Category).options(*processEager(eager)).filter(
|
||||
category = get_gamedata_session().query(Category).options(*processEager(eager)).filter(
|
||||
Category.name == lookfor).first()
|
||||
categoryNameMap[lookfor] = category.ID
|
||||
if category is not None:
|
||||
categoryNameMap[lookfor] = category.ID
|
||||
else:
|
||||
raise TypeError("Need integer or string as argument")
|
||||
return category
|
||||
@@ -237,35 +250,40 @@ metaGroupNameMap = {}
|
||||
def getMetaGroup(lookfor, eager=None):
|
||||
if isinstance(lookfor, int):
|
||||
if eager is None:
|
||||
metaGroup = gamedata_session.query(MetaGroup).get(lookfor)
|
||||
metaGroup = get_gamedata_session().query(MetaGroup).get(lookfor)
|
||||
else:
|
||||
metaGroup = gamedata_session.query(MetaGroup).options(*processEager(eager)).filter(
|
||||
metaGroup = get_gamedata_session().query(MetaGroup).options(*processEager(eager)).filter(
|
||||
MetaGroup.ID == lookfor).first()
|
||||
elif isinstance(lookfor, str):
|
||||
if lookfor in metaGroupNameMap:
|
||||
id = metaGroupNameMap[lookfor]
|
||||
if eager is None:
|
||||
metaGroup = gamedata_session.query(MetaGroup).get(id)
|
||||
metaGroup = get_gamedata_session().query(MetaGroup).get(id)
|
||||
else:
|
||||
metaGroup = gamedata_session.query(MetaGroup).options(*processEager(eager)).filter(
|
||||
metaGroup = get_gamedata_session().query(MetaGroup).options(*processEager(eager)).filter(
|
||||
MetaGroup.ID == id).first()
|
||||
else:
|
||||
# MetaGroup names are unique, so we can use first() instead of one()
|
||||
metaGroup = gamedata_session.query(MetaGroup).options(*processEager(eager)).filter(
|
||||
metaGroup = get_gamedata_session().query(MetaGroup).options(*processEager(eager)).filter(
|
||||
MetaGroup.name == lookfor).first()
|
||||
metaGroupNameMap[lookfor] = metaGroup.ID
|
||||
if metaGroup is not None:
|
||||
metaGroupNameMap[lookfor] = metaGroup.ID
|
||||
else:
|
||||
raise TypeError("Need integer or string as argument")
|
||||
return metaGroup
|
||||
|
||||
|
||||
def getMetaGroups():
|
||||
return get_gamedata_session().query(MetaGroup).all()
|
||||
|
||||
|
||||
@cachedQuery(1, "lookfor")
|
||||
def getMarketGroup(lookfor, eager=None):
|
||||
if isinstance(lookfor, int):
|
||||
if eager is None:
|
||||
marketGroup = gamedata_session.query(MarketGroup).get(lookfor)
|
||||
marketGroup = get_gamedata_session().query(MarketGroup).get(lookfor)
|
||||
else:
|
||||
marketGroup = gamedata_session.query(MarketGroup).options(*processEager(eager)).filter(
|
||||
marketGroup = get_gamedata_session().query(MarketGroup).options(*processEager(eager)).filter(
|
||||
MarketGroup.ID == lookfor).first()
|
||||
else:
|
||||
raise TypeError("Need integer as argument")
|
||||
@@ -277,7 +295,7 @@ def getMarketTreeNodeIds(rootNodeIds):
|
||||
addedIds = set(rootNodeIds)
|
||||
while addedIds:
|
||||
allIds.update(addedIds)
|
||||
addedIds = {mg.ID for mg in gamedata_session.query(MarketGroup).filter(MarketGroup.parentGroupID.in_(addedIds))}
|
||||
addedIds = {mg.ID for mg in get_gamedata_session().query(MarketGroup).filter(MarketGroup.parentGroupID.in_(addedIds))}
|
||||
return allIds
|
||||
|
||||
|
||||
@@ -291,7 +309,7 @@ def getItemsByCategory(filter, where=None, eager=None):
|
||||
raise TypeError("Need integer or string as argument")
|
||||
|
||||
filter = processWhere(filter, where)
|
||||
return gamedata_session.query(Item).options(*processEager(eager)).join(Item.group, Group.category).filter(
|
||||
return get_gamedata_session().query(Item).options(*processEager(eager)).join(Item.group, Group.category).filter(
|
||||
filter).all()
|
||||
|
||||
|
||||
@@ -306,9 +324,9 @@ def searchItems(nameLike, where=None, join=None, eager=None):
|
||||
if not hasattr(join, "__iter__"):
|
||||
join = (join,)
|
||||
|
||||
items = gamedata_session.query(Item).options(*processEager(eager)).join(*join)
|
||||
items = get_gamedata_session().query(Item).options(*processEager(eager)).join(*join)
|
||||
for token in nameLike.split(' '):
|
||||
token_safe = "%{0}%".format(sqlizeString(token))
|
||||
token_safe = "%{0}%".format(sqlizeNormalString(token))
|
||||
if where is not None:
|
||||
items = items.filter(and_(Item.name.like(token_safe, escape="\\"), where))
|
||||
else:
|
||||
@@ -317,14 +335,35 @@ def searchItems(nameLike, where=None, join=None, eager=None):
|
||||
return items
|
||||
|
||||
|
||||
@cachedQuery(3, "tokens", "where", "join")
|
||||
def searchItemsRegex(tokens, where=None, join=None, eager=None):
|
||||
if not isinstance(tokens, (tuple, list)) or not all(isinstance(t, str) for t in tokens):
|
||||
raise TypeError("Need tuple or list of strings as argument")
|
||||
|
||||
if join is None:
|
||||
join = tuple()
|
||||
|
||||
if not hasattr(join, "__iter__"):
|
||||
join = (join,)
|
||||
|
||||
items = get_gamedata_session().query(Item).options(*processEager(eager)).join(*join)
|
||||
for token in tokens:
|
||||
if where is not None:
|
||||
items = items.filter(and_(Item.name.op('regexp')(token), where))
|
||||
else:
|
||||
items = items.filter(Item.name.op('regexp')(token))
|
||||
items = items.limit(100).all()
|
||||
return items
|
||||
|
||||
|
||||
@cachedQuery(3, "where", "nameLike", "join")
|
||||
def searchSkills(nameLike, where=None, eager=None):
|
||||
if not isinstance(nameLike, str):
|
||||
raise TypeError("Need string as argument")
|
||||
|
||||
items = gamedata_session.query(Item).options(*processEager(eager)).join(Item.group, Group.category)
|
||||
items = get_gamedata_session().query(Item).options(*processEager(eager)).join(Item.group, Group.category)
|
||||
for token in nameLike.split(' '):
|
||||
token_safe = "%{0}%".format(sqlizeString(token))
|
||||
token_safe = "%{0}%".format(sqlizeNormalString(token))
|
||||
if where is not None:
|
||||
items = items.filter(and_(Item.name.like(token_safe, escape="\\"), Category.ID == 16, where))
|
||||
else:
|
||||
@@ -342,11 +381,9 @@ def getVariations(itemids, groupIDs=None, where=None, eager=None):
|
||||
if len(itemids) == 0:
|
||||
return []
|
||||
|
||||
itemfilter = or_(*(metatypes_table.c.parentTypeID == itemid for itemid in itemids))
|
||||
itemfilter = or_(*(items_table.c.variationParentTypeID == itemid for itemid in itemids))
|
||||
filter = processWhere(itemfilter, where)
|
||||
joinon = items_table.c.typeID == metatypes_table.c.typeID
|
||||
vars = gamedata_session.query(Item).options(*processEager(eager)).join((metatypes_table, joinon)).filter(
|
||||
filter).all()
|
||||
vars = get_gamedata_session().query(Item).options(*processEager(eager)).filter(filter).all()
|
||||
|
||||
if vars:
|
||||
return vars
|
||||
@@ -354,7 +391,7 @@ def getVariations(itemids, groupIDs=None, where=None, eager=None):
|
||||
itemfilter = or_(*(groups_table.c.groupID == groupID for groupID in groupIDs))
|
||||
filter = processWhere(itemfilter, where)
|
||||
joinon = items_table.c.groupID == groups_table.c.groupID
|
||||
vars = gamedata_session.query(Item).options(*processEager(eager)).join((groups_table, joinon)).filter(
|
||||
vars = get_gamedata_session().query(Item).options(*processEager(eager)).join((groups_table, joinon)).filter(
|
||||
filter).all()
|
||||
|
||||
return vars
|
||||
@@ -369,7 +406,7 @@ def getAttributeInfo(attr, eager=None):
|
||||
else:
|
||||
raise TypeError("Need integer or string as argument")
|
||||
try:
|
||||
result = gamedata_session.query(AttributeInfo).options(*processEager(eager)).filter(filter).one()
|
||||
result = get_gamedata_session().query(AttributeInfo).options(*processEager(eager)).filter(filter).one()
|
||||
except exc.NoResultFound:
|
||||
result = None
|
||||
return result
|
||||
@@ -378,7 +415,7 @@ def getAttributeInfo(attr, eager=None):
|
||||
@cachedQuery(1, "field")
|
||||
def getMetaData(field):
|
||||
if isinstance(field, str):
|
||||
data = gamedata_session.query(MetaData).get(field)
|
||||
data = get_gamedata_session().query(MetaData).get(field)
|
||||
else:
|
||||
raise TypeError("Need string as argument")
|
||||
return data
|
||||
@@ -397,12 +434,12 @@ def directAttributeRequest(itemIDs, attrIDs):
|
||||
and_(Attribute.attributeID.in_(attrIDs), Item.typeID.in_(itemIDs)),
|
||||
from_obj=[join(Attribute, Item)])
|
||||
|
||||
result = gamedata_session.execute(q).fetchall()
|
||||
result = get_gamedata_session().execute(q).fetchall()
|
||||
return result
|
||||
|
||||
|
||||
def getAbyssalTypes():
|
||||
return set([r.resultingTypeID for r in gamedata_session.query(DynamicItem.resultingTypeID).distinct()])
|
||||
return set([r.resultingTypeID for r in get_gamedata_session().query(DynamicItem.resultingTypeID).distinct()])
|
||||
|
||||
|
||||
@cachedQuery(1, "itemID")
|
||||
@@ -410,9 +447,9 @@ def getDynamicItem(itemID, eager=None):
|
||||
try:
|
||||
if isinstance(itemID, int):
|
||||
if eager is None:
|
||||
result = gamedata_session.query(DynamicItem).filter(DynamicItem.ID == itemID).one()
|
||||
result = get_gamedata_session().query(DynamicItem).filter(DynamicItem.ID == itemID).one()
|
||||
else:
|
||||
result = gamedata_session.query(DynamicItem).options(*processEager(eager)).filter(DynamicItem.ID == itemID).one()
|
||||
result = get_gamedata_session().query(DynamicItem).options(*processEager(eager)).filter(DynamicItem.ID == itemID).one()
|
||||
else:
|
||||
raise TypeError("Need integer as argument")
|
||||
except exc.NoResultFound:
|
||||
@@ -420,23 +457,7 @@ def getDynamicItem(itemID, eager=None):
|
||||
return result
|
||||
|
||||
|
||||
def getRequiredFor(itemID, attrMapping):
|
||||
Attribute1 = aliased(Attribute)
|
||||
Attribute2 = aliased(Attribute)
|
||||
|
||||
skillToLevelClauses = []
|
||||
|
||||
for attrSkill, attrLevel in attrMapping.items():
|
||||
skillToLevelClauses.append(and_(Attribute1.attributeID == attrSkill, Attribute2.attributeID == attrLevel))
|
||||
|
||||
queryOr = or_(*skillToLevelClauses)
|
||||
|
||||
q = select((Attribute2.typeID, Attribute2.value),
|
||||
and_(Attribute1.value == itemID, queryOr),
|
||||
from_obj=[
|
||||
join(Attribute1, Attribute2, Attribute1.typeID == Attribute2.typeID)
|
||||
])
|
||||
|
||||
result = gamedata_session.execute(q).fetchall()
|
||||
|
||||
return result
|
||||
@cachedQuery(1, "lookfor")
|
||||
def getAllImplantSets():
|
||||
implantSets = get_gamedata_session().query(ImplantSet).all()
|
||||
return implantSets
|
||||
|
||||
@@ -8,43 +8,25 @@ many upgrade files as there are database versions (version 5 would include
|
||||
upgrade files 1-5)
|
||||
"""
|
||||
|
||||
import pkgutil
|
||||
import re
|
||||
|
||||
from eos.utils.pyinst_support import iterNamespace
|
||||
|
||||
updates = {}
|
||||
appVersion = 0
|
||||
|
||||
prefix = __name__ + "."
|
||||
|
||||
# load modules to work based with and without pyinstaller
|
||||
# from: https://github.com/webcomics/dosage/blob/master/dosagelib/loader.py
|
||||
# see: https://github.com/pyinstaller/pyinstaller/issues/1905
|
||||
|
||||
# load modules using iter_modules()
|
||||
# (should find all filters in normal build, but not pyinstaller)
|
||||
module_names = [m[1] for m in pkgutil.iter_modules(__path__, prefix)]
|
||||
|
||||
# special handling for PyInstaller
|
||||
importers = map(pkgutil.get_importer, __path__)
|
||||
toc = set()
|
||||
for i in importers:
|
||||
if hasattr(i, 'toc'):
|
||||
toc |= i.toc
|
||||
|
||||
for elm in toc:
|
||||
if elm.startswith(prefix):
|
||||
module_names.append(elm)
|
||||
|
||||
for modname in module_names:
|
||||
for modName in iterNamespace(__name__, __path__):
|
||||
# loop through python files, extracting update number and function, and
|
||||
# adding it to a list
|
||||
modname_tail = modname.rsplit('.', 1)[-1]
|
||||
module = __import__(modname, fromlist=True)
|
||||
modname_tail = modName.rsplit('.', 1)[-1]
|
||||
m = re.match("^upgrade(?P<index>\d+)$", modname_tail)
|
||||
if not m:
|
||||
continue
|
||||
index = int(m.group("index"))
|
||||
appVersion = max(appVersion, index)
|
||||
module = __import__(modName, fromlist=True)
|
||||
upgrade = getattr(module, "upgrade", False)
|
||||
if upgrade:
|
||||
updates[index] = upgrade
|
||||
|
||||
@@ -33,9 +33,13 @@ def upgrade(saveddata_engine):
|
||||
try:
|
||||
saveddata_session.execute(commandFits_table.insert(),
|
||||
{"boosterID": value, "boostedID": boosted, "active": 1})
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
saveddata_session.commit()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
# Shouldn't fail unless you have updated database without the old fleet schema and manually modify the database version
|
||||
# If it does, simply fail. Fleet data migration isn't critically important here
|
||||
|
||||
@@ -4235,6 +4235,8 @@ def upgrade(saveddata_engine):
|
||||
|
||||
# And last but not least, delete the last subsystem
|
||||
saveddata_engine.execute("DELETE FROM modules WHERE ID = ?", (oldModules[4][0],))
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
# if something fails, fuck it, we tried. It'll default to the generic conversion below
|
||||
continue
|
||||
|
||||
16
eos/db/migrations/upgrade32.py
Normal file
16
eos/db/migrations/upgrade32.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Migration 32
|
||||
|
||||
- added speed, sig and radius columns to targetResists table
|
||||
"""
|
||||
|
||||
|
||||
import sqlalchemy
|
||||
|
||||
|
||||
def upgrade(saveddata_engine):
|
||||
for column in ('maxVelocity', 'signatureRadius', 'radius'):
|
||||
try:
|
||||
saveddata_engine.execute("SELECT {} FROM targetResists LIMIT 1;".format(column))
|
||||
except sqlalchemy.exc.DatabaseError:
|
||||
saveddata_engine.execute("ALTER TABLE targetResists ADD COLUMN {} FLOAT;".format(column))
|
||||
30
eos/db/migrations/upgrade33.py
Normal file
30
eos/db/migrations/upgrade33.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Migration 33
|
||||
|
||||
Allow use of floats in damage pattern values
|
||||
"""
|
||||
|
||||
tmpTable = """
|
||||
CREATE TABLE "damagePatternsTemp" (
|
||||
"ID" INTEGER NOT NULL,
|
||||
"name" VARCHAR,
|
||||
"emAmount" FLOAT,
|
||||
"thermalAmount" FLOAT,
|
||||
"kineticAmount" FLOAT,
|
||||
"explosiveAmount" FLOAT,
|
||||
"ownerID" INTEGER,
|
||||
"created" DATETIME,
|
||||
"modified" DATETIME,
|
||||
PRIMARY KEY ("ID"),
|
||||
FOREIGN KEY("ownerID") REFERENCES users ("ID")
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def upgrade(saveddata_engine):
|
||||
saveddata_engine.execute(tmpTable)
|
||||
saveddata_engine.execute(
|
||||
'INSERT INTO damagePatternsTemp (ID, name, emAmount, thermalAmount, kineticAmount, explosiveAmount, ownerID, created, modified) '
|
||||
'SELECT ID, name, emAmount, thermalAmount, kineticAmount, explosiveAmount, ownerID, created, modified FROM damagePatterns')
|
||||
saveddata_engine.execute('DROP TABLE damagePatterns')
|
||||
saveddata_engine.execute('ALTER TABLE damagePatternsTemp RENAME TO damagePatterns')
|
||||
25
eos/db/migrations/upgrade34.py
Normal file
25
eos/db/migrations/upgrade34.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Migration 34
|
||||
|
||||
- Adds projection range columns to projectable entities
|
||||
"""
|
||||
import sqlalchemy
|
||||
|
||||
|
||||
def upgrade(saveddata_engine):
|
||||
try:
|
||||
saveddata_engine.execute("SELECT projectionRange FROM projectedFits LIMIT 1")
|
||||
except sqlalchemy.exc.DatabaseError:
|
||||
saveddata_engine.execute("ALTER TABLE projectedFits ADD COLUMN projectionRange FLOAT;")
|
||||
try:
|
||||
saveddata_engine.execute("SELECT projectionRange FROM modules LIMIT 1")
|
||||
except sqlalchemy.exc.DatabaseError:
|
||||
saveddata_engine.execute("ALTER TABLE modules ADD COLUMN projectionRange FLOAT;")
|
||||
try:
|
||||
saveddata_engine.execute("SELECT projectionRange FROM drones LIMIT 1")
|
||||
except sqlalchemy.exc.DatabaseError:
|
||||
saveddata_engine.execute("ALTER TABLE drones ADD COLUMN projectionRange FLOAT;")
|
||||
try:
|
||||
saveddata_engine.execute("SELECT projectionRange FROM fighters LIMIT 1")
|
||||
except sqlalchemy.exc.DatabaseError:
|
||||
saveddata_engine.execute("ALTER TABLE fighters ADD COLUMN projectionRange FLOAT;")
|
||||
166
eos/db/migrations/upgrade35.py
Normal file
166
eos/db/migrations/upgrade35.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Migration 35
|
||||
|
||||
- Remove builtin damage patterns and target profiles from the database
|
||||
"""
|
||||
|
||||
import sqlalchemy
|
||||
|
||||
|
||||
dmgPatterns = (
|
||||
'Uniform',
|
||||
'[Bombs]Concussion Bomb',
|
||||
'[Bombs]Electron Bomb',
|
||||
'[Bombs]Scorch Bomb',
|
||||
'[Bombs]Shrapnel Bomb',
|
||||
'[Exotic Plasma]Baryon',
|
||||
'[Exotic Plasma]Meson',
|
||||
'[Exotic Plasma]Tetryon',
|
||||
'[Exotic Plasma][T2] Mystic',
|
||||
'[Exotic Plasma][T2] Occult',
|
||||
'[Frequency Crystals]Gamma',
|
||||
'[Frequency Crystals]Infrared',
|
||||
'[Frequency Crystals]Microwave',
|
||||
'[Frequency Crystals]Multifrequency',
|
||||
'[Frequency Crystals]Radio',
|
||||
'[Frequency Crystals]Standard',
|
||||
'[Frequency Crystals]Ultraviolet',
|
||||
'[Frequency Crystals]Xray',
|
||||
'[Frequency Crystals][T2] Aurora',
|
||||
'[Frequency Crystals][T2] Conflagration',
|
||||
'[Frequency Crystals][T2] Gleam',
|
||||
'[Frequency Crystals][T2] Scorch',
|
||||
'[Generic]EM',
|
||||
'[Generic]Explosive',
|
||||
'[Generic]Kinetic',
|
||||
'[Generic]Thermal',
|
||||
'[Hybrid Charges]Antimatter',
|
||||
'[Hybrid Charges]Iridium',
|
||||
'[Hybrid Charges]Iron',
|
||||
'[Hybrid Charges]Lead',
|
||||
'[Hybrid Charges]Plutonium',
|
||||
'[Hybrid Charges]Thorium',
|
||||
'[Hybrid Charges]Tungsten',
|
||||
'[Hybrid Charges]Uranium',
|
||||
'[Hybrid Charges][T2] Javelin',
|
||||
'[Hybrid Charges][T2] Null',
|
||||
'[Hybrid Charges][T2] Spike',
|
||||
'[Hybrid Charges][T2] Void',
|
||||
'[Missiles]Inferno',
|
||||
'[Missiles]Mjolnir',
|
||||
'[Missiles]Nova',
|
||||
'[Missiles]Scourge',
|
||||
'[Missiles][Structure) Standup Missile',
|
||||
'[Missiles][Structure] Standup Missile',
|
||||
'[NPC][Asteroid] Angel Cartel',
|
||||
'[NPC][Asteroid] Blood Raiders',
|
||||
'[NPC][Asteroid] Guristas',
|
||||
'[NPC][Asteroid] Rogue Drone',
|
||||
'[NPC][Asteroid] Sanshas Nation',
|
||||
'[NPC][Asteroid] Serpentis',
|
||||
'[NPC][Burner] Ashimmu (Blood Raiders)',
|
||||
'[NPC][Burner] Cruor (Blood Raiders)',
|
||||
'[NPC][Burner] Daredevil (Serpentis)',
|
||||
'[NPC][Burner] Dramiel (Angel)',
|
||||
'[NPC][Burner] Enyo',
|
||||
'[NPC][Burner] Hawk',
|
||||
'[NPC][Burner] Jaguar',
|
||||
'[NPC][Burner] Sentinel',
|
||||
'[NPC][Burner] Succubus (Sanshas Nation)',
|
||||
'[NPC][Burner] Talos',
|
||||
'[NPC][Burner] Vengeance',
|
||||
'[NPC][Burner] Worm (Guristas)',
|
||||
'[NPC][Deadspace] Angel Cartel',
|
||||
'[NPC][Deadspace] Blood Raiders',
|
||||
'[NPC][Deadspace] Guristas',
|
||||
'[NPC][Deadspace] Rogue Drone',
|
||||
'[NPC][Deadspace] Sanshas Nation',
|
||||
'[NPC][Deadspace] Serpentis',
|
||||
'[NPC][Mission] Amarr Empire',
|
||||
'[NPC][Mission] CONCORD',
|
||||
'[NPC][Mission] Caldari State',
|
||||
'[NPC][Mission] Gallente Federation',
|
||||
'[NPC][Mission] Khanid',
|
||||
'[NPC][Mission] Minmatar Republic',
|
||||
'[NPC][Mission] Mordus Legion',
|
||||
'[NPC][Mission] Thukker',
|
||||
'[NPC][Other] Sansha Incursion',
|
||||
'[NPC][Other] Sleepers',
|
||||
'[Projectile Ammo]Carbonized Lead',
|
||||
'[Projectile Ammo]Depleted Uranium',
|
||||
'[Projectile Ammo]EMP',
|
||||
'[Projectile Ammo]Fusion',
|
||||
'[Projectile Ammo]Nuclear',
|
||||
'[Projectile Ammo]Phased Plasma',
|
||||
'[Projectile Ammo]Proton',
|
||||
'[Projectile Ammo]Titanium Sabot',
|
||||
'[Projectile Ammo][T2] Barrage',
|
||||
'[Projectile Ammo][T2] Hail',
|
||||
'[Projectile Ammo][T2] Quake',
|
||||
'[Projectile Ammo][T2] Tremor')
|
||||
|
||||
tgtProfiles = (
|
||||
'Uniform (25%)',
|
||||
'Uniform (50%)',
|
||||
'Uniform (75%)',
|
||||
'Uniform (90%)',
|
||||
'[NPC][Asteroid] Angel Cartel',
|
||||
'[NPC][Asteroid] Blood Raiders',
|
||||
'[NPC][Asteroid] Guristas',
|
||||
'[NPC][Asteroid] Rogue Drones',
|
||||
'[NPC][Asteroid] Sanshas Nation',
|
||||
'[NPC][Asteroid] Serpentis',
|
||||
'[NPC][Burner] Ashimmu (Blood Raiders)',
|
||||
'[NPC][Burner] Cruor (Blood Raiders)',
|
||||
'[NPC][Burner] Daredevil (Serpentis)',
|
||||
'[NPC][Burner] Dramiel (Angel)',
|
||||
'[NPC][Burner] Enyo',
|
||||
'[NPC][Burner] Hawk',
|
||||
'[NPC][Burner] Jaguar',
|
||||
'[NPC][Burner] Sentinel',
|
||||
'[NPC][Burner] Succubus (Sanshas Nation)',
|
||||
'[NPC][Burner] Talos',
|
||||
'[NPC][Burner] Vengeance',
|
||||
'[NPC][Burner] Worm (Guristas)',
|
||||
'[NPC][Deadspace] Angel Cartel',
|
||||
'[NPC][Deadspace] Blood Raiders',
|
||||
'[NPC][Deadspace] Guristas',
|
||||
'[NPC][Deadspace] Rogue Drones',
|
||||
'[NPC][Deadspace] Sanshas Nation',
|
||||
'[NPC][Deadspace] Serpentis',
|
||||
'[NPC][Mission] Amarr Empire',
|
||||
'[NPC][Mission] CONCORD',
|
||||
'[NPC][Mission] Caldari State',
|
||||
'[NPC][Mission] Gallente Federation',
|
||||
'[NPC][Mission] Khanid',
|
||||
'[NPC][Mission] Minmatar Republic',
|
||||
'[NPC][Mission] Mordus Legion',
|
||||
'[NPC][Other] Sansha Incursion',
|
||||
'[NPC][Other] Sleeper',
|
||||
'[T1 Resist]Armor',
|
||||
'[T1 Resist]Armor (+T2 DCU)',
|
||||
'[T1 Resist]Hull',
|
||||
'[T1 Resist]Hull (+T2 DCU)',
|
||||
'[T1 Resist]Shield',
|
||||
'[T1 Resist]Shield (+T2 DCU)',
|
||||
'[T2 Resist]Amarr (Armor)',
|
||||
'[T2 Resist]Amarr (Shield)',
|
||||
'[T2 Resist]Caldari (Armor)',
|
||||
'[T2 Resist]Caldari (Shield)',
|
||||
'[T2 Resist]Gallente (Armor)',
|
||||
'[T2 Resist]Gallente (Shield)',
|
||||
'[T2 Resist]Minmatar (Armor)',
|
||||
'[T2 Resist]Minmatar (Shield)')
|
||||
|
||||
|
||||
def upgrade(saveddata_engine):
|
||||
saveddata_engine.execute('DELETE FROM damagePatterns WHERE name in ({});'.format(', '.join('\'{}\''.format(n) for n in dmgPatterns)))
|
||||
saveddata_engine.execute('DELETE FROM targetResists WHERE name in ({});'.format(', '.join('\'{}\''.format(n) for n in tgtProfiles)))
|
||||
try:
|
||||
saveddata_engine.execute("SELECT builtinDamagePatternID FROM fits LIMIT 1")
|
||||
except sqlalchemy.exc.DatabaseError:
|
||||
saveddata_engine.execute("ALTER TABLE fits ADD COLUMN builtinDamagePatternID INT;")
|
||||
try:
|
||||
saveddata_engine.execute("SELECT builtinTargetResistsID FROM fits LIMIT 1")
|
||||
except sqlalchemy.exc.DatabaseError:
|
||||
saveddata_engine.execute("ALTER TABLE fits ADD COLUMN builtinTargetResistsID INT;")
|
||||
84
eos/db/migrations/upgrade36.py
Normal file
84
eos/db/migrations/upgrade36.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Migration 36
|
||||
|
||||
- Shield Booster, Armor Repairer and Capacitor Transfer tiericide
|
||||
"""
|
||||
|
||||
CONVERSIONS = {
|
||||
6441: ( # Small Clarity Ward Enduring Shield Booster
|
||||
6443, # Small Converse Deflection Catalyzer
|
||||
),
|
||||
6437: ( # Small C5-L Compact Shield Booster
|
||||
6439, # Small Neutron Saturation Injector I
|
||||
),
|
||||
10868: ( # Medium Clarity Ward Enduring Shield Booster
|
||||
10870, # Medium Converse Deflection Catalyzer
|
||||
),
|
||||
10872: ( # Medium C5-L Compact Shield Booster
|
||||
10866, # Medium Neutron Saturation Injector I
|
||||
),
|
||||
10876: ( # Large Clarity Ward Enduring Shield Booster
|
||||
10878, # Large Converse Deflection Catalyzer
|
||||
),
|
||||
10880: ( # Large C5-L Compact Shield Booster
|
||||
10874, # Large Neutron Saturation Injector I
|
||||
),
|
||||
10884: ( # X-Large Clarity Ward Enduring Shield Booster
|
||||
10886, # X-Large Converse Deflection Catalyzer
|
||||
),
|
||||
10888: ( # X-Large C5-L Compact Shield Booster
|
||||
10882, # X-Large Neutron Saturation Injector I
|
||||
),
|
||||
4533: ( # Small ACM Compact Armor Repairer
|
||||
4531, # Small Inefficient Armor Repair Unit
|
||||
),
|
||||
4529: ( # Small I-a Enduring Armor Repairer
|
||||
4535, # Small Automated Carapace Restoration
|
||||
),
|
||||
4573: ( # Medium ACM Compact Armor Repairer
|
||||
4571, # Medium Inefficient Armor Repair Unit
|
||||
),
|
||||
4569: ( # Medium I-a Enduring Armor Repairer
|
||||
4575, # Medium Automated Carapace Restoration
|
||||
),
|
||||
22889: ( # 'Meditation' Medium Armor Repairer I
|
||||
4579, # Medium Nano Armor Repair Unit I
|
||||
),
|
||||
4613: ( # Large ACM Compact Armor Repairer
|
||||
4611, # Large Inefficient Armor Repair Unit
|
||||
),
|
||||
4609: ( # Large I-a Enduring Armor Repairer
|
||||
4615, # Large Automated Carapace Restoration
|
||||
),
|
||||
22891: ( # 'Protest' Large Armor Repairer I
|
||||
4621, # Large 'Reprieve' Vestment Reconstructer I
|
||||
),
|
||||
5093: ( # Small Radiative Scoped Remote Capacitor Transmitter
|
||||
5087, # Small Partial E95a Remote Capacitor Transmitter
|
||||
),
|
||||
5091: ( # Small Inductive Compact Remote Capacitor Transmitter
|
||||
5089, # Small Murky Remote Capacitor Transmitter
|
||||
),
|
||||
16489: ( # Medium Radiative Scoped Remote Capacitor Transmitter
|
||||
16493, # Medium Partial E95b Remote Capacitor Transmitter
|
||||
),
|
||||
16495: ( # Medium Inductive Compact Remote Capacitor Transmitter
|
||||
16491, # Medium Murky Remote Capacitor Transmitter
|
||||
),
|
||||
16481: ( # Large Radiative Scoped Remote Capacitor Transmitter
|
||||
16485, # Large Partial E95c Remote Capacitor Transmitter
|
||||
),
|
||||
16487: ( # Large Inductive Compact Remote Capacitor Transmitter
|
||||
16483, # Large Murky Remote Capacitor Transmitter
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def upgrade(saveddata_engine):
|
||||
# Convert modules
|
||||
for replacement_item, list in CONVERSIONS.items():
|
||||
for retired_item in list:
|
||||
saveddata_engine.execute('UPDATE "modules" SET "itemID" = ? WHERE "itemID" = ?',
|
||||
(replacement_item, retired_item))
|
||||
saveddata_engine.execute('UPDATE "cargo" SET "itemID" = ? WHERE "itemID" = ?',
|
||||
(replacement_item, retired_item))
|
||||
44
eos/db/migrations/upgrade37.py
Normal file
44
eos/db/migrations/upgrade37.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Migration 37
|
||||
|
||||
- Capacitor Booster tiericide
|
||||
"""
|
||||
|
||||
CONVERSIONS = {
|
||||
4959: ( # 'Seed' Micro Capacitor Booster I
|
||||
4957, # Micro Brief Capacitor Overcharge I
|
||||
4961, # Micro Tapered Capacitor Infusion I
|
||||
4955, # Micro F-RX Prototype Capacitor Boost
|
||||
3556, # Micro Capacitor Booster I
|
||||
3558, # Micro Capacitor Booster II
|
||||
15774, # Ammatar Navy Micro Capacitor Booster
|
||||
14180, # Dark Blood Micro Capacitor Booster
|
||||
14182, # True Sansha Micro Capacitor Booster
|
||||
15782, # Imperial Navy Micro Capacitor Booster
|
||||
),
|
||||
5011: ( # Small F-RX Compact Capacitor Booster
|
||||
5009, # Small Brief Capacitor Overcharge I
|
||||
5013, # Small Tapered Capacitor Infusion I
|
||||
5007, # Small F-RX Prototype Capacitor Boost
|
||||
),
|
||||
4833: ( # Medium F-RX Compact Capacitor Booster
|
||||
4831, # Medium Brief Capacitor Overcharge I
|
||||
4835, # Medium Tapered Capacitor Infusion I
|
||||
4829, # Medium F-RX Prototype Capacitor Boost
|
||||
),
|
||||
5051: ( # Heavy F-RX Compact Capacitor Booster
|
||||
5049, # Heavy Brief Capacitor Overcharge I
|
||||
5053, # Heavy Tapered Capacitor Infusion I
|
||||
5047, # Heavy F-RX Prototype Capacitor Boost
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def upgrade(saveddata_engine):
|
||||
# Convert modules
|
||||
for replacement_item, list in CONVERSIONS.items():
|
||||
for retired_item in list:
|
||||
saveddata_engine.execute('UPDATE "modules" SET "itemID" = ? WHERE "itemID" = ?',
|
||||
(replacement_item, retired_item))
|
||||
saveddata_engine.execute('UPDATE "cargo" SET "itemID" = ? WHERE "itemID" = ?',
|
||||
(replacement_item, retired_item))
|
||||
@@ -11,8 +11,7 @@ __all__ = [
|
||||
"implant",
|
||||
"damagePattern",
|
||||
"miscData",
|
||||
"targetResists",
|
||||
"targetProfile",
|
||||
"override",
|
||||
"implantSet",
|
||||
"loadDefaultDatabaseValues"
|
||||
"implantSet"
|
||||
]
|
||||
|
||||
@@ -17,23 +17,27 @@
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
from sqlalchemy import Table, Column, Integer, ForeignKey, String, DateTime
|
||||
from sqlalchemy import Table, Column, Integer, Float, ForeignKey, String, DateTime
|
||||
from sqlalchemy.orm import mapper
|
||||
import datetime
|
||||
|
||||
from eos.db import saveddata_meta
|
||||
from eos.saveddata.damagePattern import DamagePattern
|
||||
|
||||
damagePatterns_table = Table("damagePatterns", saveddata_meta,
|
||||
Column("ID", Integer, primary_key=True),
|
||||
Column("name", String),
|
||||
Column("emAmount", Integer),
|
||||
Column("thermalAmount", Integer),
|
||||
Column("kineticAmount", Integer),
|
||||
Column("explosiveAmount", Integer),
|
||||
Column("ownerID", ForeignKey("users.ID"), nullable=True),
|
||||
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
|
||||
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)
|
||||
)
|
||||
damagePatterns_table = Table(
|
||||
'damagePatterns',
|
||||
saveddata_meta,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('name', String),
|
||||
Column('emAmount', Float),
|
||||
Column('thermalAmount', Float),
|
||||
Column('kineticAmount', Float),
|
||||
Column('explosiveAmount', Float),
|
||||
Column('ownerID', ForeignKey('users.ID'), nullable=True),
|
||||
Column('created', DateTime, nullable=True, default=datetime.datetime.now),
|
||||
Column('modified', DateTime, nullable=True, onupdate=datetime.datetime.now))
|
||||
|
||||
mapper(DamagePattern, damagePatterns_table)
|
||||
mapper(
|
||||
DamagePattern,
|
||||
damagePatterns_table,
|
||||
properties={'rawName': damagePatterns_table.c.name})
|
||||
|
||||
@@ -23,7 +23,7 @@ from logbook import Logger
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class DatabaseCleanup(object):
|
||||
class DatabaseCleanup:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, DateTime
|
||||
from sqlalchemy import Table, Column, Integer, Float, ForeignKey, Boolean, DateTime
|
||||
from sqlalchemy.orm import mapper, relation
|
||||
import datetime
|
||||
|
||||
@@ -33,7 +33,8 @@ drones_table = Table("drones", saveddata_meta,
|
||||
Column("amountActive", Integer, nullable=False),
|
||||
Column("projected", Boolean, default=False),
|
||||
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
|
||||
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)
|
||||
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
|
||||
Column("projectionRange", Float, nullable=True)
|
||||
)
|
||||
|
||||
mapper(Drone, drones_table,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, DateTime
|
||||
from sqlalchemy import Table, Column, Integer, Float, ForeignKey, Boolean, DateTime
|
||||
from sqlalchemy.orm import mapper, relation
|
||||
import datetime
|
||||
|
||||
@@ -34,7 +34,8 @@ fighters_table = Table("fighters", saveddata_meta,
|
||||
Column("amount", Integer, nullable=False),
|
||||
Column("projected", Boolean, default=False),
|
||||
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
|
||||
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)
|
||||
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
|
||||
Column("projectionRange", Float, nullable=True),
|
||||
)
|
||||
|
||||
fighter_abilities_table = Table("fightersAbilities", saveddata_meta,
|
||||
@@ -46,6 +47,7 @@ fighter_abilities_table = Table("fightersAbilities", saveddata_meta,
|
||||
mapper(Fighter, fighters_table,
|
||||
properties={
|
||||
"owner" : relation(Fit),
|
||||
"_amount" : fighters_table.c.amount,
|
||||
"_Fighter__abilities": relation(
|
||||
FighterAbility,
|
||||
backref="fighter",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import datetime
|
||||
|
||||
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Table
|
||||
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, Float, String, Table
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.orm import mapper, reconstructor, relation, relationship
|
||||
from sqlalchemy.orm.collections import attribute_mapped_collection
|
||||
@@ -41,7 +41,7 @@ from eos.saveddata.fighter import Fighter
|
||||
from eos.saveddata.fit import Fit as es_Fit
|
||||
from eos.saveddata.implant import Implant
|
||||
from eos.saveddata.module import Module
|
||||
from eos.saveddata.targetResists import TargetResists
|
||||
from eos.saveddata.targetProfile import TargetProfile
|
||||
from eos.saveddata.user import User
|
||||
|
||||
|
||||
@@ -53,8 +53,10 @@ fits_table = Table("fits", saveddata_meta,
|
||||
Column("timestamp", Integer, nullable=False),
|
||||
Column("characterID", ForeignKey("characters.ID"), nullable=True),
|
||||
Column("damagePatternID", ForeignKey("damagePatterns.ID"), nullable=True),
|
||||
Column("builtinDamagePatternID", Integer, nullable=True),
|
||||
Column("booster", Boolean, nullable=False, index=True, default=0),
|
||||
Column("targetResistsID", ForeignKey("targetResists.ID"), nullable=True),
|
||||
Column("builtinTargetResistsID", Integer, nullable=True),
|
||||
Column("modeID", Integer, nullable=True),
|
||||
Column("implantLocation", Integer, nullable=False),
|
||||
Column("notes", String, nullable=True),
|
||||
@@ -70,7 +72,8 @@ projectedFits_table = Table("projectedFits", saveddata_meta,
|
||||
Column("amount", Integer, nullable=False, default=1),
|
||||
Column("active", Boolean, nullable=False, default=1),
|
||||
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
|
||||
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)
|
||||
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
|
||||
Column("projectionRange", Float, nullable=True),
|
||||
)
|
||||
|
||||
commandFits_table = Table("commandFits", saveddata_meta,
|
||||
@@ -82,7 +85,8 @@ commandFits_table = Table("commandFits", saveddata_meta,
|
||||
)
|
||||
|
||||
|
||||
class ProjectedFit(object):
|
||||
class ProjectedFit:
|
||||
|
||||
def __init__(self, sourceID, source_fit, amount=1, active=True):
|
||||
self.sourceID = sourceID
|
||||
self.source_fit = source_fit
|
||||
@@ -113,7 +117,7 @@ class ProjectedFit(object):
|
||||
)
|
||||
|
||||
|
||||
class CommandFit(object):
|
||||
class CommandFit:
|
||||
def __init__(self, boosterID, booster_fit, active=True):
|
||||
self.boosterID = boosterID
|
||||
self.booster_fit = booster_fit
|
||||
@@ -231,8 +235,10 @@ mapper(es_Fit, fits_table,
|
||||
"_Fit__character": relation(
|
||||
Character,
|
||||
backref="fits"),
|
||||
"_Fit__damagePattern": relation(DamagePattern),
|
||||
"_Fit__targetResists": relation(TargetResists),
|
||||
"_Fit__userDamagePattern": relation(DamagePattern),
|
||||
"_Fit__builtinDamagePatternID": fits_table.c.builtinDamagePatternID,
|
||||
"_Fit__userTargetProfile": relation(TargetProfile),
|
||||
"_Fit__builtinTargetProfileID": fits_table.c.builtinTargetResistsID,
|
||||
"projectedOnto": projectedFitSourceRel,
|
||||
"victimOf": relationship(
|
||||
ProjectedFit,
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
# ===============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
import eos.db
|
||||
from eos.saveddata.damagePattern import DamagePattern as es_DamagePattern
|
||||
from eos.saveddata.targetResists import TargetResists as es_TargetResists
|
||||
|
||||
|
||||
class ImportError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DefaultDatabaseValues(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
instance = None
|
||||
|
||||
@classmethod
|
||||
def importDamageProfileDefaults(cls):
|
||||
damageProfileList = [["Uniform", "25", "25", "25", "25"], ["[Generic]EM", "100", "0", "0", "0"],
|
||||
["[Generic]Thermal", "0", "100", "0", "0"], ["[Generic]Kinetic", "0", "0", "100", "0"],
|
||||
["[Generic]Explosive", "0", "0", "0", "100"],
|
||||
["[NPC][Asteroid] Blood Raiders", "5067", "4214", "0", "0"],
|
||||
["[Bombs]Electron Bomb", "6400", "0", "0", "0"],
|
||||
["[Bombs]Scorch Bomb", "0", "6400", "0", "0"],
|
||||
["[Bombs]Concussion Bomb", "0", "0", "6400", "0"],
|
||||
["[Bombs]Shrapnel Bomb", "0", "0", "0", "6400"],
|
||||
["[Frequency Crystals][T2] Conflagration", "61.6", "61.6", "0", "0"],
|
||||
["[Frequency Crystals][T2] Scorch", "72", "16", "0", "0"],
|
||||
["[Frequency Crystals][T2] Gleam", "56", "56", "0", "0"],
|
||||
["[Frequency Crystals][T2] Aurora", "40", "24", "0", "0"],
|
||||
["[Frequency Crystals]Multifrequency", "61.6", "44", "0", "0"],
|
||||
["[Frequency Crystals]Gamma", "61.6", "35.2", "0", "0"],
|
||||
["[Frequency Crystals]Xray", "52.8", "35.2", "0", "0"],
|
||||
["[Frequency Crystals]Ultraviolet", "52.8", "26.4", "0", "0"],
|
||||
["[Frequency Crystals]Standard", "44", "26.4", "0", "0"],
|
||||
["[Frequency Crystals]Infrared", "44", "17.6", "0", "0"],
|
||||
["[Frequency Crystals]Microwave", "35.2", "17.6", "0", "0"],
|
||||
["[Frequency Crystals]Radio", "44", "0", "0", "0"],
|
||||
["[Hybrid Charges][T2] Void", "0", "61.6", "61.6", "0"],
|
||||
["[Hybrid Charges][T2] Null", "0", "48", "40", "0"],
|
||||
["[Hybrid Charges][T2] Javelin", "0", "64", "48", "0"],
|
||||
["[Hybrid Charges][T2] Spike", "0", "32", "32", "0"],
|
||||
["[Hybrid Charges]Antimatter", "0", "48", "67.2", "0"],
|
||||
["[Hybrid Charges]Plutonium", "0", "48", "57.6", "0"],
|
||||
["[Hybrid Charges]Uranium", "0", "38.4", "57.6", "0"],
|
||||
["[Hybrid Charges]Thorium", "0", "38.4", "48", "0"],
|
||||
["[Hybrid Charges]Lead", "0", "28.8", "48", "0"],
|
||||
["[Hybrid Charges]Iridium", "0", "28.8", "38.4", "0"],
|
||||
["[Hybrid Charges]Tungsten", "0", "19.2", "38.4", "0"],
|
||||
["[Hybrid Charges]Iron", "0", "19.2", "28.8", "0"],
|
||||
["[Missiles]Mjolnir", "100", "0", "0", "0"],
|
||||
["[Missiles]Inferno", "0", "100", "0", "0"],
|
||||
["[Missiles]Scourge", "0", "0", "100", "0"],
|
||||
["[Missiles]Nova", "0", "0", "0", "100"],
|
||||
["[Missiles][Structure] Standup Missile", "100", "100", "100", "100"],
|
||||
["[Projectile Ammo][T2] Hail", "0", "0", "26.4", "96.8"],
|
||||
["[Projectile Ammo][T2] Barrage", "0", "0", "40", "48"],
|
||||
["[Projectile Ammo][T2] Quake", "0", "0", "40", "72"],
|
||||
["[Projectile Ammo][T2] Tremor", "0", "0", "24", "40"],
|
||||
["[Projectile Ammo]EMP", "79.2", "0", "8.8", "17.6"],
|
||||
["[Projectile Ammo]Phased Plasma", "0", "88", "17.6", "0"],
|
||||
["[Projectile Ammo]Fusion", "0", "0", "17.6", "88"],
|
||||
["[Projectile Ammo]Depleted Uranium", "0", "26.4", "17.6", "26.4"],
|
||||
["[Projectile Ammo]Titanium Sabot", "0", "0", "52.8", "176"],
|
||||
["[Projectile Ammo]Proton", "26.4", "0", "17.6", "0"],
|
||||
["[Projectile Ammo]Carbonized Lead", "0", "0", "35.2", "8.8"],
|
||||
["[Projectile Ammo]Nuclear", "0", "0", "8.8", "35.2"],
|
||||
# Different sizes of plasma do different damage, the values here are
|
||||
# average of proportions across sizes
|
||||
["[Exotic Plasma][T2] Occult", "0", "55863", "0", "44137"],
|
||||
["[Exotic Plasma][T2] Mystic", "0", "66319", "0", "33681"],
|
||||
["[Exotic Plasma]Tetryon", "0", "69208", "0", "30792"],
|
||||
["[Exotic Plasma]Baryon", "0", "59737", "0", "40263"],
|
||||
["[Exotic Plasma]Meson", "0", "60519", "0", "39481"],
|
||||
["[NPC][Burner] Cruor (Blood Raiders)", "90", "90", "0", "0"],
|
||||
["[NPC][Burner] Dramiel (Angel)", "55", "0", "20", "96"],
|
||||
["[NPC][Burner] Daredevil (Serpentis)", "0", "110", "154", "0"],
|
||||
["[NPC][Burner] Succubus (Sanshas Nation)", "135", "30", "0", "0"],
|
||||
["[NPC][Burner] Worm (Guristas)", "0", "0", "228", "0"],
|
||||
["[NPC][Burner] Enyo", "0", "147", "147", "0"],
|
||||
["[NPC][Burner] Hawk", "0", "0", "247", "0"],
|
||||
["[NPC][Burner] Jaguar", "36", "0", "50", "182"],
|
||||
["[NPC][Burner] Vengeance", "232", "0", "0", "0"],
|
||||
["[NPC][Burner] Ashimmu (Blood Raiders)", "260", "100", "0", "0"],
|
||||
["[NPC][Burner] Talos", "0", "413", "413", "0"],
|
||||
["[NPC][Burner] Sentinel", "0", "75", "0", "90"],
|
||||
["[NPC][Asteroid] Angel Cartel", "1838", "562", "2215", "3838"],
|
||||
["[NPC][Deadspace] Angel Cartel", "369", "533", "1395", "3302"],
|
||||
["[NPC][Deadspace] Blood Raiders", "6040", "5052", "10", "15"],
|
||||
["[NPC][Asteroid] Guristas", "0", "1828", "7413", "0"],
|
||||
["[NPC][Deadspace] Guristas", "0", "1531", "9680", "0"],
|
||||
["[NPC][Asteroid] Rogue Drone", "394", "666", "1090", "1687"],
|
||||
["[NPC][Deadspace] Rogue Drone", "276", "1071", "1069", "871"],
|
||||
["[NPC][Asteroid] Sanshas Nation", "5586", "4112", "0", "0"],
|
||||
["[NPC][Deadspace] Sanshas Nation", "3009", "2237", "0", "0"],
|
||||
["[NPC][Asteroid] Serpentis", "0", "5373", "4813", "0"],
|
||||
["[NPC][Deadspace] Serpentis", "0", "3110", "1929", "0"],
|
||||
["[NPC][Mission] Amarr Empire", "4464", "3546", "97", "0"],
|
||||
["[NPC][Mission] Caldari State", "0", "2139", "4867", "0"],
|
||||
["[NPC][Mission] CONCORD", "336", "134", "212", "412"],
|
||||
["[NPC][Mission] Gallente Federation", "9", "3712", "2758", "0"],
|
||||
["[NPC][Mission] Khanid", "612", "483", "43", "6"],
|
||||
["[NPC][Mission] Minmatar Republic", "1024", "388", "1655", "4285"],
|
||||
["[NPC][Mission] Mordus Legion", "25", "262", "625", "0"],
|
||||
["[NPC][Mission] Thukker", "0", "52", "10", "79"],
|
||||
["[NPC][Other] Sleepers", "1472", "1472", "1384", "1384"],
|
||||
["[NPC][Other] Sansha Incursion", "1682", "1347", "3678", "3678"]]
|
||||
|
||||
for damageProfileRow in damageProfileList:
|
||||
name, em, therm, kin, exp = damageProfileRow
|
||||
damageProfile = eos.db.getDamagePattern(name)
|
||||
if damageProfile is None:
|
||||
damageProfile = es_DamagePattern(em, therm, kin, exp)
|
||||
damageProfile.name = name
|
||||
eos.db.save(damageProfile)
|
||||
|
||||
@classmethod
|
||||
def importResistProfileDefaults(cls):
|
||||
targetResistProfileList = [["Uniform (25%)", "0.25", "0.25", "0.25", "0.25"],
|
||||
["Uniform (50%)", "0.50", "0.50", "0.50", "0.50"],
|
||||
["Uniform (75%)", "0.75", "0.75", "0.75", "0.75"],
|
||||
["Uniform (90%)", "0.90", "0.90", "0.90", "0.90"],
|
||||
["[T1 Resist]Shield", "0.0", "0.20", "0.40", "0.50"],
|
||||
["[T1 Resist]Armor", "0.50", "0.45", "0.25", "0.10"],
|
||||
["[T1 Resist]Hull", "0.33", "0.33", "0.33", "0.33"],
|
||||
["[T1 Resist]Shield (+T2 DCU)", "0.125", "0.30", "0.475", "0.562"],
|
||||
["[T1 Resist]Armor (+T2 DCU)", "0.575", "0.532", "0.363", "0.235"],
|
||||
["[T1 Resist]Hull (+T2 DCU)", "0.598", "0.598", "0.598", "0.598"],
|
||||
["[T2 Resist]Amarr (Shield)", "0.0", "0.20", "0.70", "0.875"],
|
||||
["[T2 Resist]Amarr (Armor)", "0.50", "0.35", "0.625", "0.80"],
|
||||
["[T2 Resist]Caldari (Shield)", "0.20", "0.84", "0.76", "0.60"],
|
||||
["[T2 Resist]Caldari (Armor)", "0.50", "0.8625", "0.625", "0.10"],
|
||||
["[T2 Resist]Gallente (Shield)", "0.0", "0.60", "0.85", "0.50"],
|
||||
["[T2 Resist]Gallente (Armor)", "0.50", "0.675", "0.8375", "0.10"],
|
||||
["[T2 Resist]Minmatar (Shield)", "0.75", "0.60", "0.40", "0.50"],
|
||||
["[T2 Resist]Minmatar (Armor)", "0.90", "0.675", "0.25", "0.10"],
|
||||
["[NPC][Asteroid] Angel Cartel", "0.54", "0.42", "0.37", "0.32"],
|
||||
["[NPC][Asteroid] Blood Raiders", "0.34", "0.39", "0.45", "0.52"],
|
||||
["[NPC][Asteroid] Guristas", "0.55", "0.35", "0.3", "0.48"],
|
||||
["[NPC][Asteroid] Rogue Drones", "0.35", "0.38", "0.44", "0.49"],
|
||||
["[NPC][Asteroid] Sanshas Nation", "0.35", "0.4", "0.47", "0.53"],
|
||||
["[NPC][Asteroid] Serpentis", "0.49", "0.38", "0.29", "0.51"],
|
||||
["[NPC][Deadspace] Angel Cartel", "0.59", "0.48", "0.4", "0.32"],
|
||||
["[NPC][Deadspace] Blood Raiders", "0.31", "0.39", "0.47", "0.56"],
|
||||
["[NPC][Deadspace] Guristas", "0.57", "0.39", "0.31", "0.5"],
|
||||
["[NPC][Deadspace] Rogue Drones", "0.42", "0.42", "0.47", "0.49"],
|
||||
["[NPC][Deadspace] Sanshas Nation", "0.31", "0.39", "0.47", "0.56"],
|
||||
["[NPC][Deadspace] Serpentis", "0.49", "0.38", "0.29", "0.56"],
|
||||
["[NPC][Mission] Amarr Empire", "0.34", "0.38", "0.42", "0.46"],
|
||||
["[NPC][Mission] Caldari State", "0.51", "0.38", "0.3", "0.51"],
|
||||
["[NPC][Mission] CONCORD", "0.47", "0.46", "0.47", "0.47"],
|
||||
["[NPC][Mission] Gallente Federation", "0.51", "0.38", "0.31", "0.52"],
|
||||
["[NPC][Mission] Khanid", "0.51", "0.42", "0.36", "0.4"],
|
||||
["[NPC][Mission] Minmatar Republic", "0.51", "0.46", "0.41", "0.35"],
|
||||
["[NPC][Mission] Mordus Legion", "0.32", "0.48", "0.4", "0.62"],
|
||||
["[NPC][Other] Sleeper", "0.61", "0.61", "0.61", "0.61"],
|
||||
["[NPC][Other] Sansha Incursion", "0.65", "0.63", "0.64", "0.65"],
|
||||
["[NPC][Burner] Cruor (Blood Raiders)", "0.8", "0.73", "0.69", "0.67"],
|
||||
["[NPC][Burner] Dramiel (Angel)", "0.35", "0.48", "0.61", "0.68"],
|
||||
["[NPC][Burner] Daredevil (Serpentis)", "0.69", "0.59", "0.59", "0.43"],
|
||||
["[NPC][Burner] Succubus (Sanshas Nation)", "0.35", "0.48", "0.61", "0.68"],
|
||||
["[NPC][Burner] Worm (Guristas)", "0.48", "0.58", "0.69", "0.74"],
|
||||
["[NPC][Burner] Enyo", "0.58", "0.72", "0.86", "0.24"],
|
||||
["[NPC][Burner] Hawk", "0.3", "0.86", "0.79", "0.65"],
|
||||
["[NPC][Burner] Jaguar", "0.78", "0.65", "0.48", "0.56"],
|
||||
["[NPC][Burner] Vengeance", "0.66", "0.56", "0.75", "0.86"],
|
||||
["[NPC][Burner] Ashimmu (Blood Raiders)", "0.8", "0.76", "0.68", "0.7"],
|
||||
["[NPC][Burner] Talos", "0.68", "0.59", "0.59", "0.43"],
|
||||
["[NPC][Burner] Sentinel", "0.58", "0.45", "0.52", "0.66"]]
|
||||
|
||||
for targetResistProfileRow in targetResistProfileList:
|
||||
name, em, therm, kin, exp = targetResistProfileRow
|
||||
resistsProfile = eos.db.eos.db.getTargetResists(name)
|
||||
if resistsProfile is None:
|
||||
resistsProfile = es_TargetResists(em, therm, kin, exp)
|
||||
resistsProfile.name = name
|
||||
eos.db.save(resistsProfile)
|
||||
|
||||
@classmethod
|
||||
def importRequiredDefaults(cls):
|
||||
damageProfileList = [["Uniform", "25", "25", "25", "25"]]
|
||||
|
||||
for damageProfileRow in damageProfileList:
|
||||
name, em, therm, kin, exp = damageProfileRow
|
||||
damageProfile = eos.db.getDamagePattern(name)
|
||||
if damageProfile is None:
|
||||
damageProfile = es_DamagePattern(em, therm, kin, exp)
|
||||
damageProfile.name = name
|
||||
eos.db.save(damageProfile)
|
||||
@@ -42,6 +42,7 @@ modules_table = Table("modules", saveddata_meta,
|
||||
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
|
||||
Column("spoolType", Integer, nullable=True),
|
||||
Column("spoolAmount", Float, nullable=True),
|
||||
Column("projectionRange", Float, nullable=True),
|
||||
CheckConstraint('("dummySlot" = NULL OR "itemID" = NULL) AND "dummySlot" != "itemID"'))
|
||||
|
||||
mapper(Module, modules_table,
|
||||
|
||||
@@ -24,16 +24,16 @@ from sqlalchemy import desc, select
|
||||
from sqlalchemy import func
|
||||
|
||||
from eos.db import saveddata_session, sd_lock
|
||||
from eos.db.saveddata.fit import projectedFits_table
|
||||
from eos.db.saveddata.fit import fits_table, projectedFits_table
|
||||
from eos.db.util import processEager, processWhere
|
||||
from eos.saveddata.price import Price
|
||||
from eos.saveddata.user import User
|
||||
from eos.saveddata.ssocharacter import SsoCharacter
|
||||
from eos.saveddata.damagePattern import DamagePattern
|
||||
from eos.saveddata.targetResists import TargetResists
|
||||
from eos.saveddata.targetProfile import TargetProfile
|
||||
from eos.saveddata.character import Character
|
||||
from eos.saveddata.implantSet import ImplantSet
|
||||
from eos.saveddata.fit import Fit
|
||||
from eos.saveddata.fit import Fit, FitLite
|
||||
from eos.saveddata.module import Module
|
||||
from eos.saveddata.miscData import MiscData
|
||||
from eos.saveddata.override import Override
|
||||
@@ -326,6 +326,17 @@ def getFitList(eager=None):
|
||||
return fits
|
||||
|
||||
|
||||
def getFitListLite():
|
||||
with sd_lock:
|
||||
stmt = select([fits_table.c.ID, fits_table.c.name, fits_table.c.shipID])
|
||||
data = eos.db.saveddata_session.execute(stmt).fetchall()
|
||||
fits = []
|
||||
for fitID, fitName, shipID in data:
|
||||
fit = FitLite(id=fitID, name=fitName, shipID=shipID)
|
||||
fits.append(fit)
|
||||
return fits
|
||||
|
||||
|
||||
@cachedQuery(Price, 1, "typeID")
|
||||
def getPrice(typeID):
|
||||
if isinstance(typeID, int):
|
||||
@@ -366,16 +377,16 @@ def clearDamagePatterns():
|
||||
return deleted_rows
|
||||
|
||||
|
||||
def getTargetResistsList(eager=None):
|
||||
def getTargetProfileList(eager=None):
|
||||
eager = processEager(eager)
|
||||
with sd_lock:
|
||||
patterns = saveddata_session.query(TargetResists).options(*eager).all()
|
||||
patterns = saveddata_session.query(TargetProfile).options(*eager).all()
|
||||
return patterns
|
||||
|
||||
|
||||
def clearTargetResists():
|
||||
def clearTargetProfiles():
|
||||
with sd_lock:
|
||||
deleted_rows = saveddata_session.query(TargetResists).delete()
|
||||
deleted_rows = saveddata_session.query(TargetProfile).delete()
|
||||
commit()
|
||||
return deleted_rows
|
||||
|
||||
@@ -402,28 +413,28 @@ def getDamagePattern(lookfor, eager=None):
|
||||
eager = processEager(eager)
|
||||
with sd_lock:
|
||||
pattern = saveddata_session.query(DamagePattern).options(*eager).filter(
|
||||
DamagePattern.name == lookfor).first()
|
||||
DamagePattern.rawName == lookfor).first()
|
||||
else:
|
||||
raise TypeError("Need integer or string as argument")
|
||||
return pattern
|
||||
|
||||
|
||||
@cachedQuery(TargetResists, 1, "lookfor")
|
||||
def getTargetResists(lookfor, eager=None):
|
||||
@cachedQuery(TargetProfile, 1, "lookfor")
|
||||
def getTargetProfile(lookfor, eager=None):
|
||||
if isinstance(lookfor, int):
|
||||
if eager is None:
|
||||
with sd_lock:
|
||||
pattern = saveddata_session.query(TargetResists).get(lookfor)
|
||||
pattern = saveddata_session.query(TargetProfile).get(lookfor)
|
||||
else:
|
||||
eager = processEager(eager)
|
||||
with sd_lock:
|
||||
pattern = saveddata_session.query(TargetResists).options(*eager).filter(
|
||||
TargetResists.ID == lookfor).first()
|
||||
pattern = saveddata_session.query(TargetProfile).options(*eager).filter(
|
||||
TargetProfile.ID == lookfor).first()
|
||||
elif isinstance(lookfor, str):
|
||||
eager = processEager(eager)
|
||||
with sd_lock:
|
||||
pattern = saveddata_session.query(TargetResists).options(*eager).filter(
|
||||
TargetResists.name == lookfor).first()
|
||||
pattern = saveddata_session.query(TargetProfile).options(*eager).filter(
|
||||
TargetProfile.rawName == lookfor).first()
|
||||
else:
|
||||
raise TypeError("Need integer or string as argument")
|
||||
return pattern
|
||||
@@ -439,11 +450,11 @@ def getImplantSet(lookfor, eager=None):
|
||||
eager = processEager(eager)
|
||||
with sd_lock:
|
||||
pattern = saveddata_session.query(ImplantSet).options(*eager).filter(
|
||||
TargetResists.ID == lookfor).first()
|
||||
TargetProfile.ID == lookfor).first()
|
||||
elif isinstance(lookfor, str):
|
||||
eager = processEager(eager)
|
||||
with sd_lock:
|
||||
pattern = saveddata_session.query(ImplantSet).options(*eager).filter(TargetResists.name == lookfor).first()
|
||||
pattern = saveddata_session.query(ImplantSet).options(*eager).filter(TargetProfile.name == lookfor).first()
|
||||
else:
|
||||
raise TypeError("Improper argument")
|
||||
return pattern
|
||||
@@ -459,7 +470,7 @@ def searchFits(nameLike, where=None, eager=None):
|
||||
filter = processWhere(Fit.name.like(nameLike, escape="\\"), where)
|
||||
eager = processEager(eager)
|
||||
with sd_lock:
|
||||
fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all())
|
||||
fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).limit(100).all())
|
||||
|
||||
return fits
|
||||
|
||||
@@ -549,6 +560,8 @@ def commit():
|
||||
with sd_lock:
|
||||
try:
|
||||
saveddata_session.commit()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception:
|
||||
saveddata_session.rollback()
|
||||
exc_info = sys.exc_info()
|
||||
@@ -559,6 +572,8 @@ def flush():
|
||||
with sd_lock:
|
||||
try:
|
||||
saveddata_session.flush()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception:
|
||||
saveddata_session.rollback()
|
||||
exc_info = sys.exc_info()
|
||||
|
||||
@@ -22,18 +22,30 @@ from sqlalchemy.orm import mapper
|
||||
import datetime
|
||||
|
||||
from eos.db import saveddata_meta
|
||||
from eos.saveddata.targetResists import TargetResists
|
||||
from eos.saveddata.targetProfile import TargetProfile
|
||||
|
||||
targetResists_table = Table("targetResists", saveddata_meta,
|
||||
Column("ID", Integer, primary_key=True),
|
||||
Column("name", String),
|
||||
Column("emAmount", Float),
|
||||
Column("thermalAmount", Float),
|
||||
Column("kineticAmount", Float),
|
||||
Column("explosiveAmount", Float),
|
||||
Column("ownerID", ForeignKey("users.ID"), nullable=True),
|
||||
Column("created", DateTime, nullable=True, default=datetime.datetime.now),
|
||||
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)
|
||||
)
|
||||
|
||||
mapper(TargetResists, targetResists_table)
|
||||
targetProfiles_table = Table(
|
||||
'targetResists',
|
||||
saveddata_meta,
|
||||
Column('ID', Integer, primary_key=True),
|
||||
Column('name', String),
|
||||
Column('emAmount', Float),
|
||||
Column('thermalAmount', Float),
|
||||
Column('kineticAmount', Float),
|
||||
Column('explosiveAmount', Float),
|
||||
Column('maxVelocity', Float, nullable=True),
|
||||
Column('signatureRadius', Float, nullable=True),
|
||||
Column('radius', Float, nullable=True),
|
||||
Column('ownerID', ForeignKey('users.ID'), nullable=True),
|
||||
Column('created', DateTime, nullable=True, default=datetime.datetime.now),
|
||||
Column('modified', DateTime, nullable=True, onupdate=datetime.datetime.now))
|
||||
|
||||
mapper(
|
||||
TargetProfile,
|
||||
targetProfiles_table,
|
||||
properties={
|
||||
'rawName': targetProfiles_table.c.name,
|
||||
'_maxVelocity': targetProfiles_table.c.maxVelocity,
|
||||
'_signatureRadius': targetProfiles_table.c.signatureRadius,
|
||||
'_radius': targetProfiles_table.c.radius})
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
|
||||
from logbook import Logger
|
||||
from sqlalchemy.orm.collections import collection
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
@@ -138,9 +139,10 @@ class HandledModuleList(HandledList):
|
||||
else:
|
||||
self.appendIgnoreEmpty(mod)
|
||||
|
||||
@collection.appender
|
||||
def appendIgnoreEmpty(self, mod):
|
||||
mod.position = len(self)
|
||||
HandledList.append(self, mod)
|
||||
super().append(mod)
|
||||
if mod.isInvalid:
|
||||
self.remove(mod)
|
||||
|
||||
@@ -394,7 +396,7 @@ class HandledProjectedDroneList(HandledDroneCargoList):
|
||||
proj.projected = False
|
||||
|
||||
|
||||
class HandledItem(object):
|
||||
class HandledItem:
|
||||
def preAssignItemAttr(self, *args, **kwargs):
|
||||
self.itemModifiedAttributes.preAssign(*args, **kwargs)
|
||||
|
||||
@@ -411,7 +413,7 @@ class HandledItem(object):
|
||||
self.itemModifiedAttributes.force(*args, **kwargs)
|
||||
|
||||
|
||||
class HandledCharge(object):
|
||||
class HandledCharge:
|
||||
def preAssignChargeAttr(self, *args, **kwargs):
|
||||
self.chargeModifiedAttributes.preAssign(*args, **kwargs)
|
||||
|
||||
|
||||
11167
eos/effects.py
11167
eos/effects.py
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
class EqBase(object):
|
||||
class EqBase:
|
||||
ID = None
|
||||
|
||||
def __eq__(self, other):
|
||||
|
||||
126
eos/gamedata.py
126
eos/gamedata.py
@@ -18,6 +18,7 @@
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
|
||||
from logbook import Logger
|
||||
@@ -183,6 +184,8 @@ class Effect(EqBase):
|
||||
self.__activeByDefault = True
|
||||
self.__type = None
|
||||
pyfalog.error("AttributeError generating handler: {0}", e)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception as e:
|
||||
self.__handler = eos.effects.DummyEffect.handler
|
||||
self.__runTime = "normal"
|
||||
@@ -199,55 +202,41 @@ class Effect(EqBase):
|
||||
|
||||
try:
|
||||
return self.__effectDef.get(key, None)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
return getattr(self.__effectDef, key, None)
|
||||
|
||||
|
||||
class Item(EqBase):
|
||||
MOVE_ATTRS = (4, # Mass
|
||||
38, # Capacity
|
||||
161) # Volume
|
||||
|
||||
MOVE_ATTR_INFO = None
|
||||
|
||||
ABYSSAL_TYPES = None
|
||||
|
||||
@classmethod
|
||||
def getMoveAttrInfo(cls):
|
||||
info = getattr(cls, "MOVE_ATTR_INFO", None)
|
||||
if info is None:
|
||||
cls.MOVE_ATTR_INFO = info = []
|
||||
for id in cls.MOVE_ATTRS:
|
||||
info.append(eos.db.getAttributeInfo(id))
|
||||
|
||||
return info
|
||||
|
||||
def moveAttrs(self):
|
||||
self.__moved = True
|
||||
for info in self.getMoveAttrInfo():
|
||||
val = getattr(self, info.name, 0)
|
||||
if val != 0:
|
||||
attr = Attribute()
|
||||
attr.info = info
|
||||
attr.value = val
|
||||
self.__attributes[info.name] = attr
|
||||
|
||||
@reconstructor
|
||||
def init(self):
|
||||
self.__race = None
|
||||
self.__requiredSkills = None
|
||||
self.__requiredFor = None
|
||||
self.__moved = False
|
||||
self.__offensive = None
|
||||
self.__assistive = None
|
||||
self.__overrides = None
|
||||
self.__priceObj = None
|
||||
|
||||
def getShortName(self, charLimit=12):
|
||||
if len(self.name) <= charLimit:
|
||||
return self.name
|
||||
splitName = self.name.strip().split(' ')
|
||||
if len(splitName) == 1:
|
||||
return self.name
|
||||
shortName = ''
|
||||
for word in splitName:
|
||||
try:
|
||||
shortName += word[0].capitalize()
|
||||
except IndexError:
|
||||
pass
|
||||
return shortName
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
if not self.__moved:
|
||||
self.moveAttrs()
|
||||
|
||||
return self.__attributes
|
||||
|
||||
@property
|
||||
@@ -300,50 +289,26 @@ class Item(EqBase):
|
||||
eos.db.saveddata_session.delete(override)
|
||||
eos.db.commit()
|
||||
|
||||
srqIDMap = {182: 277, 183: 278, 184: 279, 1285: 1286, 1289: 1287, 1290: 1288}
|
||||
|
||||
@property
|
||||
def requiredSkills(self):
|
||||
if self.__requiredSkills is None:
|
||||
requiredSkills = OrderedDict()
|
||||
self.__requiredSkills = requiredSkills
|
||||
# Map containing attribute IDs we may need for required skills
|
||||
# { requiredSkillX : requiredSkillXLevel }
|
||||
combinedAttrIDs = set(self.srqIDMap.keys()).union(set(self.srqIDMap.values()))
|
||||
# Map containing result of the request
|
||||
# { attributeID : attributeValue }
|
||||
skillAttrs = {}
|
||||
# Get relevant attribute values from db (required skill IDs and levels) for our item
|
||||
for attrInfo in eos.db.directAttributeRequest((self.ID,), tuple(combinedAttrIDs)):
|
||||
attrID = attrInfo[1]
|
||||
attrVal = attrInfo[2]
|
||||
skillAttrs[attrID] = attrVal
|
||||
# Go through all attributeID pairs
|
||||
for srqIDAtrr, srqLvlAttr in self.srqIDMap.items():
|
||||
# Check if we have both in returned result
|
||||
if srqIDAtrr in skillAttrs and srqLvlAttr in skillAttrs:
|
||||
skillID = int(skillAttrs[srqIDAtrr])
|
||||
skillLvl = skillAttrs[srqLvlAttr]
|
||||
# Fetch item from database and fill map
|
||||
item = eos.db.getItem(skillID)
|
||||
requiredSkills[item] = skillLvl
|
||||
self.__requiredSkills = {}
|
||||
if self.reqskills:
|
||||
for skillTypeID, skillLevel in json.loads(self.reqskills).items():
|
||||
skillItem = eos.db.getItem(int(skillTypeID))
|
||||
if skillItem:
|
||||
self.__requiredSkills[skillItem] = skillLevel
|
||||
return self.__requiredSkills
|
||||
|
||||
@property
|
||||
def requiredFor(self):
|
||||
if self.__requiredFor is None:
|
||||
self.__requiredFor = dict()
|
||||
|
||||
# Map containing attribute IDs we may need for required skills
|
||||
|
||||
# Get relevant attribute values from db (required skill IDs and levels) for our item
|
||||
q = eos.db.getRequiredFor(self.ID, self.srqIDMap)
|
||||
|
||||
for itemID, lvl in q:
|
||||
# Fetch item from database and fill map
|
||||
item = eos.db.getItem(itemID)
|
||||
self.__requiredFor[item] = lvl
|
||||
|
||||
self.__requiredFor = {}
|
||||
if self.requiredfor:
|
||||
for typeID, skillLevel in json.loads(self.requiredfor).items():
|
||||
requiredForItem = eos.db.getItem(int(typeID))
|
||||
if requiredForItem:
|
||||
self.__requiredFor[requiredForItem] = skillLevel
|
||||
return self.__requiredFor
|
||||
|
||||
factionMap = {
|
||||
@@ -359,7 +324,8 @@ class Item(EqBase):
|
||||
500016: "sisters",
|
||||
500018: "mordu",
|
||||
500019: "sansha",
|
||||
500020: "serpentis"
|
||||
500020: "serpentis",
|
||||
500026: "triglavian"
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -385,6 +351,7 @@ class Item(EqBase):
|
||||
9 : "guristas", # Caldari + Gallente
|
||||
10 : "angelserp", # Minmatar + Gallente, final race depends on the order of skills
|
||||
12 : "sisters", # Amarr + Gallente
|
||||
15 : "concord",
|
||||
16 : "jove",
|
||||
32 : "sansha", # Incrusion Sansha
|
||||
128: "ore",
|
||||
@@ -516,6 +483,14 @@ class Item(EqBase):
|
||||
def isBooster(self):
|
||||
return self.group.name == 'Booster' and self.category.name == 'Implant'
|
||||
|
||||
@property
|
||||
def isStandup(self):
|
||||
if self.category.name == "Structure Module":
|
||||
return True
|
||||
if self.isFighter and {'fighterSquadronIsStandupLight', 'fighterSquadronIsStandupHeavy', 'fighterSquadronIsStandupSupport'}.intersection(self.attributes):
|
||||
return True
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return "Item(ID={}, name={}) at {}".format(
|
||||
self.ID, self.name, hex(id(self))
|
||||
@@ -580,18 +555,13 @@ class DynamicItemItem(EqBase):
|
||||
class MarketGroup(EqBase):
|
||||
def __repr__(self):
|
||||
return "MarketGroup(ID={}, name={}, parent={}) at {}".format(
|
||||
self.ID, self.name, getattr(self.parent, "name", None), self.name, hex(id(self))
|
||||
)
|
||||
self.ID, self.name, getattr(self.parent, "name", None), hex(id(self)))
|
||||
|
||||
|
||||
class MetaGroup(EqBase):
|
||||
pass
|
||||
|
||||
|
||||
class MetaType(EqBase):
|
||||
pass
|
||||
|
||||
|
||||
class Unit(EqBase):
|
||||
|
||||
def __init__(self):
|
||||
@@ -723,5 +693,15 @@ class Unit(EqBase):
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class Traits(EqBase):
|
||||
pass
|
||||
|
||||
|
||||
class ImplantSet(EqBase):
|
||||
|
||||
@property
|
||||
def fullName(self):
|
||||
if not self.gradeName:
|
||||
return self.setName
|
||||
return '{} {}'.format(self.gradeName, self.setName)
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
# ===============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of eos.
|
||||
#
|
||||
# eos is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# eos is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
import itertools
|
||||
|
||||
|
||||
class Graph:
|
||||
|
||||
def __init__(self, fit, function, data=None):
|
||||
self.fit = fit
|
||||
self.data = {}
|
||||
if data is not None:
|
||||
for name, d in data.items():
|
||||
self.setData(Data(name, d))
|
||||
|
||||
self.function = function
|
||||
|
||||
def clearData(self):
|
||||
self.data.clear()
|
||||
|
||||
def setData(self, data):
|
||||
self.data[data.name] = data
|
||||
|
||||
def getIterator(self):
|
||||
pointNames = []
|
||||
pointIterators = []
|
||||
for data in self.data.values():
|
||||
pointNames.append(data.name)
|
||||
pointIterators.append(data)
|
||||
|
||||
return self._iterator(pointNames, pointIterators)
|
||||
|
||||
def _iterator(self, pointNames, pointIterators):
|
||||
for pointValues in itertools.product(*pointIterators):
|
||||
point = {}
|
||||
for i in range(len(pointValues)):
|
||||
point[pointNames[i]] = pointValues[i]
|
||||
yield point, self.function(point)
|
||||
|
||||
|
||||
class Data:
|
||||
|
||||
def __init__(self, name, dataString, step=None):
|
||||
self.name = name
|
||||
self.step = step
|
||||
self.data = self.parseString(dataString)
|
||||
|
||||
def parseString(self, dataString):
|
||||
if not isinstance(dataString, str):
|
||||
return Constant(dataString),
|
||||
|
||||
dataList = []
|
||||
for data in dataString.split(";"):
|
||||
if isinstance(data, str) and "-" in data:
|
||||
# Dealing with a range
|
||||
dataList.append(Range(data, self.step))
|
||||
else:
|
||||
dataList.append(Constant(data))
|
||||
|
||||
return dataList
|
||||
|
||||
def __iter__(self):
|
||||
for data in self.data:
|
||||
for value in data:
|
||||
yield value
|
||||
|
||||
def isConstant(self):
|
||||
return len(self.data) == 1 and self.data[0].isConstant()
|
||||
|
||||
|
||||
class Constant:
|
||||
|
||||
def __init__(self, const):
|
||||
if isinstance(const, str):
|
||||
self.value = None if const == "" else float(const)
|
||||
else:
|
||||
self.value = const
|
||||
|
||||
def __iter__(self):
|
||||
yield self.value
|
||||
|
||||
@staticmethod
|
||||
def isConstant():
|
||||
return True
|
||||
|
||||
|
||||
class Range:
|
||||
|
||||
def __init__(self, string, step):
|
||||
start, end = string.split("-")
|
||||
self.start = float(start)
|
||||
self.end = float(end)
|
||||
self.step = step
|
||||
|
||||
def __iter__(self):
|
||||
current = start = self.start
|
||||
end = self.end
|
||||
step = self.step or (end - start) / 200
|
||||
i = 0
|
||||
while current < end:
|
||||
current = start + i * step
|
||||
i += 1
|
||||
yield current
|
||||
|
||||
@staticmethod
|
||||
def isConstant():
|
||||
return False
|
||||
@@ -1,24 +0,0 @@
|
||||
import math
|
||||
from logbook import Logger
|
||||
|
||||
from eos.graph import Graph
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FitCapAmountTimeGraph(Graph):
|
||||
|
||||
defaults = {"time": 0}
|
||||
|
||||
def __init__(self, fit, data=None):
|
||||
Graph.__init__(self, fit, self.calcAmount, data if data is not None else self.defaults)
|
||||
self.fit = fit
|
||||
|
||||
def calcAmount(self, data):
|
||||
time = data["time"]
|
||||
maxCap = self.fit.ship.getModifiedItemAttr('capacitorCapacity')
|
||||
regenTime = self.fit.ship.getModifiedItemAttr('rechargeRate') / 1000
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
|
||||
cap = maxCap * (1 + math.exp(5 * -time / regenTime) * -1) ** 2
|
||||
return cap
|
||||
@@ -1,25 +0,0 @@
|
||||
import math
|
||||
from logbook import Logger
|
||||
|
||||
from eos.graph import Graph
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FitCapRegenAmountGraph(Graph):
|
||||
|
||||
defaults = {"percentage": '0-100'}
|
||||
|
||||
def __init__(self, fit, data=None):
|
||||
Graph.__init__(self, fit, self.calcRegen, data if data is not None else self.defaults)
|
||||
self.fit = fit
|
||||
|
||||
def calcRegen(self, data):
|
||||
perc = data['percentage']
|
||||
maxCap = self.fit.ship.getModifiedItemAttr('capacitorCapacity')
|
||||
regenTime = self.fit.ship.getModifiedItemAttr('rechargeRate') / 1000
|
||||
currentCap = maxCap * perc / 100
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
|
||||
regen = 10 * maxCap / regenTime * (math.sqrt(currentCap / maxCap) - currentCap / maxCap)
|
||||
return regen
|
||||
@@ -1,26 +0,0 @@
|
||||
import math
|
||||
from logbook import Logger
|
||||
|
||||
from eos.graph import Graph
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FitDistanceTimeGraph(Graph):
|
||||
|
||||
defaults = {"time": 0}
|
||||
|
||||
def __init__(self, fit, data=None):
|
||||
Graph.__init__(self, fit, self.calcDistance, data if data is not None else self.defaults)
|
||||
self.fit = fit
|
||||
|
||||
def calcDistance(self, data):
|
||||
time = data["time"]
|
||||
maxSpeed = self.fit.ship.getModifiedItemAttr('maxVelocity')
|
||||
mass = self.fit.ship.getModifiedItemAttr('mass')
|
||||
agility = self.fit.ship.getModifiedItemAttr('agility')
|
||||
# Definite integral of:
|
||||
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
|
||||
distance = maxSpeed * time + (maxSpeed * agility * mass * math.exp((-time * 1000000) / (agility * mass)) / 1000000)
|
||||
return distance
|
||||
@@ -1,113 +0,0 @@
|
||||
# ===============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of eos.
|
||||
#
|
||||
# eos is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# eos is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
from logbook import Logger
|
||||
|
||||
from eos.graph import Graph
|
||||
from eos.utils.spoolSupport import SpoolType, SpoolOptions
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FitDmgTimeGraph(Graph):
|
||||
|
||||
defaults = {"time": 0}
|
||||
|
||||
def __init__(self, fit, data=None):
|
||||
Graph.__init__(self, fit, self.calcDmg, data if data is not None else self.defaults)
|
||||
self.fit = fit
|
||||
self.__cache = {}
|
||||
|
||||
def calcDmg(self, data):
|
||||
time = data["time"] * 1000
|
||||
closestTime = max((t for t in self.__cache if t <= time), default=None)
|
||||
if closestTime is None:
|
||||
return 0
|
||||
return self.__cache[closestTime]
|
||||
|
||||
def recalc(self):
|
||||
|
||||
def addDmg(addedTime, addedDmg):
|
||||
if addedDmg == 0:
|
||||
return
|
||||
if addedTime not in self.__cache:
|
||||
prevTime = max((t for t in self.__cache if t < addedTime), default=None)
|
||||
if prevTime is None:
|
||||
self.__cache[addedTime] = 0
|
||||
else:
|
||||
self.__cache[addedTime] = self.__cache[prevTime]
|
||||
for time in (t for t in self.__cache if t >= addedTime):
|
||||
self.__cache[time] += addedDmg
|
||||
|
||||
self.__cache.clear()
|
||||
fit = self.fit
|
||||
# We'll handle calculations in milliseconds
|
||||
maxTime = self.data["time"].data[0].end * 1000
|
||||
for mod in fit.modules:
|
||||
cycleParams = mod.getCycleParameters(reloadOverride=True)
|
||||
if cycleParams is None:
|
||||
continue
|
||||
currentTime = 0
|
||||
nonstopCycles = 0
|
||||
for cycleTime, inactiveTime in cycleParams.iterCycles():
|
||||
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
|
||||
for volleyTime, volley in volleyParams.items():
|
||||
if currentTime + volleyTime <= maxTime and volleyTime <= cycleTime:
|
||||
addDmg(currentTime + volleyTime, volley.total)
|
||||
currentTime += cycleTime
|
||||
currentTime += inactiveTime
|
||||
if inactiveTime == 0:
|
||||
nonstopCycles += 1
|
||||
else:
|
||||
nonstopCycles = 0
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
for drone in fit.drones:
|
||||
cycleParams = drone.getCycleParameters(reloadOverride=True)
|
||||
if cycleParams is None:
|
||||
continue
|
||||
currentTime = 0
|
||||
volleyParams = drone.getVolleyParameters()
|
||||
for cycleTime, inactiveTime in cycleParams.iterCycles():
|
||||
for volleyTime, volley in volleyParams.items():
|
||||
if currentTime + volleyTime <= maxTime and volleyTime <= cycleTime:
|
||||
addDmg(currentTime + volleyTime, volley.total)
|
||||
currentTime += cycleTime
|
||||
currentTime += inactiveTime
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
for fighter in fit.fighters:
|
||||
cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True)
|
||||
if cycleParams is None:
|
||||
continue
|
||||
volleyParams = fighter.getVolleyParametersPerEffect()
|
||||
for effectID, abilityCycleParams in cycleParams.items():
|
||||
if effectID not in volleyParams:
|
||||
continue
|
||||
currentTime = 0
|
||||
abilityVolleyParams = volleyParams[effectID]
|
||||
for cycleTime, inactiveTime in abilityCycleParams.iterCycles():
|
||||
for volleyTime, volley in abilityVolleyParams.items():
|
||||
if currentTime + volleyTime <= maxTime and volleyTime <= cycleTime:
|
||||
addDmg(currentTime + volleyTime, volley.total)
|
||||
currentTime += cycleTime
|
||||
currentTime += inactiveTime
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
@@ -1,209 +0,0 @@
|
||||
# ===============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of eos.
|
||||
#
|
||||
# eos is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# eos is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
from math import exp, log, radians, sin, inf
|
||||
|
||||
from logbook import Logger
|
||||
|
||||
from eos.const import FittingHardpoint, FittingModuleState
|
||||
from eos.graph import Graph
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FitDpsRangeGraph(Graph):
|
||||
|
||||
defaults = {
|
||||
"angle" : 0,
|
||||
"distance" : 0,
|
||||
"signatureRadius": None,
|
||||
"velocity" : 0
|
||||
}
|
||||
|
||||
def __init__(self, fit, data=None):
|
||||
Graph.__init__(self, fit, self.calcDps, data if data is not None else self.defaults)
|
||||
self.fit = fit
|
||||
|
||||
def calcDps(self, data):
|
||||
ew = {'signatureRadius': [], 'velocity': []}
|
||||
fit = self.fit
|
||||
total = 0
|
||||
distance = data["distance"] * 1000
|
||||
abssort = lambda _val: -abs(_val - 1)
|
||||
|
||||
for mod in fit.modules:
|
||||
if not mod.isEmpty and mod.state >= FittingModuleState.ACTIVE:
|
||||
if "remoteTargetPaintFalloff" in mod.item.effects or "structureModuleEffectTargetPainter" in mod.item.effects:
|
||||
ew['signatureRadius'].append(
|
||||
1 + (mod.getModifiedItemAttr("signatureRadiusBonus") / 100) * self.calculateModuleMultiplier(
|
||||
mod, data))
|
||||
if "remoteWebifierFalloff" in mod.item.effects or "structureModuleEffectStasisWebifier" in mod.item.effects:
|
||||
if distance <= mod.getModifiedItemAttr("maxRange"):
|
||||
ew['velocity'].append(1 + (mod.getModifiedItemAttr("speedFactor") / 100))
|
||||
elif mod.getModifiedItemAttr("falloffEffectiveness") > 0:
|
||||
# I am affected by falloff
|
||||
ew['velocity'].append(
|
||||
1 + (mod.getModifiedItemAttr("speedFactor") / 100) * self.calculateModuleMultiplier(mod,
|
||||
data))
|
||||
|
||||
ew['signatureRadius'].sort(key=abssort)
|
||||
ew['velocity'].sort(key=abssort)
|
||||
|
||||
for attr, values in ew.items():
|
||||
val = data[attr]
|
||||
try:
|
||||
for i in range(len(values)):
|
||||
bonus = values[i]
|
||||
val *= 1 + (bonus - 1) * exp(- i ** 2 / 7.1289)
|
||||
data[attr] = val
|
||||
except Exception as e:
|
||||
pyfalog.critical("Caught exception in calcDPS.")
|
||||
pyfalog.critical(e)
|
||||
|
||||
for mod in fit.modules:
|
||||
dps = mod.getDps(targetResists=fit.targetResists).total
|
||||
if mod.hardpoint == FittingHardpoint.TURRET:
|
||||
if mod.state >= FittingModuleState.ACTIVE:
|
||||
total += dps * self.calculateTurretMultiplier(mod, data)
|
||||
|
||||
elif mod.hardpoint == FittingHardpoint.MISSILE:
|
||||
if mod.state >= FittingModuleState.ACTIVE and mod.maxRange is not None and mod.maxRange >= distance:
|
||||
total += dps * self.calculateMissileMultiplier(mod, data)
|
||||
|
||||
if distance <= fit.extraAttributes["droneControlRange"]:
|
||||
for drone in fit.drones:
|
||||
multiplier = 1 if drone.getModifiedItemAttr("maxVelocity") > 1 else self.calculateTurretMultiplier(
|
||||
drone, data)
|
||||
dps = drone.getDps(targetResists=fit.targetResists).total
|
||||
total += dps * multiplier
|
||||
|
||||
# this is janky as fuck
|
||||
for fighter in fit.fighters:
|
||||
if not fighter.active:
|
||||
continue
|
||||
fighterDpsMap = fighter.getDpsPerEffect(targetResists=fit.targetResists)
|
||||
for ability in fighter.abilities:
|
||||
if ability.dealsDamage and ability.active:
|
||||
if ability.effectID not in fighterDpsMap:
|
||||
continue
|
||||
multiplier = self.calculateFighterMissileMultiplier(ability, data)
|
||||
dps = fighterDpsMap[ability.effectID].total
|
||||
total += dps * multiplier
|
||||
|
||||
return total
|
||||
|
||||
@staticmethod
|
||||
def calculateMissileMultiplier(mod, data):
|
||||
targetSigRad = data["signatureRadius"]
|
||||
targetVelocity = data["velocity"]
|
||||
explosionRadius = mod.getModifiedChargeAttr("aoeCloudSize")
|
||||
targetSigRad = explosionRadius if targetSigRad is None else targetSigRad
|
||||
explosionVelocity = mod.getModifiedChargeAttr("aoeVelocity")
|
||||
damageReductionFactor = mod.getModifiedChargeAttr("aoeDamageReductionFactor")
|
||||
|
||||
sigRadiusFactor = targetSigRad / explosionRadius
|
||||
if targetVelocity:
|
||||
velocityFactor = (explosionVelocity / explosionRadius * targetSigRad / targetVelocity) ** damageReductionFactor
|
||||
else:
|
||||
velocityFactor = 1
|
||||
|
||||
return min(sigRadiusFactor, velocityFactor, 1)
|
||||
|
||||
def calculateTurretMultiplier(self, mod, data):
|
||||
# Source for most of turret calculation info: http://wiki.eveonline.com/en/wiki/Falloff
|
||||
chanceToHit = self.calculateTurretChanceToHit(mod, data)
|
||||
if chanceToHit > 0.01:
|
||||
# AvgDPS = Base Damage * [ ( ChanceToHit^2 + ChanceToHit + 0.0499 ) / 2 ]
|
||||
multiplier = (chanceToHit ** 2 + chanceToHit + 0.0499) / 2
|
||||
else:
|
||||
# All hits are wreckings
|
||||
multiplier = chanceToHit * 3
|
||||
dmgScaling = mod.getModifiedItemAttr("turretDamageScalingRadius")
|
||||
if dmgScaling:
|
||||
targetSigRad = data["signatureRadius"]
|
||||
multiplier = min(1, (float(targetSigRad) / dmgScaling) ** 2)
|
||||
return multiplier
|
||||
|
||||
@staticmethod
|
||||
def calculateFighterMissileMultiplier(ability, data):
|
||||
prefix = ability.attrPrefix
|
||||
|
||||
targetSigRad = data["signatureRadius"]
|
||||
targetVelocity = data["velocity"]
|
||||
explosionRadius = ability.fighter.getModifiedItemAttr("{}ExplosionRadius".format(prefix))
|
||||
explosionVelocity = ability.fighter.getModifiedItemAttr("{}ExplosionVelocity".format(prefix))
|
||||
damageReductionFactor = ability.fighter.getModifiedItemAttr("{}ReductionFactor".format(prefix), None)
|
||||
|
||||
# the following conditionals are because CCP can't keep a decent naming convention, as if fighter implementation
|
||||
# wasn't already fucked.
|
||||
if damageReductionFactor is None:
|
||||
damageReductionFactor = ability.fighter.getModifiedItemAttr("{}DamageReductionFactor".format(prefix))
|
||||
|
||||
damageReductionSensitivity = ability.fighter.getModifiedItemAttr("{}ReductionSensitivity".format(prefix), None)
|
||||
if damageReductionSensitivity is None:
|
||||
damageReductionSensitivity = ability.fighter.getModifiedItemAttr(
|
||||
"{}DamageReductionSensitivity".format(prefix))
|
||||
|
||||
targetSigRad = explosionRadius if targetSigRad is None else targetSigRad
|
||||
sigRadiusFactor = targetSigRad / explosionRadius
|
||||
|
||||
if targetVelocity:
|
||||
velocityFactor = (explosionVelocity / explosionRadius * targetSigRad / targetVelocity) ** (
|
||||
log(damageReductionFactor) / log(damageReductionSensitivity))
|
||||
else:
|
||||
velocityFactor = 1
|
||||
|
||||
return min(sigRadiusFactor, velocityFactor, 1)
|
||||
|
||||
def calculateTurretChanceToHit(self, mod, data):
|
||||
distance = data["distance"] * 1000
|
||||
tracking = mod.getModifiedItemAttr("trackingSpeed")
|
||||
turretOptimal = mod.maxRange
|
||||
turretFalloff = mod.falloff
|
||||
turretSigRes = mod.getModifiedItemAttr("optimalSigRadius")
|
||||
targetSigRad = data["signatureRadius"]
|
||||
targetSigRad = turretSigRes if targetSigRad is None else targetSigRad
|
||||
transversal = sin(radians(data["angle"])) * data["velocity"]
|
||||
|
||||
# Angular velocity is calculated using range from ship center to target center.
|
||||
# We do not know target radius but we know attacker radius
|
||||
angDistance = distance + self.fit.ship.getModifiedItemAttr('radius', 0)
|
||||
if angDistance == 0 and transversal == 0:
|
||||
angularVelocity = 0
|
||||
elif angDistance == 0 and transversal != 0:
|
||||
angularVelocity = inf
|
||||
else:
|
||||
angularVelocity = transversal / angDistance
|
||||
trackingEq = (((angularVelocity / tracking) *
|
||||
(turretSigRes / targetSigRad)) ** 2)
|
||||
rangeEq = ((max(0, distance - turretOptimal)) / turretFalloff) ** 2
|
||||
|
||||
return 0.5 ** (trackingEq + rangeEq)
|
||||
|
||||
@staticmethod
|
||||
def calculateModuleMultiplier(mod, data):
|
||||
# Simplified formula, we make some assumptions about the module
|
||||
# This is basically the calculateTurretChanceToHit without tracking values
|
||||
distance = data["distance"] * 1000
|
||||
turretOptimal = mod.maxRange
|
||||
turretFalloff = mod.falloff
|
||||
rangeEq = ((max(0, distance - turretOptimal)) / turretFalloff) ** 2
|
||||
|
||||
return 0.5 ** rangeEq
|
||||
@@ -1,112 +0,0 @@
|
||||
# ===============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of eos.
|
||||
#
|
||||
# eos is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# eos is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
from logbook import Logger
|
||||
|
||||
from eos.graph import Graph
|
||||
from eos.utils.spoolSupport import SpoolType, SpoolOptions
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FitDpsTimeGraph(Graph):
|
||||
|
||||
defaults = {"time": 0}
|
||||
|
||||
def __init__(self, fit, data=None):
|
||||
Graph.__init__(self, fit, self.calcDps, data if data is not None else self.defaults)
|
||||
self.fit = fit
|
||||
self.__cache = []
|
||||
|
||||
def calcDps(self, data):
|
||||
time = data["time"] * 1000
|
||||
entries = (e for e in self.__cache if e[0] <= time < e[1])
|
||||
dps = sum(e[2] for e in entries)
|
||||
return dps
|
||||
|
||||
def recalc(self):
|
||||
|
||||
def addDmg(addedTimeStart, addedTimeFinish, addedDmg):
|
||||
if addedDmg == 0:
|
||||
return
|
||||
addedDps = 1000 * addedDmg / (addedTimeFinish - addedTimeStart)
|
||||
self.__cache.append((addedTimeStart, addedTimeFinish, addedDps))
|
||||
|
||||
self.__cache = []
|
||||
fit = self.fit
|
||||
# We'll handle calculations in milliseconds
|
||||
maxTime = self.data["time"].data[0].end * 1000
|
||||
for mod in fit.modules:
|
||||
cycleParams = mod.getCycleParameters(reloadOverride=True)
|
||||
if cycleParams is None:
|
||||
continue
|
||||
currentTime = 0
|
||||
nonstopCycles = 0
|
||||
for cycleTime, inactiveTime in cycleParams.iterCycles():
|
||||
cycleDamage = 0
|
||||
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
|
||||
for volleyTime, volley in volleyParams.items():
|
||||
if currentTime + volleyTime <= maxTime and volleyTime <= cycleTime:
|
||||
cycleDamage += volley.total
|
||||
addDmg(currentTime, currentTime + cycleTime, cycleDamage)
|
||||
currentTime += cycleTime
|
||||
currentTime += inactiveTime
|
||||
if inactiveTime > 0:
|
||||
nonstopCycles = 0
|
||||
else:
|
||||
nonstopCycles += 1
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
for drone in fit.drones:
|
||||
cycleParams = drone.getCycleParameters(reloadOverride=True)
|
||||
if cycleParams is None:
|
||||
continue
|
||||
currentTime = 0
|
||||
for cycleTime, inactiveTime in cycleParams.iterCycles():
|
||||
cycleDamage = 0
|
||||
volleyParams = drone.getVolleyParameters()
|
||||
for volleyTime, volley in volleyParams.items():
|
||||
if currentTime + volleyTime <= maxTime and volleyTime <= cycleTime:
|
||||
cycleDamage += volley.total
|
||||
addDmg(currentTime, currentTime + cycleTime, cycleDamage)
|
||||
currentTime += cycleTime
|
||||
currentTime += inactiveTime
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
for fighter in fit.fighters:
|
||||
cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True)
|
||||
if cycleParams is None:
|
||||
continue
|
||||
volleyParams = fighter.getVolleyParametersPerEffect()
|
||||
for effectID, abilityCycleParams in cycleParams.items():
|
||||
if effectID not in volleyParams:
|
||||
continue
|
||||
abilityVolleyParams = volleyParams[effectID]
|
||||
currentTime = 0
|
||||
for cycleTime, inactiveTime in abilityCycleParams.iterCycles():
|
||||
cycleDamage = 0
|
||||
for volleyTime, volley in abilityVolleyParams.items():
|
||||
if currentTime + volleyTime <= maxTime and volleyTime <= cycleTime:
|
||||
cycleDamage += volley.total
|
||||
addDmg(currentTime, currentTime + cycleTime, cycleDamage)
|
||||
currentTime += cycleTime
|
||||
currentTime += inactiveTime
|
||||
if currentTime > maxTime:
|
||||
break
|
||||
@@ -1,29 +0,0 @@
|
||||
import math
|
||||
from logbook import Logger
|
||||
|
||||
from eos.graph import Graph
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FitShieldAmountTimeGraph(Graph):
|
||||
|
||||
defaults = {"time": 0}
|
||||
|
||||
def __init__(self, fit, data=None):
|
||||
Graph.__init__(self, fit, self.calcAmount, data if data is not None else self.defaults)
|
||||
self.fit = fit
|
||||
import gui.mainFrame
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
|
||||
def calcAmount(self, data):
|
||||
time = data["time"]
|
||||
maxShield = self.fit.ship.getModifiedItemAttr('shieldCapacity')
|
||||
regenTime = self.fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate (shield is similar to cap)
|
||||
shield = maxShield * (1 + math.exp(5 * -time / regenTime) * -1) ** 2
|
||||
useEhp = self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective
|
||||
if self.fit.damagePattern is not None and useEhp:
|
||||
shield = self.fit.damagePattern.effectivify(self.fit, shield, 'shield')
|
||||
return shield
|
||||
@@ -1,49 +0,0 @@
|
||||
# ===============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of eos.
|
||||
#
|
||||
# eos is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# eos is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
import math
|
||||
from logbook import Logger
|
||||
|
||||
from eos.graph import Graph
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FitShieldRegenAmountGraph(Graph):
|
||||
|
||||
defaults = {"percentage": '0-100'}
|
||||
|
||||
def __init__(self, fit, data=None):
|
||||
Graph.__init__(self, fit, self.calcRegen, data if data is not None else self.defaults)
|
||||
self.fit = fit
|
||||
import gui.mainFrame
|
||||
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
|
||||
|
||||
def calcRegen(self, data):
|
||||
perc = data["percentage"]
|
||||
maxShield = self.fit.ship.getModifiedItemAttr('shieldCapacity')
|
||||
regenTime = self.fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000
|
||||
currentShield = maxShield * perc / 100
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate (shield is similar to cap)
|
||||
regen = 10 * maxShield / regenTime * (math.sqrt(currentShield / maxShield) - currentShield / maxShield)
|
||||
useEhp = self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective
|
||||
if self.fit.damagePattern is not None and useEhp:
|
||||
regen = self.fit.damagePattern.effectivify(self.fit, regen, 'shield')
|
||||
return regen
|
||||
@@ -1,25 +0,0 @@
|
||||
import math
|
||||
from logbook import Logger
|
||||
|
||||
from eos.graph import Graph
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FitSpeedTimeGraph(Graph):
|
||||
|
||||
defaults = {"time": 0}
|
||||
|
||||
def __init__(self, fit, data=None):
|
||||
Graph.__init__(self, fit, self.calcSpeed, data if data is not None else self.defaults)
|
||||
self.fit = fit
|
||||
|
||||
def calcSpeed(self, data):
|
||||
time = data["time"]
|
||||
maxSpeed = self.fit.ship.getModifiedItemAttr('maxVelocity')
|
||||
mass = self.fit.ship.getModifiedItemAttr('mass')
|
||||
agility = self.fit.ship.getModifiedItemAttr('agility')
|
||||
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
|
||||
speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass)))
|
||||
return speed
|
||||
@@ -1,61 +0,0 @@
|
||||
import math
|
||||
from logbook import Logger
|
||||
|
||||
from eos.graph import Graph
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
AU_METERS = 149597870700
|
||||
|
||||
|
||||
class FitWarpTimeDistanceGraph(Graph):
|
||||
|
||||
defaults = {"distance": 0}
|
||||
|
||||
def __init__(self, fit, data=None):
|
||||
Graph.__init__(self, fit, self.calcTime, data if data is not None else self.defaults)
|
||||
self.fit = fit
|
||||
|
||||
def calcTime(self, data):
|
||||
distance = data["distance"]
|
||||
if distance == 0:
|
||||
return 0
|
||||
maxWarpDistance = self.fit.maxWarpDistance
|
||||
if distance > maxWarpDistance:
|
||||
return None
|
||||
maxSubwarpSpeed = self.fit.ship.getModifiedItemAttr('maxVelocity')
|
||||
maxWarpSpeed = self.fit.warpSpeed
|
||||
time = calculate_time_in_warp(maxWarpSpeed, maxSubwarpSpeed, distance * AU_METERS)
|
||||
return time
|
||||
|
||||
|
||||
# Taken from https://wiki.eveuniversity.org/Warp_time_calculation#Implementation
|
||||
# with minor modifications
|
||||
# Warp speed in AU/s, subwarp speed in m/s, distance in m
|
||||
def calculate_time_in_warp(max_warp_speed, max_subwarp_speed, warp_dist):
|
||||
|
||||
k_accel = max_warp_speed
|
||||
k_decel = min(max_warp_speed / 3, 2)
|
||||
|
||||
warp_dropout_speed = max_subwarp_speed / 2
|
||||
max_ms_warp_speed = max_warp_speed * AU_METERS
|
||||
|
||||
accel_dist = AU_METERS
|
||||
decel_dist = max_ms_warp_speed / k_decel
|
||||
|
||||
minimum_dist = accel_dist + decel_dist
|
||||
|
||||
cruise_time = 0
|
||||
|
||||
if minimum_dist > warp_dist:
|
||||
max_ms_warp_speed = warp_dist * k_accel * k_decel / (k_accel + k_decel)
|
||||
else:
|
||||
cruise_time = (warp_dist - minimum_dist) / max_ms_warp_speed
|
||||
|
||||
accel_time = math.log(max_ms_warp_speed / k_accel) / k_accel
|
||||
decel_time = math.log(max_ms_warp_speed / warp_dropout_speed) / k_decel
|
||||
|
||||
total_time = cruise_time + accel_time + decel_time
|
||||
return total_time
|
||||
@@ -17,50 +17,90 @@
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
import collections
|
||||
|
||||
from collections.abc import MutableMapping
|
||||
from copy import copy
|
||||
from math import exp
|
||||
|
||||
from eos.const import Operator
|
||||
# TODO: This needs to be moved out, we shouldn't have *ANY* dependencies back to other modules/methods inside eos.
|
||||
# This also breaks writing any tests. :(
|
||||
from eos.db.gamedata.queries import getAttributeInfo
|
||||
|
||||
|
||||
defaultValuesCache = {}
|
||||
cappingAttrKeyCache = {}
|
||||
resistanceCache = {}
|
||||
|
||||
|
||||
class ItemAttrShortcut(object):
|
||||
def getAttrDefault(key, fallback=None):
|
||||
try:
|
||||
default = defaultValuesCache[key]
|
||||
except KeyError:
|
||||
attrInfo = getAttributeInfo(key)
|
||||
if attrInfo is None:
|
||||
default = defaultValuesCache[key] = None
|
||||
else:
|
||||
default = defaultValuesCache[key] = attrInfo.defaultValue
|
||||
if default is None:
|
||||
default = fallback
|
||||
return default
|
||||
|
||||
|
||||
def getResistanceAttrID(modifyingItem, effect):
|
||||
# If it doesn't exist on the effect, check the modifying module's attributes.
|
||||
# If it's there, cache it and return
|
||||
if effect.resistanceID:
|
||||
return effect.resistanceID
|
||||
cacheKey = (modifyingItem.item.ID, effect.ID)
|
||||
try:
|
||||
return resistanceCache[cacheKey]
|
||||
except KeyError:
|
||||
attrPrefix = effect.getattr('prefix')
|
||||
if attrPrefix:
|
||||
resistanceID = int(modifyingItem.getModifiedItemAttr('{}ResistanceID'.format(attrPrefix))) or None
|
||||
if not resistanceID:
|
||||
resistanceID = int(modifyingItem.getModifiedItemAttr('{}RemoteResistanceID'.format(attrPrefix))) or None
|
||||
else:
|
||||
resistanceID = int(modifyingItem.getModifiedItemAttr("remoteResistanceID")) or None
|
||||
resistanceCache[cacheKey] = resistanceID
|
||||
return resistanceID
|
||||
|
||||
|
||||
class ItemAttrShortcut:
|
||||
|
||||
def getModifiedItemAttr(self, key, default=0):
|
||||
return_value = self.itemModifiedAttributes.get(key)
|
||||
return return_value or default
|
||||
|
||||
def getModifiedItemAttrExtended(self, key, extraMultipliers=None, ignoreAfflictors=(), default=0):
|
||||
return_value = self.itemModifiedAttributes.getExtended(key, extraMultipliers=extraMultipliers, ignoreAfflictors=ignoreAfflictors)
|
||||
return return_value or default
|
||||
|
||||
def getItemBaseAttrValue(self, key, default=0):
|
||||
"""
|
||||
Gets base value in this order:
|
||||
Mutated value > override value > attribute value
|
||||
"""
|
||||
return_value = self.itemModifiedAttributes.getOriginal(key)
|
||||
return return_value or default
|
||||
|
||||
|
||||
class ChargeAttrShortcut:
|
||||
|
||||
def getModifiedChargeAttr(self, key, default=0):
|
||||
return_value = self.chargeModifiedAttributes.get(key)
|
||||
return return_value or default
|
||||
|
||||
def getModifiedChargeAttrExtended(self, key, extraMultipliers=None, ignoreAfflictors=(), default=0):
|
||||
return_value = self.chargeModifiedAttributes.getExtended(key, extraMultipliers=extraMultipliers, ignoreAfflictors=ignoreAfflictors)
|
||||
return return_value or default
|
||||
|
||||
def getChargeBaseAttrValue(self, key, default=0):
|
||||
"""
|
||||
Gets base value in this order:
|
||||
Mutated value > override value > attribute value
|
||||
"""
|
||||
return_value = self.chargeModifiedAttributes.getOriginal(key)
|
||||
return return_value or default
|
||||
|
||||
|
||||
class ChargeAttrShortcut(object):
|
||||
def getModifiedChargeAttr(self, key, default=0):
|
||||
return_value = self.chargeModifiedAttributes.get(key)
|
||||
|
||||
return return_value or default
|
||||
|
||||
|
||||
class ModifiedAttributeDict(collections.MutableMapping):
|
||||
class ModifiedAttributeDict(MutableMapping):
|
||||
overrides_enabled = False
|
||||
|
||||
class CalculationPlaceholder(object):
|
||||
class CalculationPlaceholder:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@@ -74,6 +114,10 @@ class ModifiedAttributeDict(collections.MutableMapping):
|
||||
# Final modified values
|
||||
self.__modified = {}
|
||||
# Affected by entities
|
||||
# Format:
|
||||
# {attr name: {modifying fit: (
|
||||
# modifying item, operation, stacking group, pre-resist amount,
|
||||
# post-resist amount, affects result or not)}}
|
||||
self.__affectedBy = {}
|
||||
# Overrides (per item)
|
||||
self.__overrides = {}
|
||||
@@ -144,24 +188,74 @@ class ModifiedAttributeDict(collections.MutableMapping):
|
||||
|
||||
def __getitem__(self, key):
|
||||
# Check if we have final calculated value
|
||||
key_value = self.__modified.get(key)
|
||||
if key_value is self.CalculationPlaceholder:
|
||||
key_value = self.__modified[key] = self.__calculateValue(key)
|
||||
|
||||
if key_value is not None:
|
||||
return key_value
|
||||
val = self.__modified.get(key)
|
||||
if val is self.CalculationPlaceholder:
|
||||
val = self.__modified[key] = self.__calculateValue(key)
|
||||
if val is not None:
|
||||
return val
|
||||
|
||||
# Then in values which are not yet calculated
|
||||
if self.__intermediary:
|
||||
val = self.__intermediary.get(key)
|
||||
else:
|
||||
val = None
|
||||
|
||||
if val is not None:
|
||||
return val
|
||||
|
||||
# Original value is the least priority
|
||||
return self.getOriginal(key)
|
||||
|
||||
def getExtended(self, key, extraMultipliers=None, ignoreAfflictors=None, default=0):
|
||||
"""
|
||||
Here we consider couple of parameters. If they affect final result, we do
|
||||
not store result, and if they are - we do.
|
||||
"""
|
||||
# Here we do not have support for preAssigns/forceds, as doing them would
|
||||
# mean that we have to store all of them in a list which increases memory use,
|
||||
# and we do not actually need those operators atm
|
||||
preIncreaseAdjustment = 0
|
||||
multiplierAdjustment = 1
|
||||
ignorePenalizedMultipliers = {}
|
||||
postIncreaseAdjustment = 0
|
||||
for fit, afflictors in self.getAfflictions(key).items():
|
||||
for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in afflictors:
|
||||
if afflictor in ignoreAfflictors:
|
||||
if operator == Operator.MULTIPLY:
|
||||
if stackingGroup is None:
|
||||
multiplierAdjustment /= postResAmount
|
||||
else:
|
||||
ignorePenalizedMultipliers.setdefault(stackingGroup, []).append(postResAmount)
|
||||
elif operator == Operator.PREINCREASE:
|
||||
preIncreaseAdjustment -= postResAmount
|
||||
elif operator == Operator.POSTINCREASE:
|
||||
postIncreaseAdjustment -= postResAmount
|
||||
|
||||
# If we apply no customizations - use regular getter
|
||||
if (
|
||||
not extraMultipliers and
|
||||
preIncreaseAdjustment == 0 and multiplierAdjustment == 1 and
|
||||
postIncreaseAdjustment == 0 and len(ignorePenalizedMultipliers) == 0
|
||||
):
|
||||
return self.get(key, default=default)
|
||||
|
||||
# Try to calculate custom values
|
||||
val = self.__calculateValue(
|
||||
key, extraMultipliers=extraMultipliers, preIncAdj=preIncreaseAdjustment, multAdj=multiplierAdjustment,
|
||||
postIncAdj=postIncreaseAdjustment, ignorePenMult=ignorePenalizedMultipliers)
|
||||
if val is not None:
|
||||
return val
|
||||
|
||||
# Then the same fallbacks as in regular getter
|
||||
if self.__intermediary:
|
||||
val = self.__intermediary.get(key)
|
||||
else:
|
||||
# Original value is the least priority
|
||||
return self.getOriginal(key)
|
||||
val = None
|
||||
if val is not None:
|
||||
return val
|
||||
val = self.getOriginal(key)
|
||||
if val is not None:
|
||||
return val
|
||||
return default
|
||||
|
||||
def __delitem__(self, key):
|
||||
if key in self.__modified:
|
||||
@@ -181,6 +275,9 @@ class ModifiedAttributeDict(collections.MutableMapping):
|
||||
if self.original:
|
||||
val = self.original.get(key, val)
|
||||
|
||||
if val is None:
|
||||
val = getAttrDefault(key, fallback=None)
|
||||
|
||||
if val is None and val != default:
|
||||
val = default
|
||||
|
||||
@@ -208,7 +305,7 @@ class ModifiedAttributeDict(collections.MutableMapping):
|
||||
keys.update(iter(self.__intermediary.keys()))
|
||||
return len(keys)
|
||||
|
||||
def __calculateValue(self, key):
|
||||
def __calculateValue(self, key, extraMultipliers=None, preIncAdj=None, multAdj=None, postIncAdj=None, ignorePenMult=None):
|
||||
# It's possible that various attributes are capped by other attributes,
|
||||
# it's defined by reference maxAttributeID
|
||||
try:
|
||||
@@ -245,29 +342,52 @@ class ModifiedAttributeDict(collections.MutableMapping):
|
||||
preIncrease = self.__preIncreases.get(key, 0)
|
||||
multiplier = self.__multipliers.get(key, 1)
|
||||
penalizedMultiplierGroups = self.__penalizedMultipliers.get(key, {})
|
||||
# Add extra multipliers to the group, not modifying initial data source
|
||||
if extraMultipliers is not None:
|
||||
penalizedMultiplierGroups = copy(penalizedMultiplierGroups)
|
||||
for stackGroup, operationsData in extraMultipliers.items():
|
||||
multipliers = []
|
||||
for mult, resAttrID in operationsData:
|
||||
if not resAttrID:
|
||||
multipliers.append(mult)
|
||||
continue
|
||||
resAttrInfo = getAttributeInfo(resAttrID)
|
||||
if not resAttrInfo:
|
||||
multipliers.append(mult)
|
||||
continue
|
||||
resMult = self.fit.ship.itemModifiedAttributes[resAttrInfo.attributeName]
|
||||
if resMult is None or resMult == 1:
|
||||
multipliers.append(mult)
|
||||
continue
|
||||
mult = (mult - 1) * resMult + 1
|
||||
multipliers.append(mult)
|
||||
penalizedMultiplierGroups[stackGroup] = penalizedMultiplierGroups.get(stackGroup, []) + multipliers
|
||||
postIncrease = self.__postIncreases.get(key, 0)
|
||||
|
||||
# Grab initial value, priorities are:
|
||||
# Results of ongoing calculation > preAssign > original > 0
|
||||
try:
|
||||
default = defaultValuesCache[key]
|
||||
except KeyError:
|
||||
attrInfo = getAttributeInfo(key)
|
||||
if attrInfo is None:
|
||||
default = defaultValuesCache[key] = 0.0
|
||||
else:
|
||||
dv = attrInfo.defaultValue
|
||||
default = defaultValuesCache[key] = dv if dv is not None else 0.0
|
||||
|
||||
default = getAttrDefault(key, fallback=0.0)
|
||||
val = self.__intermediary.get(key, self.__preAssigns.get(key, self.getOriginal(key, default)))
|
||||
|
||||
# We'll do stuff in the following order:
|
||||
# preIncrease > multiplier > stacking penalized multipliers > postIncrease
|
||||
val += preIncrease
|
||||
if preIncAdj is not None:
|
||||
val += preIncAdj
|
||||
val *= multiplier
|
||||
if multAdj is not None:
|
||||
val *= multAdj
|
||||
# Each group is penalized independently
|
||||
# Things in different groups will not be stack penalized between each other
|
||||
for penalizedMultipliers in penalizedMultiplierGroups.values():
|
||||
for penaltyGroup, penalizedMultipliers in penalizedMultiplierGroups.items():
|
||||
if ignorePenMult is not None and penaltyGroup in ignorePenMult:
|
||||
# Avoid modifying source and remove multipliers we were asked to remove for this calc
|
||||
penalizedMultipliers = penalizedMultipliers[:]
|
||||
for ignoreMult in ignorePenMult[penaltyGroup]:
|
||||
try:
|
||||
penalizedMultipliers.remove(ignoreMult)
|
||||
except ValueError:
|
||||
pass
|
||||
# A quick explanation of how this works:
|
||||
# 1: Bonuses and penalties are calculated seperately, so we'll have to filter each of them
|
||||
l1 = [_val for _val in penalizedMultipliers if _val > 1]
|
||||
@@ -285,6 +405,8 @@ class ModifiedAttributeDict(collections.MutableMapping):
|
||||
bonus = l[i]
|
||||
val *= 1 + (bonus - 1) * exp(- i ** 2 / 7.1289)
|
||||
val += postIncrease
|
||||
if postIncAdj is not None:
|
||||
val += postIncAdj
|
||||
|
||||
# Cap value if we have cap defined
|
||||
if cappingValue is not None:
|
||||
@@ -311,7 +433,7 @@ class ModifiedAttributeDict(collections.MutableMapping):
|
||||
def iterAfflictions(self):
|
||||
return self.__affectedBy.__iter__()
|
||||
|
||||
def __afflict(self, attributeName, operation, bonus, used=True):
|
||||
def __afflict(self, attributeName, operator, stackingGroup, preResAmount, postResAmount, used=True):
|
||||
"""Add modifier to list of things affecting current item"""
|
||||
# Do nothing if no fit is assigned
|
||||
fit = self.fit
|
||||
@@ -337,13 +459,13 @@ class ModifiedAttributeDict(collections.MutableMapping):
|
||||
modifier = fit.getModifier()
|
||||
|
||||
# Add current affliction to list
|
||||
affs.append((modifier, operation, bonus, used))
|
||||
affs.append((modifier, operator, stackingGroup, preResAmount, postResAmount, used))
|
||||
|
||||
def preAssign(self, attributeName, value):
|
||||
def preAssign(self, attributeName, value, **kwargs):
|
||||
"""Overwrites original value of the entity with given one, allowing further modification"""
|
||||
self.__preAssigns[attributeName] = value
|
||||
self.__placehold(attributeName)
|
||||
self.__afflict(attributeName, "=", value, value != self.getOriginal(attributeName))
|
||||
self.__afflict(attributeName, Operator.PREASSIGN, None, value, value, value != self.getOriginal(attributeName))
|
||||
|
||||
def increase(self, attributeName, increase, position="pre", skill=None, **kwargs):
|
||||
"""Increase value of given attribute by given number"""
|
||||
@@ -356,8 +478,10 @@ class ModifiedAttributeDict(collections.MutableMapping):
|
||||
# Increases applied before multiplications and after them are
|
||||
# written in separate maps
|
||||
if position == "pre":
|
||||
operator = Operator.PREINCREASE
|
||||
tbl = self.__preIncreases
|
||||
elif position == "post":
|
||||
operator = Operator.POSTINCREASE
|
||||
tbl = self.__postIncreases
|
||||
else:
|
||||
raise ValueError("position should be either pre or post")
|
||||
@@ -365,9 +489,9 @@ class ModifiedAttributeDict(collections.MutableMapping):
|
||||
tbl[attributeName] = 0
|
||||
tbl[attributeName] += increase
|
||||
self.__placehold(attributeName)
|
||||
self.__afflict(attributeName, "+", increase, increase != 0)
|
||||
self.__afflict(attributeName, operator, None, increase, increase, increase != 0)
|
||||
|
||||
def multiply(self, attributeName, multiplier, stackingPenalties=False, penaltyGroup="default", skill=None, resist=True, *args, **kwargs):
|
||||
def multiply(self, attributeName, multiplier, stackingPenalties=False, penaltyGroup="default", skill=None, **kwargs):
|
||||
"""Multiply value of given attribute by given factor"""
|
||||
if multiplier is None: # See GH issue 397
|
||||
return
|
||||
@@ -375,6 +499,15 @@ class ModifiedAttributeDict(collections.MutableMapping):
|
||||
if skill:
|
||||
multiplier *= self.__handleSkill(skill)
|
||||
|
||||
preResMultiplier = multiplier
|
||||
resisted = False
|
||||
# Goddammit CCP, make up your mind where you want this information >.< See #1139
|
||||
if 'effect' in kwargs:
|
||||
resistFactor = ModifiedAttributeDict.getResistance(self.fit, kwargs['effect']) or 1
|
||||
if resistFactor != 1:
|
||||
resisted = True
|
||||
multiplier = (multiplier - 1) * resistFactor + 1
|
||||
|
||||
# If we're asked to do stacking penalized multiplication, append values
|
||||
# to per penalty group lists
|
||||
if stackingPenalties:
|
||||
@@ -395,53 +528,46 @@ class ModifiedAttributeDict(collections.MutableMapping):
|
||||
afflictPenal = ""
|
||||
if stackingPenalties:
|
||||
afflictPenal += "s"
|
||||
if resist:
|
||||
if resisted:
|
||||
afflictPenal += "r"
|
||||
|
||||
self.__afflict(attributeName, "%s*" % afflictPenal, multiplier, multiplier != 1)
|
||||
self.__afflict(
|
||||
attributeName, Operator.MULTIPLY, penaltyGroup if stackingPenalties else None,
|
||||
preResMultiplier, multiplier, multiplier != 1)
|
||||
|
||||
def boost(self, attributeName, boostFactor, skill=None, *args, **kwargs):
|
||||
def boost(self, attributeName, boostFactor, skill=None, **kwargs):
|
||||
"""Boost value by some percentage"""
|
||||
if skill:
|
||||
boostFactor *= self.__handleSkill(skill)
|
||||
|
||||
resist = None
|
||||
|
||||
# Goddammit CCP, make up your mind where you want this information >.< See #1139
|
||||
if 'effect' in kwargs:
|
||||
resist = ModifiedAttributeDict.getResistance(self.fit, kwargs['effect']) or 1
|
||||
boostFactor *= resist
|
||||
|
||||
# We just transform percentage boost into multiplication factor
|
||||
self.multiply(attributeName, 1 + boostFactor / 100.0, resist=(True if resist else False), *args, **kwargs)
|
||||
self.multiply(attributeName, 1 + boostFactor / 100.0, **kwargs)
|
||||
|
||||
def force(self, attributeName, value):
|
||||
def force(self, attributeName, value, **kwargs):
|
||||
"""Force value to attribute and prohibit any changes to it"""
|
||||
self.__forced[attributeName] = value
|
||||
self.__placehold(attributeName)
|
||||
self.__afflict(attributeName, "\u2263", value)
|
||||
self.__afflict(attributeName, Operator.FORCE, None, value, value)
|
||||
|
||||
@staticmethod
|
||||
def getResistance(fit, effect):
|
||||
remoteResistID = effect.resistanceID
|
||||
|
||||
# If it doesn't exist on the effect, check the modifying modules attributes. If it's there, set it on the
|
||||
# effect for this session so that we don't have to look here again (won't always work when it's None, but
|
||||
# will catch most)
|
||||
# Resistances are applicable only to projected effects
|
||||
if isinstance(effect.type, (tuple, list)):
|
||||
effectType = effect.type
|
||||
else:
|
||||
effectType = (effect.type,)
|
||||
if 'projected' not in effectType:
|
||||
return 1
|
||||
remoteResistID = getResistanceAttrID(modifyingItem=fit.getModifier(), effect=effect)
|
||||
if not remoteResistID:
|
||||
mod = fit.getModifier()
|
||||
effect.resistanceID = int(mod.getModifiedItemAttr("remoteResistanceID")) or None
|
||||
remoteResistID = effect.resistanceID
|
||||
|
||||
return 1
|
||||
attrInfo = getAttributeInfo(remoteResistID)
|
||||
|
||||
# Get the attribute of the resist
|
||||
resist = fit.ship.itemModifiedAttributes[attrInfo.attributeName] or None
|
||||
|
||||
return resist or 1.0
|
||||
return resist or 1
|
||||
|
||||
|
||||
class Affliction(object):
|
||||
class Affliction:
|
||||
def __init__(self, affliction_type, amount):
|
||||
self.type = affliction_type
|
||||
self.amount = amount
|
||||
|
||||
@@ -122,7 +122,7 @@ class Booster(HandledItem, ItemAttrShortcut):
|
||||
(effect.isType("passive") or effect.isType("boosterSideEffect")):
|
||||
if effect.isType("boosterSideEffect") and effect not in self.activeSideEffectEffects:
|
||||
continue
|
||||
effect.handler(fit, self, ("booster",))
|
||||
effect.handler(fit, self, ("booster",), None, effect=effect)
|
||||
|
||||
@validates("ID", "itemID", "ammoID", "active")
|
||||
def validator(self, key, val):
|
||||
|
||||
@@ -21,10 +21,13 @@ from logbook import Logger
|
||||
|
||||
from sqlalchemy.orm import reconstructor
|
||||
|
||||
from eos.utils.round import roundToPrec
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class BoosterSideEffect(object):
|
||||
class BoosterSideEffect:
|
||||
|
||||
def __init__(self, effect):
|
||||
"""Initialize from the program"""
|
||||
@@ -56,9 +59,8 @@ class BoosterSideEffect(object):
|
||||
@property
|
||||
def name(self):
|
||||
return "{0}% {1}".format(
|
||||
self.booster.getModifiedItemAttr(self.attr),
|
||||
self.__effect.getattr('displayName') or self.__effect.name,
|
||||
)
|
||||
roundToPrec(self.booster.getModifiedItemAttr(self.attr), 5),
|
||||
self.__effect.getattr('displayName') or self.__effect.name)
|
||||
|
||||
@property
|
||||
def attr(self):
|
||||
|
||||
@@ -32,7 +32,7 @@ from eos.effectHandlerHelpers import HandledItem, HandledImplantList
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class Character(object):
|
||||
class Character:
|
||||
__itemList = None
|
||||
__itemIDMap = None
|
||||
__itemNameMap = None
|
||||
@@ -422,7 +422,7 @@ class Skill(HandledItem):
|
||||
(not fit.isStructure or effect.isType("structure")) and \
|
||||
effect.activeByDefault:
|
||||
try:
|
||||
effect.handler(fit, self, ("skill",))
|
||||
effect.handler(fit, self, ("skill",), None, effect=effect)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
|
||||
@@ -18,21 +18,174 @@
|
||||
# ===============================================================================
|
||||
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
|
||||
from sqlalchemy.orm import reconstructor
|
||||
|
||||
import eos.db
|
||||
|
||||
|
||||
class DamagePattern(object):
|
||||
DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive")
|
||||
# Order is significant here - UI uses order as-is for built-in patterns
|
||||
BUILTINS = OrderedDict([
|
||||
(-1, ('Uniform', 25, 25, 25, 25)),
|
||||
(-2, ('[Generic]EM', 1, 0, 0, 0)),
|
||||
(-3, ('[Generic]Thermal', 0, 1, 0, 0)),
|
||||
(-4, ('[Generic]Kinetic', 0, 0, 1, 0)),
|
||||
(-5, ('[Generic]Explosive', 0, 0, 0, 1)),
|
||||
(-6, ('[Frequency Crystals]|[T2] Aurora', 5, 3, 0, 0)),
|
||||
(-7, ('[Frequency Crystals]|[T2] Scorch', 9, 2, 0, 0)),
|
||||
(-8, ('[Frequency Crystals]Radio', 5, 0, 0, 0)),
|
||||
(-9, ('[Frequency Crystals]Microwave', 4, 2, 0, 0)),
|
||||
(-10, ('[Frequency Crystals]Infrared', 5, 2, 0, 0)),
|
||||
(-11, ('[Frequency Crystals]Standard', 5, 3, 0, 0)),
|
||||
(-12, ('[Frequency Crystals]Ultraviolet', 6, 3, 0, 0)),
|
||||
(-13, ('[Frequency Crystals]Xray', 6, 4, 0, 0)),
|
||||
(-14, ('[Frequency Crystals]Gamma', 7, 4, 0, 0)),
|
||||
(-15, ('[Frequency Crystals]Multifrequency', 7, 5, 0, 0)),
|
||||
(-16, ('[Frequency Crystals]|[T2] Gleam', 7, 7, 0, 0)),
|
||||
(-17, ('[Frequency Crystals]|[T2] Conflagration', 7.7, 7.7, 0, 0)),
|
||||
# Different sizes of plasma do different damage ratios, the values here
|
||||
# are average of ratios across sizes
|
||||
(-18, ('[Exotic Plasma]|[T2] Mystic', 0, 66319, 0, 33681)),
|
||||
(-19, ('[Exotic Plasma]Meson', 0, 60519, 0, 39481)),
|
||||
(-20, ('[Exotic Plasma]Baryon', 0, 59737, 0, 40263)),
|
||||
(-21, ('[Exotic Plasma]Tetryon', 0, 69208, 0, 30792)),
|
||||
(-22, ('[Exotic Plasma]|[T2] Occult', 0, 55863, 0, 44137)),
|
||||
(-23, ('[Hybrid Charges]|[T2] Spike', 0, 4, 4, 0)),
|
||||
(-24, ('[Hybrid Charges]|[T2] Null', 0, 6, 5, 0)),
|
||||
(-25, ('[Hybrid Charges]Iron', 0, 2, 3, 0)),
|
||||
(-26, ('[Hybrid Charges]Tungsten', 0, 2, 4, 0)),
|
||||
(-27, ('[Hybrid Charges]Iridium', 0, 3, 4, 0)),
|
||||
(-28, ('[Hybrid Charges]Lead', 0, 3, 5, 0)),
|
||||
(-29, ('[Hybrid Charges]Thorium', 0, 4, 5, 0)),
|
||||
(-30, ('[Hybrid Charges]Uranium', 0, 4, 6, 0)),
|
||||
(-31, ('[Hybrid Charges]Plutonium', 0, 5, 6, 0)),
|
||||
(-32, ('[Hybrid Charges]Antimatter', 0, 5, 7, 0)),
|
||||
(-33, ('[Hybrid Charges]|[T2] Javelin', 0, 8, 6, 0)),
|
||||
(-34, ('[Hybrid Charges]|[T2] Void', 0, 7.7, 7.7, 0)),
|
||||
(-35, ('[Projectile Ammo]|[T2] Tremor', 0, 0, 3, 5)),
|
||||
(-36, ('[Projectile Ammo]|[T2] Barrage', 0, 0, 5, 6)),
|
||||
(-37, ('[Projectile Ammo]Carbonized Lead', 0, 0, 4, 1)),
|
||||
(-38, ('[Projectile Ammo]Nuclear', 0, 0, 1, 4)),
|
||||
(-39, ('[Projectile Ammo]Proton', 3, 0, 2, 0)),
|
||||
(-40, ('[Projectile Ammo]Depleted Uranium', 0, 3, 2, 3)),
|
||||
(-41, ('[Projectile Ammo]Titanium Sabot', 0, 0, 6, 2)),
|
||||
(-42, ('[Projectile Ammo]EMP', 9, 0, 1, 2)),
|
||||
(-43, ('[Projectile Ammo]Phased Plasma', 0, 10, 2, 0)),
|
||||
(-44, ('[Projectile Ammo]Fusion', 0, 0, 2, 10)),
|
||||
(-45, ('[Projectile Ammo]|[T2] Quake', 0, 0, 5, 9)),
|
||||
(-46, ('[Projectile Ammo]|[T2] Hail', 0, 0, 3.3, 12.1)),
|
||||
(-47, ('[Missiles]Mjolnir', 1, 0, 0, 0)),
|
||||
(-48, ('[Missiles]Inferno', 0, 1, 0, 0)),
|
||||
(-49, ('[Missiles]Scourge', 0, 0, 1, 0)),
|
||||
(-50, ('[Missiles]Nova', 0, 0, 0, 1)),
|
||||
(-51, ('[Bombs]Electron Bomb', 6400, 0, 0, 0)),
|
||||
(-52, ('[Bombs]Scorch Bomb', 0, 6400, 0, 0)),
|
||||
(-53, ('[Bombs]Concussion Bomb', 0, 0, 6400, 0)),
|
||||
(-54, ('[Bombs]Shrapnel Bomb', 0, 0, 0, 6400)),
|
||||
# Source: ticket #2067
|
||||
(-55, ('[NPC][Abyssal]All', 130, 396, 258, 216)),
|
||||
(-56, ('[NPC][Abyssal]Drifter', 250, 250, 250, 250)),
|
||||
(-57, ('[NPC][Abyssal]Drones', 250, 250, 250, 250)),
|
||||
(-58, ('[NPC][Abyssal]Overmind', 0, 408, 592, 0)),
|
||||
(-59, ('[NPC][Abyssal]Seeker', 406, 406, 94, 94)),
|
||||
(-60, ('[NPC][Abyssal]Sleeper', 313, 313, 187, 187)),
|
||||
(-61, ('[NPC][Abyssal]Triglavian', 0, 610, 0, 390)),
|
||||
(-62, ('[NPC][Asteroid]Angel Cartel', 1838, 562, 2215, 3838)),
|
||||
(-63, ('[NPC][Asteroid]Blood Raiders', 5067, 4214, 0, 0)),
|
||||
(-64, ('[NPC][Asteroid]Guristas', 0, 1828, 7413, 0)),
|
||||
(-65, ('[NPC][Asteroid]Rogue Drone', 394, 666, 1090, 1687)),
|
||||
(-66, ('[NPC][Asteroid]Sanshas Nation', 5586, 4112, 0, 0)),
|
||||
(-67, ('[NPC][Asteroid]Serpentis', 0, 5373, 4813, 0)),
|
||||
(-68, ('[NPC][Burner]Cruor (Blood Raiders)', 90, 90, 0, 0)),
|
||||
(-69, ('[NPC][Burner]Dramiel (Angel)', 55, 0, 20, 96)),
|
||||
(-70, ('[NPC][Burner]Daredevil (Serpentis)', 0, 110, 154, 0)),
|
||||
(-71, ('[NPC][Burner]Succubus (Sanshas Nation)', 135, 30, 0, 0)),
|
||||
(-72, ('[NPC][Burner]Worm (Guristas)', 0, 0, 228, 0)),
|
||||
(-73, ('[NPC][Burner]Enyo', 0, 147, 147, 0)),
|
||||
(-74, ('[NPC][Burner]Hawk', 0, 0, 247, 0)),
|
||||
(-75, ('[NPC][Burner]Jaguar', 36, 0, 50, 182)),
|
||||
(-76, ('[NPC][Burner]Vengeance', 232, 0, 0, 0)),
|
||||
(-77, ('[NPC][Burner]Ashimmu (Blood Raiders)', 260, 100, 0, 0)),
|
||||
(-78, ('[NPC][Burner]Talos', 0, 413, 413, 0)),
|
||||
(-79, ('[NPC][Burner]Sentinel', 0, 75, 0, 90)),
|
||||
(-80, ('[NPC][Deadspace]Angel Cartel', 369, 533, 1395, 3302)),
|
||||
(-81, ('[NPC][Deadspace]Blood Raiders', 6040, 5052, 10, 15)),
|
||||
(-82, ('[NPC][Deadspace]Guristas', 0, 1531, 9680, 0)),
|
||||
(-83, ('[NPC][Deadspace]Rogue Drone', 276, 1071, 1069, 871)),
|
||||
(-84, ('[NPC][Deadspace]Sanshas Nation', 3009, 2237, 0, 0)),
|
||||
(-85, ('[NPC][Deadspace]Serpentis', 0, 3110, 1929, 0)),
|
||||
# Source: ticket #2067
|
||||
(-86, ('[NPC][Invasion][Invading Precursor Entities]Dread', 0, 417, 0, 583)),
|
||||
(-87, ('[NPC][Invasion][Invading Precursor Entities]Normal Subcaps', 0, 610, 0, 390)),
|
||||
(-88, ('[NPC][Invasion][Invading Precursor Entities]Subcaps w/missiles 0% spool up', 367, 155, 367, 112)),
|
||||
(-89, ('[NPC][Invasion][Invading Precursor Entities]Subcaps w/missiles 50% spool up', 291, 243, 291, 175)),
|
||||
(-90, ('[NPC][Invasion][Invading Precursor Entities]Subcaps w/missiles 100% spool up', 241, 301, 241, 217)),
|
||||
(-91, ('[NPC][Invasion][Retaliating Amarr Entities]Dread/Subcaps', 583, 417, 0, 0)),
|
||||
(-92, ('[NPC][Invasion][Retaliating Caldari Entities]Dread', 1000, 0, 0, 0)),
|
||||
(-93, ('[NPC][Invasion][Retaliating Caldari Entities]Subcaps', 511, 21, 29, 440)),
|
||||
(-94, ('[NPC][Invasion][Retaliating Gallente Entities]Dread/Subcaps', 0, 417, 583, 0)),
|
||||
(-95, ('[NPC][Invasion][Retaliating Minmatar Entities]Dread', 0, 0, 583, 417)),
|
||||
(-96, ('[NPC][Invasion][Retaliating Minmatar Entities]Subcaps', 302, 136, 328, 234)),
|
||||
(-97, ('[NPC][Mission]Amarr Empire', 4464, 3546, 97, 0)),
|
||||
(-98, ('[NPC][Mission]Caldari State', 0, 2139, 4867, 0)),
|
||||
(-99, ('[NPC][Mission]CONCORD', 336, 134, 212, 412)),
|
||||
(-100, ('[NPC][Mission]Gallente Federation', 9, 3712, 2758, 0)),
|
||||
(-101, ('[NPC][Mission]Khanid', 612, 483, 43, 6)),
|
||||
(-102, ('[NPC][Mission]Minmatar Republic', 1024, 388, 1655, 4285)),
|
||||
(-103, ('[NPC][Mission]Mordus Legion', 25, 262, 625, 0)),
|
||||
(-104, ('[NPC][Mission]Thukker', 0, 52, 10, 79)),
|
||||
(-105, ('[NPC]Sansha Incursion', 1682, 1347, 3678, 3678)),
|
||||
(-106, ('[NPC]Sleepers', 1472, 1472, 1384, 1384))])
|
||||
|
||||
|
||||
class DamagePattern:
|
||||
|
||||
DAMAGE_TYPES = ('em', 'thermal', 'kinetic', 'explosive')
|
||||
_builtins = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.builtin = False
|
||||
self.update(*args, **kwargs)
|
||||
|
||||
@reconstructor
|
||||
def init(self):
|
||||
self.builtin = False
|
||||
|
||||
def update(self, emAmount=25, thermalAmount=25, kineticAmount=25, explosiveAmount=25):
|
||||
self.emAmount = emAmount
|
||||
self.thermalAmount = thermalAmount
|
||||
self.kineticAmount = kineticAmount
|
||||
self.explosiveAmount = explosiveAmount
|
||||
|
||||
@classmethod
|
||||
def getBuiltinList(cls):
|
||||
if cls._builtins is None:
|
||||
cls.__generateBuiltins()
|
||||
return list(cls._builtins.values())
|
||||
|
||||
@classmethod
|
||||
def getBuiltinById(cls, id):
|
||||
if cls._builtins is None:
|
||||
cls.__generateBuiltins()
|
||||
return cls._builtins.get(id)
|
||||
|
||||
@classmethod
|
||||
def getDefaultBuiltin(cls):
|
||||
if cls._builtins is None:
|
||||
cls.__generateBuiltins()
|
||||
return cls._builtins.get(-1)
|
||||
|
||||
@classmethod
|
||||
def __generateBuiltins(cls):
|
||||
cls._builtins = OrderedDict()
|
||||
for id, (rawName, em, therm, kin, explo) in BUILTINS.items():
|
||||
pattern = DamagePattern(emAmount=em, thermalAmount=therm, kineticAmount=kin, explosiveAmount=explo)
|
||||
pattern.ID = id
|
||||
pattern.rawName = rawName
|
||||
pattern.builtin = True
|
||||
cls._builtins[id] = pattern
|
||||
|
||||
def calculateEhp(self, fit):
|
||||
ehp = {}
|
||||
for (type, attr) in (('shield', 'shieldCapacity'), ('armor', 'armorHP'), ('hull', 'hp')):
|
||||
@@ -78,6 +231,15 @@ class DamagePattern(object):
|
||||
"exp" : "explosive"
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def oneType(cls, damageType, amount=100):
|
||||
pattern = DamagePattern()
|
||||
pattern.update(amount if damageType == "em" else 0,
|
||||
amount if damageType == "thermal" else 0,
|
||||
amount if damageType == "kinetic" else 0,
|
||||
amount if damageType == "explosive" else 0)
|
||||
return pattern
|
||||
|
||||
@classmethod
|
||||
def importPatterns(cls, text):
|
||||
lines = re.split('[\n\r]+', text)
|
||||
@@ -89,7 +251,7 @@ class DamagePattern(object):
|
||||
lookup = {}
|
||||
current = eos.db.getDamagePatternList()
|
||||
for pattern in current:
|
||||
lookup[pattern.name] = pattern
|
||||
lookup[pattern.rawName] = pattern
|
||||
|
||||
for line in lines:
|
||||
try:
|
||||
@@ -98,6 +260,8 @@ class DamagePattern(object):
|
||||
line = line.split('#', 1)[0] # allows for comments
|
||||
type, data = line.rsplit('=', 1)
|
||||
type, data = type.strip(), data.split(',')
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
# Data isn't in correct format, continue to next line
|
||||
continue
|
||||
@@ -112,6 +276,8 @@ class DamagePattern(object):
|
||||
for index, val in enumerate(data):
|
||||
try:
|
||||
fields["%sAmount" % cls.DAMAGE_TYPES[index]] = int(val)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
continue
|
||||
|
||||
@@ -122,7 +288,7 @@ class DamagePattern(object):
|
||||
eos.db.save(pattern)
|
||||
else:
|
||||
pattern = DamagePattern(**fields)
|
||||
pattern.name = name.strip()
|
||||
pattern.rawName = name.strip()
|
||||
eos.db.save(pattern)
|
||||
patterns.append(pattern)
|
||||
|
||||
@@ -138,11 +304,41 @@ class DamagePattern(object):
|
||||
out += "# Values are in following format:\n"
|
||||
out += "# DamageProfile = [name],[EM amount],[Thermal amount],[Kinetic amount],[Explosive amount]\n\n"
|
||||
for dp in patterns:
|
||||
out += cls.EXPORT_FORMAT % (dp.name, dp.emAmount, dp.thermalAmount, dp.kineticAmount, dp.explosiveAmount)
|
||||
out += cls.EXPORT_FORMAT % (dp.rawName, dp.emAmount, dp.thermalAmount, dp.kineticAmount, dp.explosiveAmount)
|
||||
|
||||
return out.strip()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.rawName
|
||||
|
||||
@property
|
||||
def fullName(self):
|
||||
categories, tail = self.__parseRawName()
|
||||
return '{}{}'.format(''.join('[{}]'.format(c) for c in categories), tail)
|
||||
|
||||
@property
|
||||
def shortName(self):
|
||||
return self.__parseRawName()[1]
|
||||
|
||||
@property
|
||||
def hierarchy(self):
|
||||
return self.__parseRawName()[0]
|
||||
|
||||
def __parseRawName(self):
|
||||
categories = []
|
||||
remainingName = self.rawName.strip() if self.rawName else ''
|
||||
while True:
|
||||
start, end = remainingName.find('['), remainingName.find(']')
|
||||
if start == -1 or end == -1:
|
||||
return categories, remainingName
|
||||
splitter = remainingName.find('|')
|
||||
if splitter != -1 and splitter == start - 1:
|
||||
return categories, remainingName[1:]
|
||||
categories.append(remainingName[start + 1:end])
|
||||
remainingName = remainingName[end + 1:].strip()
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
p = DamagePattern(self.emAmount, self.thermalAmount, self.kineticAmount, self.explosiveAmount)
|
||||
p.name = "%s copy" % self.name
|
||||
p.rawName = "%s copy" % self.rawName
|
||||
return p
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
# ===============================================================================
|
||||
|
||||
import math
|
||||
|
||||
from logbook import Logger
|
||||
from sqlalchemy.orm import reconstructor, validates
|
||||
|
||||
@@ -25,7 +26,8 @@ import eos.db
|
||||
from eos.effectHandlerHelpers import HandledCharge, HandledItem
|
||||
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
|
||||
from eos.utils.cycles import CycleInfo
|
||||
from eos.utils.stats import DmgTypes
|
||||
from eos.utils.default import DEFAULT
|
||||
from eos.utils.stats import DmgTypes, RRTypes
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
@@ -45,6 +47,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
self.amount = 0
|
||||
self.amountActive = 0
|
||||
self.projected = False
|
||||
self.projectionRange = None
|
||||
self.build()
|
||||
|
||||
@reconstructor
|
||||
@@ -68,7 +71,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
""" Build object. Assumes proper and valid item already set """
|
||||
self.__charge = None
|
||||
self.__baseVolley = None
|
||||
self.__baseRemoteReps = None
|
||||
self.__baseRRAmount = None
|
||||
self.__miningyield = None
|
||||
self.__itemModifiedAttributes = ModifiedAttributeDict()
|
||||
self.__itemModifiedAttributes.original = self.__item.attributes
|
||||
@@ -131,7 +134,14 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
def hasAmmo(self):
|
||||
return self.charge is not None
|
||||
|
||||
def getVolleyParameters(self, targetResists=None):
|
||||
def isDealingDamage(self):
|
||||
volleyParams = self.getVolleyParameters()
|
||||
for volley in volleyParams.values():
|
||||
if volley.total > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def getVolleyParameters(self, targetProfile=None):
|
||||
if not self.dealsDamage or self.amountActive <= 0:
|
||||
return {0: DmgTypes(0, 0, 0, 0)}
|
||||
if self.__baseVolley is None:
|
||||
@@ -143,17 +153,17 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
|
||||
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
|
||||
volley = DmgTypes(
|
||||
em=self.__baseVolley.em * (1 - getattr(targetResists, "emAmount", 0)),
|
||||
thermal=self.__baseVolley.thermal * (1 - getattr(targetResists, "thermalAmount", 0)),
|
||||
kinetic=self.__baseVolley.kinetic * (1 - getattr(targetResists, "kineticAmount", 0)),
|
||||
explosive=self.__baseVolley.explosive * (1 - getattr(targetResists, "explosiveAmount", 0)))
|
||||
em=self.__baseVolley.em * (1 - getattr(targetProfile, "emAmount", 0)),
|
||||
thermal=self.__baseVolley.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)),
|
||||
kinetic=self.__baseVolley.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)),
|
||||
explosive=self.__baseVolley.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)))
|
||||
return {0: volley}
|
||||
|
||||
def getVolley(self, targetResists=None):
|
||||
return self.getVolleyParameters(targetResists=targetResists)[0]
|
||||
def getVolley(self, targetProfile=None):
|
||||
return self.getVolleyParameters(targetProfile=targetProfile)[0]
|
||||
|
||||
def getDps(self, targetResists=None):
|
||||
volley = self.getVolley(targetResists=targetResists)
|
||||
def getDps(self, targetProfile=None):
|
||||
volley = self.getVolley(targetProfile=targetProfile)
|
||||
if not volley:
|
||||
return DmgTypes(0, 0, 0, 0)
|
||||
cycleParams = self.getCycleParameters()
|
||||
@@ -167,41 +177,52 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
explosive=volley.explosive * dpsFactor)
|
||||
return dps
|
||||
|
||||
def isRemoteRepping(self, ignoreState=False):
|
||||
repParams = self.getRepAmountParameters(ignoreState=ignoreState)
|
||||
for rrData in repParams.values():
|
||||
if rrData:
|
||||
return True
|
||||
return False
|
||||
|
||||
def getRepAmountParameters(self, ignoreState=False):
|
||||
amount = self.amount if ignoreState else self.amountActive
|
||||
if amount <= 0:
|
||||
return {}
|
||||
if self.__baseRRAmount is None:
|
||||
self.__baseRRAmount = {}
|
||||
hullAmount = self.getModifiedItemAttr("structureDamageAmount", 0)
|
||||
armorAmount = self.getModifiedItemAttr("armorDamageAmount", 0)
|
||||
shieldAmount = self.getModifiedItemAttr("shieldBonus", 0)
|
||||
if shieldAmount:
|
||||
self.__baseRRAmount[0] = RRTypes(
|
||||
shield=shieldAmount * amount,
|
||||
armor=0, hull=0, capacitor=0)
|
||||
if armorAmount or hullAmount:
|
||||
self.__baseRRAmount[self.cycleTime] = RRTypes(
|
||||
shield=0, armor=armorAmount * amount,
|
||||
hull=hullAmount * amount, capacitor=0)
|
||||
return self.__baseRRAmount
|
||||
|
||||
def getRemoteReps(self, ignoreState=False):
|
||||
rrDuringCycle = RRTypes(0, 0, 0, 0)
|
||||
cycleParams = self.getCycleParameters()
|
||||
if cycleParams is None:
|
||||
return rrDuringCycle
|
||||
repAmountParams = self.getRepAmountParameters(ignoreState=ignoreState)
|
||||
avgCycleTime = cycleParams.averageTime
|
||||
if len(repAmountParams) == 0 or avgCycleTime == 0:
|
||||
return rrDuringCycle
|
||||
for rrAmount in repAmountParams.values():
|
||||
rrDuringCycle += rrAmount
|
||||
rrFactor = 1 / (avgCycleTime / 1000)
|
||||
rrDuringCycle *= rrFactor
|
||||
return rrDuringCycle
|
||||
|
||||
def getCycleParameters(self, reloadOverride=None):
|
||||
cycleTime = self.cycleTime
|
||||
if cycleTime == 0:
|
||||
return None
|
||||
return CycleInfo(self.cycleTime, 0, math.inf)
|
||||
|
||||
def getRemoteReps(self, ignoreState=False):
|
||||
if self.amountActive <= 0 and not ignoreState:
|
||||
return (None, 0)
|
||||
if self.__baseRemoteReps is None:
|
||||
rrShield = self.getModifiedItemAttr("shieldBonus", 0)
|
||||
rrArmor = self.getModifiedItemAttr("armorDamageAmount", 0)
|
||||
rrHull = self.getModifiedItemAttr("structureDamageAmount", 0)
|
||||
if rrShield:
|
||||
rrType = "Shield"
|
||||
rrAmount = rrShield
|
||||
elif rrArmor:
|
||||
rrType = "Armor"
|
||||
rrAmount = rrArmor
|
||||
elif rrHull:
|
||||
rrType = "Hull"
|
||||
rrAmount = rrHull
|
||||
else:
|
||||
rrType = None
|
||||
rrAmount = 0
|
||||
if rrAmount:
|
||||
droneAmount = self.amount if ignoreState else self.amountActive
|
||||
cycleParams = self.getCycleParameters()
|
||||
if cycleParams is None:
|
||||
rrType = None
|
||||
rrAmount = 0
|
||||
else:
|
||||
rrAmount *= droneAmount / (cycleParams.averageTime / 1000)
|
||||
self.__baseRemoteReps = (rrType, rrAmount)
|
||||
return self.__baseRemoteReps
|
||||
return CycleInfo(self.cycleTime, 0, math.inf, False)
|
||||
|
||||
@property
|
||||
def miningStats(self):
|
||||
@@ -263,7 +284,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
|
||||
def clear(self):
|
||||
self.__baseVolley = None
|
||||
self.__baseRemoteReps = None
|
||||
self.__baseRRAmount = None
|
||||
self.__miningyield = None
|
||||
self.itemModifiedAttributes.clear()
|
||||
self.chargeModifiedAttributes.clear()
|
||||
@@ -286,7 +307,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
else:
|
||||
return True
|
||||
|
||||
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False):
|
||||
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False, forcedProjRange=DEFAULT):
|
||||
if self.projected or forceProjected:
|
||||
context = "projected", "drone"
|
||||
projected = True
|
||||
@@ -294,6 +315,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
context = ("drone",)
|
||||
projected = False
|
||||
|
||||
projectionRange = self.projectionRange if forcedProjRange is DEFAULT else forcedProjRange
|
||||
|
||||
for effect in self.item.effects.values():
|
||||
if effect.runTime == runTime and \
|
||||
effect.activeByDefault and \
|
||||
@@ -301,30 +324,34 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
projected is False and effect.isType("passive")):
|
||||
# See GH issue #765
|
||||
if effect.getattr('grouped'):
|
||||
effect.handler(fit, self, context)
|
||||
effect.handler(fit, self, context, projectionRange, effect=effect)
|
||||
else:
|
||||
i = 0
|
||||
while i != self.amountActive:
|
||||
effect.handler(fit, self, context)
|
||||
effect.handler(fit, self, context, projectionRange, effect=effect)
|
||||
i += 1
|
||||
|
||||
if self.charge:
|
||||
for effect in self.charge.effects.values():
|
||||
if effect.runTime == runTime and effect.activeByDefault:
|
||||
effect.handler(fit, self, ("droneCharge",))
|
||||
effect.handler(fit, self, ("droneCharge",), projectionRange, effect=effect)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
copy = Drone(self.item)
|
||||
copy.amount = self.amount
|
||||
copy.amountActive = self.amountActive
|
||||
copy.projectionRange = self.projectionRange
|
||||
return copy
|
||||
|
||||
def rebase(self, item):
|
||||
amount = self.amount
|
||||
amountActive = self.amountActive
|
||||
projectionRange = self.projectionRange
|
||||
|
||||
Drone.__init__(self, item)
|
||||
self.amount = amount
|
||||
self.amountActive = amountActive
|
||||
self.projectionRange = projectionRange
|
||||
|
||||
def fits(self, fit):
|
||||
fitDroneGroupLimits = set()
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
# ===============================================================================
|
||||
|
||||
import math
|
||||
|
||||
from logbook import Logger
|
||||
from sqlalchemy.orm import reconstructor, validates
|
||||
|
||||
@@ -27,8 +28,9 @@ from eos.effectHandlerHelpers import HandledCharge, HandledItem
|
||||
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
|
||||
from eos.saveddata.fighterAbility import FighterAbility
|
||||
from eos.utils.cycles import CycleInfo, CycleSequence
|
||||
from eos.utils.stats import DmgTypes
|
||||
from eos.utils.default import DEFAULT
|
||||
from eos.utils.float import floatUnerr
|
||||
from eos.utils.stats import DmgTypes
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
@@ -47,11 +49,12 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
|
||||
self.itemID = item.ID if item is not None else None
|
||||
self.projected = False
|
||||
self.projectionRange = None
|
||||
self.active = True
|
||||
|
||||
# -1 is a placeholder that represents max squadron size, which we may not know yet as ships may modify this with
|
||||
# their effects. If user changes this, it is then overridden with user value.
|
||||
self.amount = -1
|
||||
self._amount = -1
|
||||
|
||||
self.__abilities = self.__getAbilities()
|
||||
|
||||
@@ -96,7 +99,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
self.__itemModifiedAttributes = ModifiedAttributeDict()
|
||||
self.__chargeModifiedAttributes = ModifiedAttributeDict()
|
||||
|
||||
if len(self.abilities) != len(self.item.effects):
|
||||
if {a.effectID for a in self.abilities} != {e.ID for e in self.item.effects.values()}:
|
||||
self.__abilities = []
|
||||
for ability in self.__getAbilities():
|
||||
self.__abilities.append(ability)
|
||||
@@ -136,12 +139,15 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
return self.__slot
|
||||
|
||||
@property
|
||||
def amountActive(self):
|
||||
return int(self.getModifiedItemAttr("fighterSquadronMaxSize")) if self.amount == -1 else self.amount
|
||||
def amount(self):
|
||||
return int(self.getModifiedItemAttr("fighterSquadronMaxSize")) if self._amount == -1 else self._amount
|
||||
|
||||
@amountActive.setter
|
||||
def amountActive(self, i):
|
||||
self.amount = int(max(min(i, self.getModifiedItemAttr("fighterSquadronMaxSize")), 0))
|
||||
@amount.setter
|
||||
def amount(self, amount):
|
||||
amount = max(0, int(amount))
|
||||
if amount >= self.getModifiedItemAttr("fighterSquadronMaxSize"):
|
||||
amount = -1
|
||||
self._amount = amount
|
||||
|
||||
@property
|
||||
def fighterSquadronMaxSize(self):
|
||||
@@ -175,8 +181,16 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
def hasAmmo(self):
|
||||
return self.charge is not None
|
||||
|
||||
def getVolleyParametersPerEffect(self, targetResists=None):
|
||||
if not self.active or self.amountActive <= 0:
|
||||
def isDealingDamage(self):
|
||||
volleyParams = self.getVolleyParametersPerEffect()
|
||||
for effectData in volleyParams.values():
|
||||
for volley in effectData.values():
|
||||
if volley.total > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def getVolleyParametersPerEffect(self, targetProfile=None):
|
||||
if not self.active or self.amount <= 0:
|
||||
return {}
|
||||
if self.__baseVolley is None:
|
||||
self.__baseVolley = {}
|
||||
@@ -188,14 +202,21 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
adjustedVolley[effectID] = {}
|
||||
for volleyTime, volleyValue in effectData.items():
|
||||
adjustedVolley[effectID][volleyTime] = DmgTypes(
|
||||
em=volleyValue.em * (1 - getattr(targetResists, "emAmount", 0)),
|
||||
thermal=volleyValue.thermal * (1 - getattr(targetResists, "thermalAmount", 0)),
|
||||
kinetic=volleyValue.kinetic * (1 - getattr(targetResists, "kineticAmount", 0)),
|
||||
explosive=volleyValue.explosive * (1 - getattr(targetResists, "explosiveAmount", 0)))
|
||||
em=volleyValue.em * (1 - getattr(targetProfile, "emAmount", 0)),
|
||||
thermal=volleyValue.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)),
|
||||
kinetic=volleyValue.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)),
|
||||
explosive=volleyValue.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)))
|
||||
return adjustedVolley
|
||||
|
||||
def getVolley(self, targetResists=None):
|
||||
volleyParams = self.getVolleyParametersPerEffect(targetResists=targetResists)
|
||||
def getVolleyPerEffect(self, targetProfile=None):
|
||||
volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile)
|
||||
volleyMap = {}
|
||||
for effectID, volleyData in volleyParams.items():
|
||||
volleyMap[effectID] = volleyData[0]
|
||||
return volleyMap
|
||||
|
||||
def getVolley(self, targetProfile=None):
|
||||
volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile)
|
||||
em = 0
|
||||
therm = 0
|
||||
kin = 0
|
||||
@@ -207,30 +228,30 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
exp += volleyData[0].explosive
|
||||
return DmgTypes(em, therm, kin, exp)
|
||||
|
||||
def getDps(self, targetResists=None):
|
||||
def getDps(self, targetProfile=None):
|
||||
em = 0
|
||||
thermal = 0
|
||||
kinetic = 0
|
||||
explosive = 0
|
||||
for dps in self.getDpsPerEffect(targetResists=targetResists).values():
|
||||
for dps in self.getDpsPerEffect(targetProfile=targetProfile).values():
|
||||
em += dps.em
|
||||
thermal += dps.thermal
|
||||
kinetic += dps.kinetic
|
||||
explosive += dps.explosive
|
||||
return DmgTypes(em=em, thermal=thermal, kinetic=kinetic, explosive=explosive)
|
||||
|
||||
def getDpsPerEffect(self, targetResists=None):
|
||||
if not self.active or self.amountActive <= 0:
|
||||
def getDpsPerEffect(self, targetProfile=None):
|
||||
if not self.active or self.amount <= 0:
|
||||
return {}
|
||||
cycleParams = self.getCycleParametersPerEffectOptimizedDps(targetResists=targetResists)
|
||||
cycleParams = self.getCycleParametersPerEffectOptimizedDps(targetProfile=targetProfile)
|
||||
dpsMap = {}
|
||||
for ability in self.abilities:
|
||||
if ability.effectID in cycleParams:
|
||||
cycleTime = cycleParams[ability.effectID].averageTime
|
||||
dpsMap[ability.effectID] = ability.getDps(targetResists=targetResists, cycleTimeOverride=cycleTime)
|
||||
dpsMap[ability.effectID] = ability.getDps(targetProfile=targetProfile, cycleTimeOverride=cycleTime)
|
||||
return dpsMap
|
||||
|
||||
def getCycleParametersPerEffectOptimizedDps(self, targetResists=None, reloadOverride=None):
|
||||
def getCycleParametersPerEffectOptimizedDps(self, targetProfile=None, reloadOverride=None):
|
||||
cycleParamsInfinite = self.getCycleParametersPerEffectInfinite()
|
||||
cycleParamsReload = self.getCycleParametersPerEffect(reloadOverride=reloadOverride)
|
||||
dpsMapOnlyInfinite = {}
|
||||
@@ -239,25 +260,28 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
for ability in self.abilities:
|
||||
if ability.effectID in cycleParamsInfinite:
|
||||
cycleTime = cycleParamsInfinite[ability.effectID].averageTime
|
||||
dpsMapOnlyInfinite[ability.effectID] = ability.getDps(targetResists=targetResists, cycleTimeOverride=cycleTime)
|
||||
dpsMapOnlyInfinite[ability.effectID] = ability.getDps(targetProfile=targetProfile, cycleTimeOverride=cycleTime)
|
||||
if ability.effectID in cycleParamsReload:
|
||||
cycleTime = cycleParamsReload[ability.effectID].averageTime
|
||||
dpsMapAllWithReloads[ability.effectID] = ability.getDps(targetResists=targetResists, cycleTimeOverride=cycleTime)
|
||||
dpsMapAllWithReloads[ability.effectID] = ability.getDps(targetProfile=targetProfile, cycleTimeOverride=cycleTime)
|
||||
totalOnlyInfinite = sum(i.total for i in dpsMapOnlyInfinite.values())
|
||||
totalAllWithReloads = sum(i.total for i in dpsMapAllWithReloads.values())
|
||||
return cycleParamsInfinite if totalOnlyInfinite >= totalAllWithReloads else cycleParamsReload
|
||||
|
||||
def getCycleParametersPerEffectInfinite(self):
|
||||
return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.numShots == 0 and a.cycleTime > 0}
|
||||
return {
|
||||
a.effectID: CycleInfo(a.cycleTime, 0, math.inf, False)
|
||||
for a in self.abilities
|
||||
if a.numShots == 0 and a.cycleTime > 0}
|
||||
|
||||
def getCycleParametersPerEffect(self, reloadOverride=None):
|
||||
factorReload = reloadOverride if reloadOverride is not None else self.owner.factorReload
|
||||
# Assume it can cycle infinitely
|
||||
if not factorReload:
|
||||
return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.cycleTime > 0}
|
||||
return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf, False) for a in self.abilities if a.cycleTime > 0}
|
||||
limitedAbilities = [a for a in self.abilities if a.numShots > 0 and a.cycleTime > 0]
|
||||
if len(limitedAbilities) == 0:
|
||||
return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf) for a in self.abilities if a.cycleTime > 0}
|
||||
return {a.effectID: CycleInfo(a.cycleTime, 0, math.inf, False) for a in self.abilities if a.cycleTime > 0}
|
||||
validAbilities = [a for a in self.abilities if a.cycleTime > 0]
|
||||
if len(validAbilities) == 0:
|
||||
return {}
|
||||
@@ -285,13 +309,13 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
sequence = []
|
||||
if extraShotTime is not None:
|
||||
if regularShots > 0:
|
||||
sequence.append(CycleInfo(ability.cycleTime, 0, regularShots))
|
||||
sequence.append(CycleInfo(extraShotTime, refuelTime, 1))
|
||||
sequence.append(CycleInfo(ability.cycleTime, 0, regularShots, False))
|
||||
sequence.append(CycleInfo(extraShotTime, refuelTime, 1, True))
|
||||
else:
|
||||
regularShotsNonReload = regularShots - 1
|
||||
if regularShotsNonReload > 0:
|
||||
sequence.append(CycleInfo(ability.cycleTime, 0, regularShotsNonReload))
|
||||
sequence.append(CycleInfo(ability.cycleTime, refuelTime, 1))
|
||||
sequence.append(CycleInfo(ability.cycleTime, 0, regularShotsNonReload, False))
|
||||
sequence.append(CycleInfo(ability.cycleTime, refuelTime, 1, True))
|
||||
cycleParams[ability.effectID] = CycleSequence(sequence, math.inf)
|
||||
return cycleParams
|
||||
|
||||
@@ -321,7 +345,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
if falloff is not None:
|
||||
return falloff
|
||||
|
||||
@validates("ID", "itemID", "chargeID", "amount", "amountActive")
|
||||
@validates("ID", "itemID", "chargeID", "amount")
|
||||
def validator(self, key, val):
|
||||
map = {
|
||||
"ID" : lambda _val: isinstance(_val, int),
|
||||
@@ -329,7 +353,6 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
"chargeID": lambda _val: isinstance(_val, int),
|
||||
"amount" : lambda _val: isinstance(_val, int) and _val >= -1,
|
||||
}
|
||||
|
||||
if not map[key](val):
|
||||
raise ValueError(str(val) + " is not a valid value for " + key)
|
||||
else:
|
||||
@@ -360,7 +383,7 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
else:
|
||||
return True
|
||||
|
||||
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False):
|
||||
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False, forcedProjRange=DEFAULT):
|
||||
if not self.active:
|
||||
return
|
||||
|
||||
@@ -371,6 +394,8 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
context = ("fighter",)
|
||||
projected = False
|
||||
|
||||
projectionRange = self.projectionRange if forcedProjRange is DEFAULT else forcedProjRange
|
||||
|
||||
for ability in self.abilities:
|
||||
if not ability.active:
|
||||
continue
|
||||
@@ -379,32 +404,36 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
if effect.runTime == runTime and effect.activeByDefault and \
|
||||
((projected and effect.isType("projected")) or not projected):
|
||||
if ability.grouped:
|
||||
effect.handler(fit, self, context)
|
||||
effect.handler(fit, self, context, projectionRange, effect=effect)
|
||||
else:
|
||||
i = 0
|
||||
while i != self.amountActive:
|
||||
effect.handler(fit, self, context)
|
||||
while i != self.amount:
|
||||
effect.handler(fit, self, context, projectionRange, effect=effect)
|
||||
i += 1
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
copy = Fighter(self.item)
|
||||
copy.amount = self.amount
|
||||
copy._amount = self._amount
|
||||
copy.active = self.active
|
||||
for ability in self.abilities:
|
||||
copyAbility = next(filter(lambda a: a.effectID == ability.effectID, copy.abilities))
|
||||
copyAbility.active = ability.active
|
||||
copy.projectionRange = self.projectionRange
|
||||
return copy
|
||||
|
||||
def rebase(self, item):
|
||||
amount = self.amount
|
||||
amount = self._amount
|
||||
active = self.active
|
||||
abilityEffectStates = {a.effectID: a.active for a in self.abilities}
|
||||
projectionRange = self.projectionRange
|
||||
|
||||
Fighter.__init__(self, item)
|
||||
self.amount = amount
|
||||
self._amount = amount
|
||||
self.active = active
|
||||
for ability in self.abilities:
|
||||
if ability.effectID in abilityEffectStates:
|
||||
ability.active = abilityEffectStates[ability.effectID]
|
||||
self.projectionRange = projectionRange
|
||||
|
||||
def fits(self, fit):
|
||||
# If ships doesn't support this type of fighter, don't add it
|
||||
|
||||
@@ -26,7 +26,7 @@ from eos.utils.stats import DmgTypes
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class FighterAbility(object):
|
||||
class FighterAbility:
|
||||
|
||||
# We aren't able to get data on the charges that can be stored with fighters. So we hardcode that data here, keyed
|
||||
# with the fighter squadron role
|
||||
@@ -114,7 +114,7 @@ class FighterAbility(object):
|
||||
speed = self.fighter.getModifiedItemAttr("{}Duration".format(self.attrPrefix))
|
||||
return speed
|
||||
|
||||
def getVolley(self, targetResists=None):
|
||||
def getVolley(self, targetProfile=None):
|
||||
if not self.dealsDamage or not self.active:
|
||||
return DmgTypes(0, 0, 0, 0)
|
||||
if self.attrPrefix == "fighterAbilityLaunchBomb":
|
||||
@@ -127,16 +127,16 @@ class FighterAbility(object):
|
||||
therm = self.fighter.getModifiedItemAttr("{}DamageTherm".format(self.attrPrefix), 0)
|
||||
kin = self.fighter.getModifiedItemAttr("{}DamageKin".format(self.attrPrefix), 0)
|
||||
exp = self.fighter.getModifiedItemAttr("{}DamageExp".format(self.attrPrefix), 0)
|
||||
dmgMult = self.fighter.amountActive * self.fighter.getModifiedItemAttr("{}DamageMultiplier".format(self.attrPrefix), 1)
|
||||
dmgMult = self.fighter.amount * self.fighter.getModifiedItemAttr("{}DamageMultiplier".format(self.attrPrefix), 1)
|
||||
volley = DmgTypes(
|
||||
em=em * dmgMult * (1 - getattr(targetResists, "emAmount", 0)),
|
||||
thermal=therm * dmgMult * (1 - getattr(targetResists, "thermalAmount", 0)),
|
||||
kinetic=kin * dmgMult * (1 - getattr(targetResists, "kineticAmount", 0)),
|
||||
explosive=exp * dmgMult * (1 - getattr(targetResists, "explosiveAmount", 0)))
|
||||
em=em * dmgMult * (1 - getattr(targetProfile, "emAmount", 0)),
|
||||
thermal=therm * dmgMult * (1 - getattr(targetProfile, "thermalAmount", 0)),
|
||||
kinetic=kin * dmgMult * (1 - getattr(targetProfile, "kineticAmount", 0)),
|
||||
explosive=exp * dmgMult * (1 - getattr(targetProfile, "explosiveAmount", 0)))
|
||||
return volley
|
||||
|
||||
def getDps(self, targetResists=None, cycleTimeOverride=None):
|
||||
volley = self.getVolley(targetResists=targetResists)
|
||||
def getDps(self, targetProfile=None, cycleTimeOverride=None):
|
||||
volley = self.getVolley(targetProfile=targetProfile)
|
||||
if not volley:
|
||||
return DmgTypes(0, 0, 0, 0)
|
||||
cycleTime = cycleTimeOverride if cycleTimeOverride is not None else self.cycleTime
|
||||
@@ -149,5 +149,4 @@ class FighterAbility(object):
|
||||
return dps
|
||||
|
||||
def clear(self):
|
||||
self.__dps = None
|
||||
self.__volley = None
|
||||
pass
|
||||
|
||||
@@ -21,28 +21,44 @@ import datetime
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from itertools import chain
|
||||
from math import log, sqrt
|
||||
|
||||
from logbook import Logger
|
||||
from math import asinh, log, sqrt
|
||||
from sqlalchemy.orm import reconstructor, validates
|
||||
|
||||
import eos.db
|
||||
from eos import capSim
|
||||
from eos.calc import calculateLockTime, calculateMultiplier
|
||||
from eos.const import CalcType, FitSystemSecurity, FittingHardpoint, FittingModuleState, FittingSlot, ImplantLocation
|
||||
from eos.effectHandlerHelpers import (
|
||||
HandledBoosterList, HandledDroneCargoList, HandledImplantList,
|
||||
HandledModuleList, HandledProjectedDroneList, HandledProjectedModList)
|
||||
from eos.saveddata.character import Character
|
||||
from eos.saveddata.citadel import Citadel
|
||||
from eos.saveddata.damagePattern import DamagePattern
|
||||
from eos.saveddata.module import Module
|
||||
from eos.saveddata.ship import Ship
|
||||
from eos.utils.stats import DmgTypes
|
||||
from eos.saveddata.targetProfile import TargetProfile
|
||||
from eos.utils.stats import DmgTypes, RRTypes
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class Fit(object):
|
||||
class FitLite:
|
||||
|
||||
def __init__(self, id=None, name=None, shipID=None, shipName=None, shipNameShort=None):
|
||||
self.ID = id
|
||||
self.name = name
|
||||
self.shipID = shipID
|
||||
self.shipName = shipName
|
||||
self.shipNameShort = shipNameShort
|
||||
|
||||
def __repr__(self):
|
||||
return 'FitLite(ID={})'.format(self.ID)
|
||||
|
||||
|
||||
class Fit:
|
||||
"""Represents a fitting, with modules, ship, implants, etc."""
|
||||
|
||||
PEAK_RECHARGE = 0.25
|
||||
@@ -130,6 +146,7 @@ class Fit(object):
|
||||
self.__capState = None
|
||||
self.__capUsed = None
|
||||
self.__capRecharge = None
|
||||
self.__savedCapSimData = {}
|
||||
self.__calculatedTargets = []
|
||||
self.factorReload = False
|
||||
self.boostsFits = set()
|
||||
@@ -137,13 +154,40 @@ class Fit(object):
|
||||
self.ecmProjectedStr = 1
|
||||
self.commandBonuses = {}
|
||||
|
||||
@property
|
||||
def targetResists(self):
|
||||
return self.__targetResists
|
||||
def clearFactorReloadDependentData(self):
|
||||
# Here we clear all data known to rely on cycle parameters
|
||||
# (which, in turn, relies on factor reload flag)
|
||||
self.__weaponDpsMap.clear()
|
||||
self.__droneDps = None
|
||||
self.__remoteRepMap.clear()
|
||||
self.__capStable = None
|
||||
self.__capState = None
|
||||
self.__capUsed = None
|
||||
self.__capRecharge = None
|
||||
self.__savedCapSimData.clear()
|
||||
# Ancillary tank modules affect this
|
||||
self.__sustainableTank = None
|
||||
self.__effectiveSustainableTank = None
|
||||
|
||||
@targetResists.setter
|
||||
def targetResists(self, targetResists):
|
||||
self.__targetResists = targetResists
|
||||
@property
|
||||
def targetProfile(self):
|
||||
if self.__userTargetProfile is not None:
|
||||
return self.__userTargetProfile
|
||||
if self.__builtinTargetProfileID is not None:
|
||||
return TargetProfile.getBuiltinById(self.__builtinTargetProfileID)
|
||||
return None
|
||||
|
||||
@targetProfile.setter
|
||||
def targetProfile(self, targetProfile):
|
||||
if targetProfile is None:
|
||||
self.__userTargetProfile = None
|
||||
self.__builtinTargetProfileID = None
|
||||
elif targetProfile.builtin:
|
||||
self.__userTargetProfile = None
|
||||
self.__builtinTargetProfileID = targetProfile.ID
|
||||
else:
|
||||
self.__userTargetProfile = targetProfile
|
||||
self.__builtinTargetProfileID = None
|
||||
self.__weaponDpsMap = {}
|
||||
self.__weaponVolleyMap = {}
|
||||
self.__droneDps = None
|
||||
@@ -151,11 +195,25 @@ class Fit(object):
|
||||
|
||||
@property
|
||||
def damagePattern(self):
|
||||
return self.__damagePattern
|
||||
if self.__userDamagePattern is not None:
|
||||
return self.__userDamagePattern
|
||||
if self.__builtinDamagePatternID is not None:
|
||||
pattern = DamagePattern.getBuiltinById(self.__builtinDamagePatternID)
|
||||
if pattern is not None:
|
||||
return pattern
|
||||
return DamagePattern.getDefaultBuiltin()
|
||||
|
||||
@damagePattern.setter
|
||||
def damagePattern(self, damagePattern):
|
||||
self.__damagePattern = damagePattern
|
||||
if damagePattern is None:
|
||||
self.__userDamagePattern = None
|
||||
self.__builtinDamagePatternID = None
|
||||
elif damagePattern.builtin:
|
||||
self.__userDamagePattern = None
|
||||
self.__builtinDamagePatternID = damagePattern.ID
|
||||
else:
|
||||
self.__userDamagePattern = damagePattern
|
||||
self.__builtinDamagePatternID = None
|
||||
self.__ehp = None
|
||||
self.__effectiveTank = None
|
||||
|
||||
@@ -414,10 +472,8 @@ class Fit(object):
|
||||
return False
|
||||
|
||||
# Citadel modules are now under a new category, so we can check this to ensure only structure modules can fit on a citadel
|
||||
if isinstance(self.ship, Citadel) and item.category.name != "Structure Module" or \
|
||||
not isinstance(self.ship, Citadel) and item.category.name == "Structure Module":
|
||||
if isinstance(self.ship, Citadel) is not item.isStandup:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def clear(self, projected=False, command=False):
|
||||
@@ -437,6 +493,7 @@ class Fit(object):
|
||||
self.__capState = None
|
||||
self.__capUsed = None
|
||||
self.__capRecharge = None
|
||||
self.__savedCapSimData.clear()
|
||||
self.ecmProjectedStr = 1
|
||||
# self.commandBonuses = {}
|
||||
|
||||
@@ -519,11 +576,15 @@ class Fit(object):
|
||||
|
||||
if warfareBuffID == 11: # Shield Burst: Active Shielding: Repair Duration/Capacitor
|
||||
self.modules.filteredItemBoost(
|
||||
lambda mod: mod.item.requiresSkill("Shield Operation") or mod.item.requiresSkill(
|
||||
"Shield Emission Systems"), "capacitorNeed", value)
|
||||
lambda mod: mod.item.requiresSkill("Shield Operation") or
|
||||
mod.item.requiresSkill("Shield Emission Systems") or
|
||||
mod.item.requiresSkill("Capital Shield Emission Systems"),
|
||||
"capacitorNeed", value)
|
||||
self.modules.filteredItemBoost(
|
||||
lambda mod: mod.item.requiresSkill("Shield Operation") or mod.item.requiresSkill(
|
||||
"Shield Emission Systems"), "duration", value)
|
||||
lambda mod: mod.item.requiresSkill("Shield Operation") or
|
||||
mod.item.requiresSkill("Shield Emission Systems") or
|
||||
mod.item.requiresSkill("Capital Shield Emission Systems"),
|
||||
"duration", value)
|
||||
|
||||
if warfareBuffID == 12: # Shield Burst: Shield Extension: Shield HP
|
||||
self.ship.boostItemAttr("shieldCapacity", value, stackingPenalties=True)
|
||||
@@ -533,12 +594,16 @@ class Fit(object):
|
||||
self.ship.boostItemAttr("armor%sDamageResonance" % damageType, value, stackingPenalties=True)
|
||||
|
||||
if warfareBuffID == 14: # Armor Burst: Rapid Repair: Repair Duration/Capacitor
|
||||
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or
|
||||
mod.item.requiresSkill("Repair Systems"),
|
||||
"capacitorNeed", value)
|
||||
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or
|
||||
mod.item.requiresSkill("Repair Systems"),
|
||||
"duration", value)
|
||||
self.modules.filteredItemBoost(
|
||||
lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or
|
||||
mod.item.requiresSkill("Repair Systems") or
|
||||
mod.item.requiresSkill("Capital Remote Armor Repair Systems"),
|
||||
"capacitorNeed", value)
|
||||
self.modules.filteredItemBoost(
|
||||
lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or
|
||||
mod.item.requiresSkill("Repair Systems") or
|
||||
mod.item.requiresSkill("Capital Remote Armor Repair Systems"),
|
||||
"duration", value)
|
||||
|
||||
if warfareBuffID == 15: # Armor Burst: Armor Reinforcement: Armor HP
|
||||
self.ship.boostItemAttr("armorHP", value, stackingPenalties=True)
|
||||
@@ -907,13 +972,20 @@ class Fit(object):
|
||||
To support a simpler way of doing self projections (so that we don't have to make a copy of the fit and
|
||||
recalculate), this function was developed to be a common source of projected effect application.
|
||||
"""
|
||||
c = chain(self.drones, self.fighters, self.modules)
|
||||
for item in c:
|
||||
for item in chain(self.drones, self.fighters):
|
||||
if item is not None:
|
||||
# apply effects onto target fit x amount of times
|
||||
for _ in range(projectionInfo.amount):
|
||||
targetFit.register(item, origin=self)
|
||||
item.calculateModifiedAttributes(targetFit, runTime, True)
|
||||
item.calculateModifiedAttributes(
|
||||
targetFit, runTime, forceProjected=True,
|
||||
forcedProjRange=0)
|
||||
for mod in self.modules:
|
||||
for _ in range(projectionInfo.amount):
|
||||
targetFit.register(mod, origin=self)
|
||||
mod.calculateModifiedAttributes(
|
||||
targetFit, runTime, forceProjected=True,
|
||||
forcedProjRange=projectionInfo.projectionRange)
|
||||
|
||||
def fill(self):
|
||||
"""
|
||||
@@ -954,6 +1026,16 @@ class Fit(object):
|
||||
if mod.isEmpty:
|
||||
del self.modules[i]
|
||||
|
||||
def clearTail(self):
|
||||
tailPositions = {}
|
||||
for mod in self.modules:
|
||||
if not mod.isEmpty:
|
||||
break
|
||||
tailPositions[self.modules.index(mod)] = mod.slot
|
||||
for pos in sorted(tailPositions, reverse=True):
|
||||
self.modules.remove(self.modules[pos])
|
||||
return tailPositions
|
||||
|
||||
@property
|
||||
def modCount(self):
|
||||
x = 0
|
||||
@@ -1063,7 +1145,7 @@ class Fit(object):
|
||||
def droneBayUsed(self):
|
||||
amount = 0
|
||||
for d in self.drones:
|
||||
amount += d.item.volume * d.amount
|
||||
amount += d.item.attributes['volume'].value * d.amount
|
||||
|
||||
return amount
|
||||
|
||||
@@ -1071,7 +1153,7 @@ class Fit(object):
|
||||
def fighterBayUsed(self):
|
||||
amount = 0
|
||||
for f in self.fighters:
|
||||
amount += f.item.volume * f.amountActive
|
||||
amount += f.item.attributes['volume'].value * f.amount
|
||||
|
||||
return amount
|
||||
|
||||
@@ -1167,9 +1249,15 @@ class Fit(object):
|
||||
|
||||
return self.__capRecharge
|
||||
|
||||
def calculateCapRecharge(self, percent=PEAK_RECHARGE):
|
||||
capacity = self.ship.getModifiedItemAttr("capacitorCapacity")
|
||||
rechargeRate = self.ship.getModifiedItemAttr("rechargeRate") / 1000.0
|
||||
@property
|
||||
def capDelta(self):
|
||||
return (self.__capRecharge or 0) - (self.__capUsed or 0)
|
||||
|
||||
def calculateCapRecharge(self, percent=PEAK_RECHARGE, capacity=None, rechargeRate=None):
|
||||
if capacity is None:
|
||||
capacity = self.ship.getModifiedItemAttr("capacitorCapacity")
|
||||
if rechargeRate is None:
|
||||
rechargeRate = self.ship.getModifiedItemAttr("rechargeRate") / 1000.0
|
||||
return 10 / rechargeRate * sqrt(percent) * (1 - sqrt(percent)) * capacity
|
||||
|
||||
def calculateShieldRecharge(self, percent=PEAK_RECHARGE):
|
||||
@@ -1186,8 +1274,8 @@ class Fit(object):
|
||||
# Signature reduction, uses the bomb formula as per CCP Larrikin
|
||||
if energyNeutralizerSignatureResolution:
|
||||
capNeed = capNeed * min(1, signatureRadius / energyNeutralizerSignatureResolution)
|
||||
|
||||
self.__extraDrains.append((cycleTime, capNeed, clipSize, reloadTime))
|
||||
if capNeed:
|
||||
self.__extraDrains.append((cycleTime, capNeed, clipSize, reloadTime))
|
||||
|
||||
def removeDrain(self, i):
|
||||
del self.__extraDrains[i]
|
||||
@@ -1199,29 +1287,39 @@ class Fit(object):
|
||||
drains = []
|
||||
capUsed = 0
|
||||
capAdded = 0
|
||||
for mod in self.modules:
|
||||
if mod.state >= FittingModuleState.ACTIVE:
|
||||
if (mod.getModifiedItemAttr("capacitorNeed") or 0) != 0:
|
||||
cycleTime = mod.rawCycleTime or 0
|
||||
reactivationTime = mod.getModifiedItemAttr("moduleReactivationDelay") or 0
|
||||
fullCycleTime = cycleTime + reactivationTime
|
||||
reloadTime = mod.reloadTime
|
||||
if fullCycleTime > 0:
|
||||
capNeed = mod.capUse
|
||||
if capNeed > 0:
|
||||
capUsed += capNeed
|
||||
else:
|
||||
capAdded -= capNeed
|
||||
for mod in self.activeModulesIter():
|
||||
if (mod.getModifiedItemAttr("capacitorNeed") or 0) != 0:
|
||||
cycleTime = mod.rawCycleTime or 0
|
||||
reactivationTime = mod.getModifiedItemAttr("moduleReactivationDelay") or 0
|
||||
fullCycleTime = cycleTime + reactivationTime
|
||||
reloadTime = mod.reloadTime
|
||||
if fullCycleTime > 0:
|
||||
capNeed = mod.capUse
|
||||
if capNeed > 0:
|
||||
capUsed += capNeed
|
||||
else:
|
||||
capAdded -= capNeed
|
||||
|
||||
# If this is a turret, don't stagger activations
|
||||
disableStagger = mod.hardpoint == FittingHardpoint.TURRET
|
||||
# If this is a turret, don't stagger activations
|
||||
disableStagger = mod.hardpoint == FittingHardpoint.TURRET
|
||||
|
||||
drains.append((int(fullCycleTime), mod.getModifiedItemAttr("capacitorNeed") or 0,
|
||||
mod.numShots or 0, disableStagger, reloadTime))
|
||||
drains.append((
|
||||
int(fullCycleTime),
|
||||
mod.getModifiedItemAttr("capacitorNeed") or 0,
|
||||
mod.numShots or 0,
|
||||
disableStagger,
|
||||
reloadTime,
|
||||
mod.item.group.name == 'Capacitor Booster'))
|
||||
|
||||
for fullCycleTime, capNeed, clipSize, reloadTime in self.iterDrains():
|
||||
# Stagger incoming effects for cap simulation
|
||||
drains.append((int(fullCycleTime), capNeed, clipSize, False, reloadTime))
|
||||
drains.append((
|
||||
int(fullCycleTime),
|
||||
capNeed,
|
||||
clipSize,
|
||||
# Stagger incoming effects for cap simulation
|
||||
False,
|
||||
reloadTime,
|
||||
False))
|
||||
if capNeed > 0:
|
||||
capUsed += capNeed / (fullCycleTime / 1000.0)
|
||||
else:
|
||||
@@ -1232,17 +1330,8 @@ class Fit(object):
|
||||
def simulateCap(self):
|
||||
drains, self.__capUsed, self.__capRecharge = self.__generateDrain()
|
||||
self.__capRecharge += self.calculateCapRecharge()
|
||||
if len(drains) > 0:
|
||||
sim = capSim.CapSimulator()
|
||||
sim.init(drains)
|
||||
sim.capacitorCapacity = self.ship.getModifiedItemAttr("capacitorCapacity")
|
||||
sim.capacitorRecharge = self.ship.getModifiedItemAttr("rechargeRate")
|
||||
sim.stagger = True
|
||||
sim.scale = False
|
||||
sim.t_max = 6 * 60 * 60 * 1000
|
||||
sim.reload = self.factorReload
|
||||
sim.run()
|
||||
|
||||
sim = self.__runCapSim(drains=drains)
|
||||
if sim is not None:
|
||||
capState = (sim.cap_stable_low + sim.cap_stable_high) / (2 * sim.capacitorCapacity)
|
||||
self.__capStable = capState > 0
|
||||
self.__capState = min(100, capState * 100) if self.__capStable else sim.t / 1000.0
|
||||
@@ -1250,23 +1339,55 @@ class Fit(object):
|
||||
self.__capStable = True
|
||||
self.__capState = 100
|
||||
|
||||
def getCapSimData(self, startingCap):
|
||||
if startingCap not in self.__savedCapSimData:
|
||||
self.__runCapSim(startingCap=startingCap, tMax=3600, optimizeRepeats=False)
|
||||
return self.__savedCapSimData[startingCap]
|
||||
|
||||
def __runCapSim(self, drains=None, startingCap=None, tMax=None, optimizeRepeats=True):
|
||||
if drains is None:
|
||||
drains, nil, nil = self.__generateDrain()
|
||||
if tMax is None:
|
||||
tMax = 6 * 60 * 60 * 1000
|
||||
else:
|
||||
tMax *= 1000
|
||||
if len(drains) > 0:
|
||||
sim = capSim.CapSimulator()
|
||||
sim.init(drains)
|
||||
sim.capacitorCapacity = self.ship.getModifiedItemAttr("capacitorCapacity")
|
||||
sim.capacitorRecharge = self.ship.getModifiedItemAttr("rechargeRate")
|
||||
sim.startingCapacity = startingCap = self.ship.getModifiedItemAttr("capacitorCapacity") if startingCap is None else startingCap
|
||||
sim.stagger = True
|
||||
sim.scale = False
|
||||
sim.t_max = tMax
|
||||
sim.reload = self.factorReload
|
||||
sim.optimize_repeats = optimizeRepeats
|
||||
sim.run()
|
||||
# We do not want to store partial results
|
||||
if not sim.result_optimized_repeats:
|
||||
self.__savedCapSimData[startingCap] = sim.saved_changes
|
||||
return sim
|
||||
else:
|
||||
self.__savedCapSimData[startingCap] = []
|
||||
return None
|
||||
|
||||
def getCapRegenGainFromMod(self, mod):
|
||||
"""Return how much cap regen do we gain from having this module"""
|
||||
currentRegen = self.calculateCapRecharge()
|
||||
nomodRegen = self.calculateCapRecharge(
|
||||
capacity=self.ship.getModifiedItemAttrExtended("capacitorCapacity", ignoreAfflictors=[mod]),
|
||||
rechargeRate=self.ship.getModifiedItemAttrExtended("rechargeRate", ignoreAfflictors=[mod]) / 1000.0)
|
||||
return currentRegen - nomodRegen
|
||||
|
||||
def getRemoteReps(self, spoolOptions=None):
|
||||
if spoolOptions not in self.__remoteRepMap:
|
||||
remoteReps = {}
|
||||
remoteReps = RRTypes(0, 0, 0, 0)
|
||||
|
||||
for module in self.modules:
|
||||
rrType, rrAmount = module.getRemoteReps(spoolOptions=spoolOptions)
|
||||
if rrType:
|
||||
if rrType not in remoteReps:
|
||||
remoteReps[rrType] = 0
|
||||
remoteReps[rrType] += rrAmount
|
||||
remoteReps += module.getRemoteReps(spoolOptions=spoolOptions)
|
||||
|
||||
for drone in self.drones:
|
||||
rrType, rrAmount = drone.getRemoteReps()
|
||||
if rrType:
|
||||
if rrType not in remoteReps:
|
||||
remoteReps[rrType] = 0
|
||||
remoteReps[rrType] += rrAmount
|
||||
remoteReps += drone.getRemoteReps()
|
||||
|
||||
self.__remoteRepMap[spoolOptions] = remoteReps
|
||||
|
||||
@@ -1361,47 +1482,47 @@ class Fit(object):
|
||||
for tankType in localAdjustment:
|
||||
dict = self.extraAttributes.getAfflictions(tankType)
|
||||
if self in dict:
|
||||
for mod, _, amount, used in dict[self]:
|
||||
for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in dict[self]:
|
||||
if not used:
|
||||
continue
|
||||
if mod.projected:
|
||||
if afflictor.projected:
|
||||
continue
|
||||
if mod.item.group.name not in groupAttrMap:
|
||||
if afflictor.item.group.name not in groupAttrMap:
|
||||
continue
|
||||
usesCap = True
|
||||
try:
|
||||
if mod.capUse:
|
||||
capUsed -= mod.capUse
|
||||
if afflictor.capUse:
|
||||
capUsed -= afflictor.capUse
|
||||
else:
|
||||
usesCap = False
|
||||
except AttributeError:
|
||||
usesCap = False
|
||||
|
||||
# Normal Repairers
|
||||
if usesCap and not mod.charge:
|
||||
cycleTime = mod.rawCycleTime
|
||||
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
|
||||
if usesCap and not afflictor.charge:
|
||||
cycleTime = afflictor.rawCycleTime
|
||||
amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name])
|
||||
localAdjustment[tankType] -= amount / (cycleTime / 1000.0)
|
||||
repairers.append(mod)
|
||||
repairers.append(afflictor)
|
||||
# Ancillary Armor reps etc
|
||||
elif usesCap and mod.charge:
|
||||
cycleTime = mod.rawCycleTime
|
||||
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
|
||||
if mod.charge.name == "Nanite Repair Paste":
|
||||
multiplier = mod.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
|
||||
elif usesCap and afflictor.charge:
|
||||
cycleTime = afflictor.rawCycleTime
|
||||
amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name])
|
||||
if afflictor.charge.name == "Nanite Repair Paste":
|
||||
multiplier = afflictor.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
|
||||
else:
|
||||
multiplier = 1
|
||||
localAdjustment[tankType] -= amount * multiplier / (cycleTime / 1000.0)
|
||||
repairers.append(mod)
|
||||
repairers.append(afflictor)
|
||||
# Ancillary Shield boosters etc
|
||||
elif not usesCap and mod.item.group.name in ("Ancillary Shield Booster", "Ancillary Remote Shield Booster"):
|
||||
cycleTime = mod.rawCycleTime
|
||||
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
|
||||
if self.factorReload and mod.charge:
|
||||
reloadtime = mod.reloadTime
|
||||
elif not usesCap and afflictor.item.group.name in ("Ancillary Shield Booster", "Ancillary Remote Shield Booster"):
|
||||
cycleTime = afflictor.rawCycleTime
|
||||
amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name])
|
||||
if self.factorReload and afflictor.charge:
|
||||
reloadtime = afflictor.reloadTime
|
||||
else:
|
||||
reloadtime = 0.0
|
||||
offdutycycle = reloadtime / ((max(mod.numShots, 1) * cycleTime) + reloadtime)
|
||||
offdutycycle = reloadtime / ((max(afflictor.numShots, 1) * cycleTime) + reloadtime)
|
||||
localAdjustment[tankType] -= amount * offdutycycle / (cycleTime / 1000.0)
|
||||
|
||||
# Sort repairers by efficiency. We want to use the most efficient repairers first
|
||||
@@ -1413,35 +1534,35 @@ class Fit(object):
|
||||
# Most efficient first, as we sorted earlier.
|
||||
# calculate how much the repper can rep stability & add to total
|
||||
totalPeakRecharge = self.capRecharge
|
||||
for mod in repairers:
|
||||
for afflictor in repairers:
|
||||
if capUsed > totalPeakRecharge:
|
||||
break
|
||||
|
||||
if self.factorReload and mod.charge:
|
||||
reloadtime = mod.reloadTime
|
||||
if self.factorReload and afflictor.charge:
|
||||
reloadtime = afflictor.reloadTime
|
||||
else:
|
||||
reloadtime = 0.0
|
||||
|
||||
cycleTime = mod.rawCycleTime
|
||||
capPerSec = mod.capUse
|
||||
cycleTime = afflictor.rawCycleTime
|
||||
capPerSec = afflictor.capUse
|
||||
|
||||
if capPerSec is not None and cycleTime is not None:
|
||||
# Check how much this repper can work
|
||||
sustainability = min(1, (totalPeakRecharge - capUsed) / capPerSec)
|
||||
amount = mod.getModifiedItemAttr(groupAttrMap[mod.item.group.name])
|
||||
amount = afflictor.getModifiedItemAttr(groupAttrMap[afflictor.item.group.name])
|
||||
# Add the sustainable amount
|
||||
if not mod.charge:
|
||||
localAdjustment[groupStoreMap[mod.item.group.name]] += sustainability * amount / (
|
||||
if not afflictor.charge:
|
||||
localAdjustment[groupStoreMap[afflictor.item.group.name]] += sustainability * amount / (
|
||||
cycleTime / 1000.0)
|
||||
else:
|
||||
if mod.charge.name == "Nanite Repair Paste":
|
||||
multiplier = mod.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
|
||||
if afflictor.charge.name == "Nanite Repair Paste":
|
||||
multiplier = afflictor.getModifiedItemAttr("chargedArmorDamageMultiplier") or 1
|
||||
else:
|
||||
multiplier = 1
|
||||
ondutycycle = (max(mod.numShots, 1) * cycleTime) / (
|
||||
(max(mod.numShots, 1) * cycleTime) + reloadtime)
|
||||
ondutycycle = (max(afflictor.numShots, 1) * cycleTime) / (
|
||||
(max(afflictor.numShots, 1) * cycleTime) + reloadtime)
|
||||
localAdjustment[groupStoreMap[
|
||||
mod.item.group.name]] += sustainability * amount * ondutycycle * multiplier / (
|
||||
afflictor.item.group.name]] += sustainability * amount * ondutycycle * multiplier / (
|
||||
cycleTime / 1000.0)
|
||||
|
||||
capUsed += capPerSec
|
||||
@@ -1458,9 +1579,7 @@ class Fit(object):
|
||||
def calculateLockTime(self, radius):
|
||||
scanRes = self.ship.getModifiedItemAttr("scanResolution")
|
||||
if scanRes is not None and scanRes > 0:
|
||||
# Yes, this function returns time in seconds, not miliseconds.
|
||||
# 40,000 is indeed the correct constant here.
|
||||
return min(40000 / scanRes / asinh(radius) ** 2, 30 * 60)
|
||||
return calculateLockTime(srcScanRes=scanRes, tgtSigRadius=radius)
|
||||
else:
|
||||
return self.ship.getModifiedItemAttr("scanSpeed") / 1000.0
|
||||
|
||||
@@ -1482,8 +1601,8 @@ class Fit(object):
|
||||
weaponDps = DmgTypes(0, 0, 0, 0)
|
||||
|
||||
for mod in self.modules:
|
||||
weaponVolley += mod.getVolley(spoolOptions=spoolOptions, targetResists=self.targetResists)
|
||||
weaponDps += mod.getDps(spoolOptions=spoolOptions, targetResists=self.targetResists)
|
||||
weaponVolley += mod.getVolley(spoolOptions=spoolOptions, targetProfile=self.targetProfile)
|
||||
weaponDps += mod.getDps(spoolOptions=spoolOptions, targetProfile=self.targetProfile)
|
||||
|
||||
self.__weaponVolleyMap[spoolOptions] = weaponVolley
|
||||
self.__weaponDpsMap[spoolOptions] = weaponDps
|
||||
@@ -1493,12 +1612,12 @@ class Fit(object):
|
||||
droneDps = DmgTypes(0, 0, 0, 0)
|
||||
|
||||
for drone in self.drones:
|
||||
droneVolley += drone.getVolley(targetResists=self.targetResists)
|
||||
droneDps += drone.getDps(targetResists=self.targetResists)
|
||||
droneVolley += drone.getVolley(targetProfile=self.targetProfile)
|
||||
droneDps += drone.getDps(targetProfile=self.targetProfile)
|
||||
|
||||
for fighter in self.fighters:
|
||||
droneVolley += fighter.getVolley(targetResists=self.targetResists)
|
||||
droneDps += fighter.getDps(targetResists=self.targetResists)
|
||||
droneVolley += fighter.getVolley(targetProfile=self.targetProfile)
|
||||
droneDps += fighter.getDps(targetProfile=self.targetProfile)
|
||||
|
||||
self.__droneDps = droneDps
|
||||
self.__droneVolley = droneVolley
|
||||
@@ -1533,6 +1652,44 @@ class Fit(object):
|
||||
secstatus = FitSystemSecurity.NULLSEC
|
||||
return secstatus
|
||||
|
||||
|
||||
def activeModulesIter(self):
|
||||
for mod in self.modules:
|
||||
if mod.state >= FittingModuleState.ACTIVE:
|
||||
yield mod
|
||||
|
||||
def activeDronesIter(self):
|
||||
for drone in self.drones:
|
||||
if drone.amountActive > 0:
|
||||
yield drone
|
||||
|
||||
def activeFightersIter(self):
|
||||
for fighter in self.fighters:
|
||||
if fighter.active:
|
||||
yield fighter
|
||||
|
||||
def activeFighterAbilityIter(self):
|
||||
for fighter in self.activeFightersIter():
|
||||
for ability in fighter.abilities:
|
||||
if ability.active:
|
||||
yield fighter, ability
|
||||
|
||||
def getDampMultScanRes(self):
|
||||
damps = []
|
||||
for mod in self.activeModulesIter():
|
||||
for effectName in ('remoteSensorDampFalloff', 'structureModuleEffectRemoteSensorDampener'):
|
||||
if effectName in mod.item.effects:
|
||||
damps.append((mod.getModifiedItemAttr('scanResolutionBonus'), 'default'))
|
||||
if 'doomsdayAOEDamp' in mod.item.effects:
|
||||
damps.append((mod.getModifiedItemAttr('scanResolutionBonus'), 'default'))
|
||||
for drone in self.activeDronesIter():
|
||||
if 'remoteSensorDampEntity' in drone.item.effects:
|
||||
damps.extend(drone.amountActive * ((drone.getModifiedItemAttr('scanResolutionBonus'), 'default'),))
|
||||
mults = {}
|
||||
for strength, stackingGroup in damps:
|
||||
mults.setdefault(stackingGroup, []).append((1 + strength / 100, None))
|
||||
return calculateMultiplier(mults)
|
||||
|
||||
def __deepcopy__(self, memo=None):
|
||||
fitCopy = Fit()
|
||||
# Character and owner are not copied
|
||||
@@ -1542,7 +1699,7 @@ class Fit(object):
|
||||
fitCopy.mode = deepcopy(self.mode)
|
||||
fitCopy.name = "%s copy" % self.name
|
||||
fitCopy.damagePattern = self.damagePattern
|
||||
fitCopy.targetResists = self.targetResists
|
||||
fitCopy.targetProfile = self.targetProfile
|
||||
fitCopy.implantLocation = self.implantLocation
|
||||
fitCopy.systemSecurity = self.systemSecurity
|
||||
fitCopy.notes = self.notes
|
||||
@@ -1583,6 +1740,8 @@ class Fit(object):
|
||||
copyProjectionInfo = fit.getProjectionInfo(fitCopy.ID)
|
||||
originalProjectionInfo = fit.getProjectionInfo(self.ID)
|
||||
copyProjectionInfo.active = originalProjectionInfo.active
|
||||
copyProjectionInfo.amount = originalProjectionInfo.amount
|
||||
copyProjectionInfo.projectionRange = originalProjectionInfo.projectionRange
|
||||
forceUpdateSavedata(fit)
|
||||
|
||||
return fitCopy
|
||||
|
||||
@@ -95,7 +95,7 @@ class Implant(HandledItem, ItemAttrShortcut):
|
||||
return
|
||||
for effect in self.item.effects.values():
|
||||
if effect.runTime == runTime and effect.isType("passive") and effect.activeByDefault:
|
||||
effect.handler(fit, self, ("implant",))
|
||||
effect.handler(fit, self, ("implant",), None, effect=effect)
|
||||
|
||||
@validates("fitID", "itemID", "active")
|
||||
def validator(self, key, val):
|
||||
|
||||
@@ -22,7 +22,7 @@ from copy import deepcopy
|
||||
from eos.effectHandlerHelpers import HandledImplantList
|
||||
|
||||
|
||||
class ImplantSet(object):
|
||||
class ImplantSet:
|
||||
def __init__(self, name=None):
|
||||
self.name = name
|
||||
self.__implants = HandledImplantList()
|
||||
|
||||
@@ -54,7 +54,7 @@ class Mode(ItemAttrShortcut, HandledItem):
|
||||
if self.item:
|
||||
for effect in self.item.effects.values():
|
||||
if effect.runTime == runTime and effect.activeByDefault:
|
||||
effect.handler(fit, self, context=("module",))
|
||||
effect.handler(fit, self, ("module",), None, effect=effect)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
copy = Mode(self.item)
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
from logbook import Logger
|
||||
import math
|
||||
|
||||
from logbook import Logger
|
||||
from sqlalchemy.orm import reconstructor, validates
|
||||
|
||||
import eos.db
|
||||
@@ -28,9 +29,10 @@ from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, Modi
|
||||
from eos.saveddata.citadel import Citadel
|
||||
from eos.saveddata.mutator import Mutator
|
||||
from eos.utils.cycles import CycleInfo, CycleSequence
|
||||
from eos.utils.default import DEFAULT
|
||||
from eos.utils.float import floatUnerr
|
||||
from eos.utils.spoolSupport import calculateSpoolup, resolveSpoolOptions
|
||||
from eos.utils.stats import DmgTypes
|
||||
from eos.utils.stats import DmgTypes, RRTypes
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
@@ -90,6 +92,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
self.__charge = None
|
||||
|
||||
self.projected = False
|
||||
self.projectionRange = None
|
||||
self.state = FittingModuleState.ONLINE
|
||||
self.build()
|
||||
|
||||
@@ -134,7 +137,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
self.__charge = None
|
||||
|
||||
self.__baseVolley = None
|
||||
self.__baseRemoteReps = None
|
||||
self.__baseRRAmount = None
|
||||
self.__miningyield = None
|
||||
self.__reloadTime = None
|
||||
self.__reloadForce = None
|
||||
@@ -211,8 +214,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
if charge is None:
|
||||
charges = 0
|
||||
else:
|
||||
chargeVolume = charge.volume
|
||||
containerCapacity = self.item.capacity
|
||||
chargeVolume = charge.attributes['volume'].value
|
||||
containerCapacity = self.item.attributes['capacity'].value
|
||||
if chargeVolume is None or containerCapacity is None:
|
||||
charges = 0
|
||||
else:
|
||||
@@ -315,31 +318,68 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
"energyDestabilizationRange", "empFieldRange",
|
||||
"ecmBurstRange", "warpScrambleRange", "cargoScanRange",
|
||||
"shipScanRange", "surveyScanRange")
|
||||
maxRange = None
|
||||
for attr in attrs:
|
||||
maxRange = self.getModifiedItemAttr(attr, None)
|
||||
if maxRange is not None:
|
||||
return maxRange
|
||||
if self.charge is not None:
|
||||
try:
|
||||
chargeName = self.charge.group.name
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if chargeName in ("Scanner Probe", "Survey Probe"):
|
||||
return None
|
||||
break
|
||||
if maxRange is not None:
|
||||
if 'burst projector' in self.item.name.lower():
|
||||
maxRange -= self.owner.ship.getModifiedItemAttr("radius")
|
||||
return maxRange
|
||||
missileMaxRangeData = self.missileMaxRangeData
|
||||
if missileMaxRangeData is None:
|
||||
return None
|
||||
lowerRange, higherRange, higherChance = missileMaxRangeData
|
||||
maxRange = lowerRange * (1 - higherChance) + higherRange * higherChance
|
||||
return maxRange
|
||||
|
||||
@property
|
||||
def missileMaxRangeData(self):
|
||||
if self.charge is None:
|
||||
return None
|
||||
try:
|
||||
chargeName = self.charge.group.name
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if chargeName in ("Scanner Probe", "Survey Probe"):
|
||||
return None
|
||||
|
||||
def calculateRange(maxVelocity, mass, agility, flightTime):
|
||||
# Source: http://www.eveonline.com/ingameboard.asp?a=topic&threadID=1307419&page=1#15
|
||||
# D_m = V_m * (T_m + T_0*[exp(- T_m/T_0)-1])
|
||||
maxVelocity = self.getModifiedChargeAttr("maxVelocity")
|
||||
flightTime = self.getModifiedChargeAttr("explosionDelay") / 1000.0
|
||||
mass = self.getModifiedChargeAttr("mass")
|
||||
agility = self.getModifiedChargeAttr("agility")
|
||||
if maxVelocity and (flightTime or mass or agility):
|
||||
accelTime = min(flightTime, mass * agility / 1000000)
|
||||
# Average distance done during acceleration
|
||||
duringAcceleration = maxVelocity / 2 * accelTime
|
||||
# Distance done after being at full speed
|
||||
fullSpeed = maxVelocity * (flightTime - accelTime)
|
||||
return duringAcceleration + fullSpeed
|
||||
accelTime = min(flightTime, mass * agility / 1000000)
|
||||
# Average distance done during acceleration
|
||||
duringAcceleration = maxVelocity / 2 * accelTime
|
||||
# Distance done after being at full speed
|
||||
fullSpeed = maxVelocity * (flightTime - accelTime)
|
||||
maxRange = duringAcceleration + fullSpeed
|
||||
return maxRange
|
||||
|
||||
maxVelocity = self.getModifiedChargeAttr("maxVelocity")
|
||||
if not maxVelocity:
|
||||
return None
|
||||
shipRadius = self.owner.ship.getModifiedItemAttr("radius")
|
||||
# Flight time has bonus based on ship radius, see https://github.com/pyfa-org/Pyfa/issues/2083
|
||||
flightTime = floatUnerr(self.getModifiedChargeAttr("explosionDelay") / 1000 + shipRadius / maxVelocity)
|
||||
mass = self.getModifiedChargeAttr("mass")
|
||||
agility = self.getModifiedChargeAttr("agility")
|
||||
lowerTime = math.floor(flightTime)
|
||||
higherTime = math.ceil(flightTime)
|
||||
lowerRange = calculateRange(maxVelocity, mass, agility, lowerTime)
|
||||
higherRange = calculateRange(maxVelocity, mass, agility, higherTime)
|
||||
# Fof range limit is supposedly calculated based on overview (surface-to-surface) range
|
||||
if 'fofMissileLaunching' in self.charge.effects:
|
||||
rangeLimit = self.getModifiedChargeAttr("maxFOFTargetRange")
|
||||
if rangeLimit:
|
||||
lowerRange = min(lowerRange, rangeLimit)
|
||||
higherRange = min(higherRange, rangeLimit)
|
||||
# Make range center-to-surface, as missiles spawn in the center of the ship
|
||||
lowerRange = max(0, lowerRange - shipRadius)
|
||||
higherRange = max(0, higherRange - shipRadius)
|
||||
higherChance = flightTime - lowerTime
|
||||
return lowerRange, higherRange, higherChance
|
||||
|
||||
@property
|
||||
def falloff(self):
|
||||
@@ -414,17 +454,31 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
|
||||
return self.__miningyield
|
||||
|
||||
def getVolleyParameters(self, spoolOptions=None, targetResists=None, ignoreState=False):
|
||||
def isDealingDamage(self, ignoreState=False):
|
||||
volleyParams = self.getVolleyParameters(ignoreState=ignoreState)
|
||||
for volley in volleyParams.values():
|
||||
if volley.total > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def getVolleyParameters(self, spoolOptions=None, targetProfile=None, ignoreState=False):
|
||||
if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState):
|
||||
return {0: DmgTypes(0, 0, 0, 0)}
|
||||
if self.__baseVolley is None:
|
||||
self.__baseVolley = {}
|
||||
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
|
||||
dmgMult = self.getModifiedItemAttr("damageMultiplier", 1)
|
||||
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0) or self.getModifiedItemAttr("doomsdayWarningDuration", 0)
|
||||
# Some delay attributes have non-0 default value, so we have to pick according to effects
|
||||
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(self.item.effects):
|
||||
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0)
|
||||
elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT'}.intersection(self.item.effects):
|
||||
dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0)
|
||||
else:
|
||||
dmgDelay = 0
|
||||
dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0)
|
||||
dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0)
|
||||
if dmgDuration != 0 and dmgSubcycle != 0:
|
||||
# Reaper DD can damage each target only once
|
||||
if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects:
|
||||
subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle))
|
||||
else:
|
||||
subcycles = 1
|
||||
@@ -443,24 +497,24 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
adjustedVolley = {}
|
||||
for volleyTime, volleyValue in self.__baseVolley.items():
|
||||
adjustedVolley[volleyTime] = DmgTypes(
|
||||
em=volleyValue.em * spoolMultiplier * (1 - getattr(targetResists, "emAmount", 0)),
|
||||
thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetResists, "thermalAmount", 0)),
|
||||
kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetResists, "kineticAmount", 0)),
|
||||
explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetResists, "explosiveAmount", 0)))
|
||||
em=volleyValue.em * spoolMultiplier * (1 - getattr(targetProfile, "emAmount", 0)),
|
||||
thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetProfile, "thermalAmount", 0)),
|
||||
kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetProfile, "kineticAmount", 0)),
|
||||
explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetProfile, "explosiveAmount", 0)))
|
||||
return adjustedVolley
|
||||
|
||||
def getVolley(self, spoolOptions=None, targetResists=None, ignoreState=False):
|
||||
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetResists=targetResists, ignoreState=ignoreState)
|
||||
def getVolley(self, spoolOptions=None, targetProfile=None, ignoreState=False):
|
||||
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
|
||||
if len(volleyParams) == 0:
|
||||
return DmgTypes(0, 0, 0, 0)
|
||||
return volleyParams[min(volleyParams)]
|
||||
|
||||
def getDps(self, spoolOptions=None, targetResists=None, ignoreState=False):
|
||||
def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False):
|
||||
dmgDuringCycle = DmgTypes(0, 0, 0, 0)
|
||||
cycleParams = self.getCycleParameters()
|
||||
if cycleParams is None:
|
||||
return dmgDuringCycle
|
||||
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetResists=targetResists, ignoreState=ignoreState)
|
||||
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
|
||||
avgCycleTime = cycleParams.averageTime
|
||||
if len(volleyParams) == 0 or avgCycleTime == 0:
|
||||
return dmgDuringCycle
|
||||
@@ -474,56 +528,75 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
explosive=dmgDuringCycle.explosive * dpsFactor)
|
||||
return dps
|
||||
|
||||
def getRemoteReps(self, spoolOptions=None, ignoreState=False):
|
||||
def isRemoteRepping(self, ignoreState=False):
|
||||
repParams = self.getRepAmountParameters(ignoreState=ignoreState)
|
||||
for rrData in repParams.values():
|
||||
if rrData:
|
||||
return True
|
||||
return False
|
||||
|
||||
def getRepAmountParameters(self, spoolOptions=None, ignoreState=False):
|
||||
if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState):
|
||||
return None, 0
|
||||
|
||||
def getBaseRemoteReps(module):
|
||||
remoteModuleGroups = {
|
||||
"Remote Armor Repairer": "Armor",
|
||||
"Ancillary Remote Armor Repairer": "Armor",
|
||||
"Mutadaptive Remote Armor Repairer": "Armor",
|
||||
"Remote Hull Repairer": "Hull",
|
||||
"Remote Shield Booster": "Shield",
|
||||
"Ancillary Remote Shield Booster": "Shield",
|
||||
"Remote Capacitor Transmitter": "Capacitor"}
|
||||
rrType = remoteModuleGroups.get(module.item.group.name, None)
|
||||
if not rrType:
|
||||
return None, 0
|
||||
return {}
|
||||
remoteModuleGroups = {
|
||||
"Remote Armor Repairer": "Armor",
|
||||
"Ancillary Remote Armor Repairer": "Armor",
|
||||
"Mutadaptive Remote Armor Repairer": "Armor",
|
||||
"Remote Hull Repairer": "Hull",
|
||||
"Remote Shield Booster": "Shield",
|
||||
"Ancillary Remote Shield Booster": "Shield",
|
||||
"Remote Capacitor Transmitter": "Capacitor"}
|
||||
rrType = remoteModuleGroups.get(self.item.group.name)
|
||||
if rrType is None:
|
||||
return {}
|
||||
if self.__baseRRAmount is None:
|
||||
self.__baseRRAmount = {}
|
||||
shieldAmount = 0
|
||||
armorAmount = 0
|
||||
hullAmount = 0
|
||||
capacitorAmount = 0
|
||||
if rrType == "Hull":
|
||||
rrAmount = module.getModifiedItemAttr("structureDamageAmount", 0)
|
||||
hullAmount += self.getModifiedItemAttr("structureDamageAmount", 0)
|
||||
elif rrType == "Armor":
|
||||
rrAmount = module.getModifiedItemAttr("armorDamageAmount", 0)
|
||||
if self.item.group.name == "Ancillary Remote Armor Repairer" and self.charge:
|
||||
mult = self.getModifiedItemAttr("chargedArmorDamageMultiplier", 1)
|
||||
else:
|
||||
mult = 1
|
||||
armorAmount += self.getModifiedItemAttr("armorDamageAmount", 0) * mult
|
||||
elif rrType == "Shield":
|
||||
rrAmount = module.getModifiedItemAttr("shieldBonus", 0)
|
||||
shieldAmount += self.getModifiedItemAttr("shieldBonus", 0)
|
||||
elif rrType == "Capacitor":
|
||||
rrAmount = module.getModifiedItemAttr("powerTransferAmount", 0)
|
||||
capacitorAmount += self.getModifiedItemAttr("powerTransferAmount", 0)
|
||||
rrDelay = 0 if rrType == "Shield" else self.rawCycleTime
|
||||
self.__baseRRAmount[rrDelay] = RRTypes(shield=shieldAmount, armor=armorAmount, hull=hullAmount, capacitor=capacitorAmount)
|
||||
spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self)
|
||||
spoolBoost = calculateSpoolup(
|
||||
self.getModifiedItemAttr("repairMultiplierBonusMax", 0),
|
||||
self.getModifiedItemAttr("repairMultiplierBonusPerCycle", 0),
|
||||
self.rawCycleTime / 1000, spoolType, spoolAmount)[0]
|
||||
spoolMultiplier = 1 + spoolBoost
|
||||
adjustedRRAmount = {}
|
||||
for rrTime, rrAmount in self.__baseRRAmount.items():
|
||||
if spoolMultiplier == 1:
|
||||
adjustedRRAmount[rrTime] = rrAmount
|
||||
else:
|
||||
return None, 0
|
||||
if rrAmount:
|
||||
cycleParams = self.getCycleParameters()
|
||||
if cycleParams is None:
|
||||
return None, 0
|
||||
rrAmount *= 1 / (cycleParams.averageTime / 1000)
|
||||
if module.item.group.name == "Ancillary Remote Armor Repairer" and module.charge:
|
||||
rrAmount *= module.getModifiedItemAttr("chargedArmorDamageMultiplier", 1)
|
||||
adjustedRRAmount[rrTime] = rrAmount * spoolMultiplier
|
||||
return adjustedRRAmount
|
||||
|
||||
return rrType, rrAmount
|
||||
|
||||
if self.__baseRemoteReps is None:
|
||||
self.__baseRemoteReps = getBaseRemoteReps(self)
|
||||
|
||||
rrType, rrAmount = self.__baseRemoteReps
|
||||
|
||||
if rrType and rrAmount and self.item.group.name == "Mutadaptive Remote Armor Repairer":
|
||||
spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self)
|
||||
spoolBoost = calculateSpoolup(
|
||||
self.getModifiedItemAttr("repairMultiplierBonusMax", 0),
|
||||
self.getModifiedItemAttr("repairMultiplierBonusPerCycle", 0),
|
||||
self.rawCycleTime / 1000, spoolType, spoolAmount)[0]
|
||||
rrAmount *= (1 + spoolBoost)
|
||||
|
||||
return rrType, rrAmount
|
||||
def getRemoteReps(self, spoolOptions=None, ignoreState=False, reloadOverride=None):
|
||||
rrDuringCycle = RRTypes(0, 0, 0, 0)
|
||||
cycleParams = self.getCycleParameters(reloadOverride=reloadOverride)
|
||||
if cycleParams is None:
|
||||
return rrDuringCycle
|
||||
repAmountParams = self.getRepAmountParameters(spoolOptions=spoolOptions, ignoreState=ignoreState)
|
||||
avgCycleTime = cycleParams.averageTime
|
||||
if len(repAmountParams) == 0 or avgCycleTime == 0:
|
||||
return rrDuringCycle
|
||||
for rrAmount in repAmountParams.values():
|
||||
rrDuringCycle += rrAmount
|
||||
rrFactor = 1 / (avgCycleTime / 1000)
|
||||
rps = rrDuringCycle * rrFactor
|
||||
return rps
|
||||
|
||||
def getSpoolData(self, spoolOptions=None):
|
||||
weaponMultMax = self.getModifiedItemAttr("damageMultiplierBonusMax", 0)
|
||||
@@ -623,7 +696,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
|
||||
# Check this only if we're told to do so
|
||||
if hardpointLimit:
|
||||
if fit.getHardpointsFree(self.hardpoint) < 1:
|
||||
if fit.getHardpointsFree(self.hardpoint) < (1 if self.owner != fit else 0):
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -650,30 +723,41 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
|
||||
def canHaveState(self, state=None, projectedOnto=None):
|
||||
"""
|
||||
Check with other modules if there are restrictions that might not allow this module to be activated
|
||||
Check with other modules if there are restrictions that might not allow this module to be activated.
|
||||
Returns True if state is allowed, or max state module can have if current state is invalid.
|
||||
"""
|
||||
# If we're going to set module to offline or online for local modules or offline for projected,
|
||||
# it should be fine for all cases
|
||||
# If we're going to set module to offline, it should be fine for all cases
|
||||
item = self.item
|
||||
if (state <= FittingModuleState.ONLINE and projectedOnto is None) or (state <= FittingModuleState.OFFLINE):
|
||||
if state <= FittingModuleState.OFFLINE:
|
||||
return True
|
||||
|
||||
# Check if the local module is over it's max limit; if it's not, we're fine
|
||||
maxGroupOnline = self.getModifiedItemAttr("maxGroupOnline", None)
|
||||
maxGroupActive = self.getModifiedItemAttr("maxGroupActive", None)
|
||||
if maxGroupActive is None and projectedOnto is None:
|
||||
if maxGroupOnline is None and maxGroupActive is None and projectedOnto is None:
|
||||
return True
|
||||
|
||||
# Following is applicable only to local modules, we do not want to limit projected
|
||||
if projectedOnto is None:
|
||||
currOnline = 0
|
||||
currActive = 0
|
||||
group = item.group.name
|
||||
maxState = None
|
||||
for mod in self.owner.modules:
|
||||
currItem = getattr(mod, "item", None)
|
||||
if mod.state >= FittingModuleState.ACTIVE and currItem is not None and currItem.group.name == group:
|
||||
currActive += 1
|
||||
if currActive > maxGroupActive:
|
||||
break
|
||||
return currActive <= maxGroupActive
|
||||
if currItem is not None and currItem.group.name == group:
|
||||
if mod.state >= FittingModuleState.ONLINE:
|
||||
currOnline += 1
|
||||
if mod.state >= FittingModuleState.ACTIVE:
|
||||
currActive += 1
|
||||
if maxGroupOnline is not None and currOnline > maxGroupOnline:
|
||||
if maxState is None or maxState > FittingModuleState.OFFLINE:
|
||||
maxState = FittingModuleState.OFFLINE
|
||||
break
|
||||
if maxGroupActive is not None and currActive > maxGroupActive:
|
||||
if maxState is None or maxState > FittingModuleState.ONLINE:
|
||||
maxState = FittingModuleState.ONLINE
|
||||
return True if maxState is None else maxState
|
||||
# For projected, we're checking if ship is vulnerable to given item
|
||||
else:
|
||||
# Do not allow to apply offensive modules on ship with offensive module immunite, with few exceptions
|
||||
@@ -684,18 +768,18 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
"energyNosferatuFalloff",
|
||||
"energyNeutralizerFalloff"}
|
||||
if not offensiveNonModifiers.intersection(set(item.effects)):
|
||||
return False
|
||||
return FittingModuleState.OFFLINE
|
||||
# If assistive modules are not allowed, do not let to apply these altogether
|
||||
if item.assistive and projectedOnto.ship.getModifiedItemAttr("disallowAssistance") == 1:
|
||||
return False
|
||||
return FittingModuleState.OFFLINE
|
||||
return True
|
||||
|
||||
def isValidCharge(self, charge):
|
||||
# Check sizes, if 'charge size > module volume' it won't fit
|
||||
if charge is None:
|
||||
return True
|
||||
chargeVolume = charge.volume
|
||||
moduleCapacity = self.item.capacity
|
||||
chargeVolume = charge.attributes['volume'].value
|
||||
moduleCapacity = self.item.attributes['capacity'].value
|
||||
if chargeVolume is not None and moduleCapacity is not None and chargeVolume > moduleCapacity:
|
||||
return False
|
||||
|
||||
@@ -780,7 +864,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
|
||||
def clear(self):
|
||||
self.__baseVolley = None
|
||||
self.__baseRemoteReps = None
|
||||
self.__baseRRAmount = None
|
||||
self.__miningyield = None
|
||||
self.__reloadTime = None
|
||||
self.__reloadForce = None
|
||||
@@ -788,7 +872,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
self.itemModifiedAttributes.clear()
|
||||
self.chargeModifiedAttributes.clear()
|
||||
|
||||
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False, gang=False):
|
||||
def calculateModifiedAttributes(self, fit, runTime, forceProjected=False, gang=False, forcedProjRange=DEFAULT):
|
||||
# We will run the effect when two conditions are met:
|
||||
# 1: It makes sense to run the effect
|
||||
# The effect is either offline
|
||||
@@ -805,6 +889,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
context = ("module",)
|
||||
projected = False
|
||||
|
||||
projectionRange = self.projectionRange if forcedProjRange is DEFAULT else forcedProjRange
|
||||
|
||||
if self.charge is not None:
|
||||
# fix for #82 and it's regression #106
|
||||
if not projected or (self.projected and not forceProjected) or gang:
|
||||
@@ -818,13 +904,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
(not gang or (gang and effect.isType("gang")))
|
||||
):
|
||||
contexts = ("moduleCharge",)
|
||||
# For gang effects, we pass in the effect itself as an argument. However, to avoid going through all
|
||||
# the effect definitions and defining this argument, do a simple try/catch here and be done with it.
|
||||
# @todo: possibly fix this
|
||||
try:
|
||||
effect.handler(fit, self, contexts, effect=effect)
|
||||
except:
|
||||
effect.handler(fit, self, contexts)
|
||||
effect.handler(fit, self, contexts, projectionRange, effect=effect)
|
||||
|
||||
if self.item:
|
||||
if self.state >= FittingModuleState.OVERHEATED:
|
||||
@@ -834,7 +914,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
and not forceProjected \
|
||||
and effect.activeByDefault \
|
||||
and ((gang and effect.isType("gang")) or not gang):
|
||||
effect.handler(fit, self, context)
|
||||
effect.handler(fit, self, context, projectionRange, effect=effect)
|
||||
|
||||
for effect in self.item.effects.values():
|
||||
if effect.runTime == runTime and \
|
||||
@@ -844,10 +924,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
(effect.isType("active") and self.state >= FittingModuleState.ACTIVE)) \
|
||||
and ((projected and effect.isType("projected")) or not projected) \
|
||||
and ((gang and effect.isType("gang")) or not gang):
|
||||
try:
|
||||
effect.handler(fit, self, context, effect=effect)
|
||||
except:
|
||||
effect.handler(fit, self, context)
|
||||
effect.handler(fit, self, context, projectionRange, effect=effect)
|
||||
|
||||
def getCycleParameters(self, reloadOverride=None):
|
||||
"""Copied from new eos as well"""
|
||||
@@ -873,14 +950,14 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
early_cycles = cycles_until_reload - final_cycles
|
||||
# Single cycle until effect cannot run anymore
|
||||
if early_cycles == 0:
|
||||
return CycleInfo(active_time, 0, 1)
|
||||
return CycleInfo(active_time, 0, 1, False)
|
||||
# Multiple cycles with the same parameters
|
||||
if forced_inactive_time == 0:
|
||||
return CycleInfo(active_time, 0, cycles_until_reload)
|
||||
return CycleInfo(active_time, 0, cycles_until_reload, False)
|
||||
# Multiple cycles with different parameters
|
||||
return CycleSequence((
|
||||
CycleInfo(active_time, forced_inactive_time, early_cycles),
|
||||
CycleInfo(active_time, 0, final_cycles)
|
||||
CycleInfo(active_time, forced_inactive_time, early_cycles, False),
|
||||
CycleInfo(active_time, 0, final_cycles, False)
|
||||
), 1)
|
||||
# Module cycles the same way all the time in 3 cases:
|
||||
# 1) caller doesn't want to take into account reload time
|
||||
@@ -891,7 +968,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
cycles_until_reload == math.inf or
|
||||
forced_inactive_time >= reload_time
|
||||
):
|
||||
return CycleInfo(active_time, forced_inactive_time, math.inf)
|
||||
isInactivityReload = factorReload and forced_inactive_time >= reload_time
|
||||
return CycleInfo(active_time, forced_inactive_time, math.inf, isInactivityReload)
|
||||
# We've got to take reload into consideration
|
||||
else:
|
||||
final_cycles = 1
|
||||
@@ -899,10 +977,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
# If effect has to reload after each its cycle, then its parameters
|
||||
# are the same all the time
|
||||
if early_cycles == 0:
|
||||
return CycleInfo(active_time, reload_time, math.inf)
|
||||
return CycleInfo(active_time, reload_time, math.inf, True)
|
||||
return CycleSequence((
|
||||
CycleInfo(active_time, forced_inactive_time, early_cycles),
|
||||
CycleInfo(active_time, reload_time, final_cycles)
|
||||
CycleInfo(active_time, forced_inactive_time, early_cycles, False),
|
||||
CycleInfo(active_time, reload_time, final_cycles, True)
|
||||
), math.inf)
|
||||
|
||||
@property
|
||||
@@ -977,6 +1055,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
copy.state = self.state
|
||||
copy.spoolType = self.spoolType
|
||||
copy.spoolAmount = self.spoolAmount
|
||||
copy.projectionRange = self.projectionRange
|
||||
|
||||
for x in self.mutators.values():
|
||||
Mutator(copy, x.attribute, x.value)
|
||||
@@ -986,10 +1065,17 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
|
||||
def rebase(self, item):
|
||||
state = self.state
|
||||
charge = self.charge
|
||||
spoolType = self.spoolType
|
||||
spoolAmount = self.spoolAmount
|
||||
projectionRange = self.projectionRange
|
||||
|
||||
Module.__init__(self, item, self.baseItem, self.mutaplasmid)
|
||||
self.state = state
|
||||
if self.isValidCharge(charge):
|
||||
self.charge = charge
|
||||
self.spoolType = spoolType
|
||||
self.spoolAmount = spoolAmount
|
||||
self.projectionRange = projectionRange
|
||||
for x in self.mutators.values():
|
||||
Mutator(self, x.attribute, x.value)
|
||||
|
||||
|
||||
@@ -73,12 +73,16 @@ class Mutator(EqBase):
|
||||
self.dynamicAttribute = next(a for a in self.module.mutaplasmid.attributes if a.attributeID == self.attrID)
|
||||
# base attribute links to the base ite's attribute for this mutated definition (contains original, base value)
|
||||
self.baseAttribute = self.module.item.attributes[self.dynamicAttribute.name]
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
self.module = None
|
||||
|
||||
@validates("value")
|
||||
def validator(self, key, val):
|
||||
""" Validates values as properly falling within the range of the modules' Mutaplasmid """
|
||||
if self.baseValue == 0:
|
||||
return 0
|
||||
mod = val / self.baseValue
|
||||
|
||||
if self.minMod <= mod <= self.maxMod:
|
||||
|
||||
@@ -42,7 +42,7 @@ class PriceStatus(IntEnum):
|
||||
fetchTimeout = 4
|
||||
|
||||
|
||||
class Price(object):
|
||||
class Price:
|
||||
def __init__(self, typeID):
|
||||
self.typeID = typeID
|
||||
self.time = 0
|
||||
|
||||
@@ -98,7 +98,7 @@ class Ship(ItemAttrShortcut, HandledItem):
|
||||
# skillbook modifiers will use the stale modifier value
|
||||
# GH issue #351
|
||||
fit.register(self)
|
||||
effect.handler(fit, self, ("ship",))
|
||||
effect.handler(fit, self, ("ship",), None, effect=effect)
|
||||
|
||||
def validateModeItem(self, item, owner=None):
|
||||
""" Checks if provided item is a valid mode """
|
||||
|
||||
@@ -24,7 +24,7 @@ import time
|
||||
# from tomorrow import threads
|
||||
|
||||
|
||||
class SsoCharacter(object):
|
||||
class SsoCharacter:
|
||||
def __init__(self, charID, name, client, accessToken=None, refreshToken=None):
|
||||
self.characterID = charID
|
||||
self.characterName = name
|
||||
|
||||
405
eos/saveddata/targetProfile.py
Normal file
405
eos/saveddata/targetProfile.py
Normal file
@@ -0,0 +1,405 @@
|
||||
# ===============================================================================
|
||||
# Copyright (C) 2014 Ryan Holmes
|
||||
#
|
||||
# This file is part of eos.
|
||||
#
|
||||
# eos is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# eos is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
import math
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
|
||||
from logbook import Logger
|
||||
from sqlalchemy.orm import reconstructor
|
||||
|
||||
import eos.db
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
BUILTINS = OrderedDict([
|
||||
# 0 is taken by ideal target profile, composed manually in one of TargetProfile methods
|
||||
(-1, ('Uniform (25%)', 0.25, 0.25, 0.25, 0.25)),
|
||||
(-2, ('Uniform (50%)', 0.50, 0.50, 0.50, 0.50)),
|
||||
(-3, ('Uniform (75%)', 0.75, 0.75, 0.75, 0.75)),
|
||||
(-4, ('Uniform (90%)', 0.90, 0.90, 0.90, 0.90)),
|
||||
(-5, ('[T1 Resist]Shield', 0.0, 0.20, 0.40, 0.50)),
|
||||
(-6, ('[T1 Resist]Armor', 0.50, 0.45, 0.25, 0.10)),
|
||||
(-7, ('[T1 Resist]Hull', 0.33, 0.33, 0.33, 0.33)),
|
||||
(-8, ('[T1 Resist]Shield (+T2 DCU)', 0.125, 0.30, 0.475, 0.562)),
|
||||
(-9, ('[T1 Resist]Armor (+T2 DCU)', 0.575, 0.532, 0.363, 0.235)),
|
||||
(-10, ('[T1 Resist]Hull (+T2 DCU)', 0.598, 0.598, 0.598, 0.598)),
|
||||
(-11, ('[T2 Resist]Amarr (Shield)', 0.0, 0.20, 0.70, 0.875)),
|
||||
(-12, ('[T2 Resist]Amarr (Armor)', 0.50, 0.35, 0.625, 0.80)),
|
||||
(-13, ('[T2 Resist]Caldari (Shield)', 0.20, 0.84, 0.76, 0.60)),
|
||||
(-14, ('[T2 Resist]Caldari (Armor)', 0.50, 0.8625, 0.625, 0.10)),
|
||||
(-15, ('[T2 Resist]Gallente (Shield)', 0.0, 0.60, 0.85, 0.50)),
|
||||
(-16, ('[T2 Resist]Gallente (Armor)', 0.50, 0.675, 0.8375, 0.10)),
|
||||
(-17, ('[T2 Resist]Minmatar (Shield)', 0.75, 0.60, 0.40, 0.50)),
|
||||
(-18, ('[T2 Resist]Minmatar (Armor)', 0.90, 0.675, 0.25, 0.10)),
|
||||
(-19, ('[NPC][Asteroid]Angel Cartel', 0.54, 0.42, 0.37, 0.32)),
|
||||
(-20, ('[NPC][Asteroid]Blood Raiders', 0.34, 0.39, 0.45, 0.52)),
|
||||
(-21, ('[NPC][Asteroid]Guristas', 0.55, 0.35, 0.3, 0.48)),
|
||||
(-22, ('[NPC][Asteroid]Rogue Drones', 0.35, 0.38, 0.44, 0.49)),
|
||||
(-23, ('[NPC][Asteroid]Sanshas Nation', 0.35, 0.4, 0.47, 0.53)),
|
||||
(-24, ('[NPC][Asteroid]Serpentis', 0.49, 0.38, 0.29, 0.51)),
|
||||
(-25, ('[NPC][Deadspace]Angel Cartel', 0.59, 0.48, 0.4, 0.32)),
|
||||
(-26, ('[NPC][Deadspace]Blood Raiders', 0.31, 0.39, 0.47, 0.56)),
|
||||
(-27, ('[NPC][Deadspace]Guristas', 0.57, 0.39, 0.31, 0.5)),
|
||||
(-28, ('[NPC][Deadspace]Rogue Drones', 0.42, 0.42, 0.47, 0.49)),
|
||||
(-29, ('[NPC][Deadspace]Sanshas Nation', 0.31, 0.39, 0.47, 0.56)),
|
||||
(-30, ('[NPC][Deadspace]Serpentis', 0.49, 0.38, 0.29, 0.56)),
|
||||
(-31, ('[NPC][Mission]Amarr Empire', 0.34, 0.38, 0.42, 0.46)),
|
||||
(-32, ('[NPC][Mission]Caldari State', 0.51, 0.38, 0.3, 0.51)),
|
||||
(-33, ('[NPC][Mission]CONCORD', 0.47, 0.46, 0.47, 0.47)),
|
||||
(-34, ('[NPC][Mission]Gallente Federation', 0.51, 0.38, 0.31, 0.52)),
|
||||
(-35, ('[NPC][Mission]Khanid', 0.51, 0.42, 0.36, 0.4)),
|
||||
(-36, ('[NPC][Mission]Minmatar Republic', 0.51, 0.46, 0.41, 0.35)),
|
||||
(-37, ('[NPC][Mission]Mordus Legion', 0.32, 0.48, 0.4, 0.62)),
|
||||
(-38, ('[NPC][Other]Sleeper', 0.61, 0.61, 0.61, 0.61)),
|
||||
(-39, ('[NPC][Other]Sansha Incursion', 0.65, 0.63, 0.64, 0.65)),
|
||||
(-40, ('[NPC][Burner]Cruor (Blood Raiders)', 0.8, 0.73, 0.69, 0.67)),
|
||||
(-41, ('[NPC][Burner]Dramiel (Angel)', 0.35, 0.48, 0.61, 0.68)),
|
||||
(-42, ('[NPC][Burner]Daredevil (Serpentis)', 0.69, 0.59, 0.59, 0.43)),
|
||||
(-43, ('[NPC][Burner]Succubus (Sanshas Nation)', 0.35, 0.48, 0.61, 0.68)),
|
||||
(-44, ('[NPC][Burner]Worm (Guristas)', 0.48, 0.58, 0.69, 0.74)),
|
||||
(-45, ('[NPC][Burner]Enyo', 0.58, 0.72, 0.86, 0.24)),
|
||||
(-46, ('[NPC][Burner]Hawk', 0.3, 0.86, 0.79, 0.65)),
|
||||
(-47, ('[NPC][Burner]Jaguar', 0.78, 0.65, 0.48, 0.56)),
|
||||
(-48, ('[NPC][Burner]Vengeance', 0.66, 0.56, 0.75, 0.86)),
|
||||
(-49, ('[NPC][Burner]Ashimmu (Blood Raiders)', 0.8, 0.76, 0.68, 0.7)),
|
||||
(-50, ('[NPC][Burner]Talos', 0.68, 0.59, 0.59, 0.43)),
|
||||
(-51, ('[NPC][Burner]Sentinel', 0.58, 0.45, 0.52, 0.66)),
|
||||
# Source: ticket #2067
|
||||
(-52, ('[NPC][Invasion]Invading Precursor Entities', 0.422, 0.367, 0.453, 0.411)),
|
||||
(-53, ('[NPC][Invasion]Retaliating Amarr Entities', 0.360, 0.310, 0.441, 0.602)),
|
||||
(-54, ('[NPC][Invasion]Retaliating Caldari Entities', 0.287, 0.610, 0.487, 0.401)),
|
||||
(-55, ('[NPC][Invasion]Retaliating Gallente Entities', 0.383, 0.414, 0.578, 0.513)),
|
||||
(-56, ('[NPC][Invasion]Retaliating Minmatar Entities', 0.620, 0.422, 0.355, 0.399)),
|
||||
(-57, ('[NPC][Abyssal][Dark Matter All Tiers]Drones', 0.439, 0.522, 0.529, 0.435)),
|
||||
(-58, ('[NPC][Abyssal][Dark Matter All Tiers]Overmind', 0.626, 0.576, 0.612, 0.624)),
|
||||
(-59, ('[NPC][Abyssal][Dark Matter All Tiers]Seeker', 0.082, 0.082, 0.082, 0.082)),
|
||||
(-60, ('[NPC][Abyssal][Dark Matter All Tiers]Triglavian', 0.477, 0.401, 0.449, 0.37)),
|
||||
(-61, ('[NPC][Abyssal][Dark Matter All Tiers]Drifter', 0.403, 0.403, 0.403, 0.403)),
|
||||
(-62, ('[NPC][Abyssal][Dark Matter All Tiers]Sleeper', 0.435, 0.435, 0.435, 0.435)),
|
||||
(-63, ('[NPC][Abyssal][Dark Matter All Tiers]All', 0.507, 0.477, 0.502, 0.493)),
|
||||
(-64, ('[NPC][Abyssal][Electrical T1/T2]Drones', 0.323, 0.522, 0.529, 0.435)),
|
||||
(-65, ('[NPC][Abyssal][Electrical T1/T2]Overmind', 0.521, 0.576, 0.612, 0.624)),
|
||||
(-66, ('[NPC][Abyssal][Electrical T1/T2]Seeker', 0, 0.082, 0.082, 0.082)),
|
||||
(-67, ('[NPC][Abyssal][Electrical T1/T2]Triglavian', 0.333, 0.401, 0.449, 0.37)),
|
||||
(-68, ('[NPC][Abyssal][Electrical T1/T2]Drifter', 0.267, 0.403, 0.403, 0.403)),
|
||||
(-69, ('[NPC][Abyssal][Electrical T1/T2]Sleeper', 0.329, 0.435, 0.435, 0.435)),
|
||||
(-70, ('[NPC][Abyssal][Electrical T1/T2]All', 0.385, 0.477, 0.502, 0.493)),
|
||||
(-71, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]Drones', 0.255, 0.522, 0.529, 0.435)),
|
||||
(-72, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]Overmind', 0.457, 0.576, 0.612, 0.624)),
|
||||
(-73, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]Seeker', 0, 0.082, 0.082, 0.082)),
|
||||
(-74, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]Triglavian', 0.241, 0.401, 0.449, 0.37)),
|
||||
(-75, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]Drifter', 0.184, 0.403, 0.403, 0.403)),
|
||||
(-76, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]Sleeper', 0.268, 0.435, 0.435, 0.435)),
|
||||
(-77, ('[NPC][Abyssal][Electrical T3 (Some T5 Rooms)]All', 0.313, 0.477, 0.502, 0.493)),
|
||||
(-78, ('[NPC][Abyssal][Electrical T4/T5]Drones', 0.193, 0.522, 0.529, 0.435)),
|
||||
(-79, ('[NPC][Abyssal][Electrical T4/T5]Overmind', 0.398, 0.576, 0.612, 0.624)),
|
||||
(-80, ('[NPC][Abyssal][Electrical T4/T5]Seeker', 0, 0.082, 0.082, 0.082)),
|
||||
(-81, ('[NPC][Abyssal][Electrical T4/T5]Triglavian', 0.183, 0.401, 0.449, 0.37)),
|
||||
(-82, ('[NPC][Abyssal][Electrical T4/T5]Drifter', 0.107, 0.403, 0.403, 0.403)),
|
||||
(-83, ('[NPC][Abyssal][Electrical T4/T5]Sleeper', 0.215, 0.435, 0.435, 0.435)),
|
||||
(-84, ('[NPC][Abyssal][Electrical T4/T5]All', 0.25, 0.477, 0.502, 0.493)),
|
||||
(-85, ('[NPC][Abyssal][Firestorm T1/T2]Drones', 0.461, 0.425, 0.541, 0.443)),
|
||||
(-86, ('[NPC][Abyssal][Firestorm T1/T2]Overmind', 0.65, 0.469, 0.625, 0.633)),
|
||||
(-87, ('[NPC][Abyssal][Firestorm T1/T2]Seeker', 0.084, 0, 0.084, 0.084)),
|
||||
(-88, ('[NPC][Abyssal][Firestorm T1/T2]Triglavian', 0.534, 0.266, 0.484, 0.366)),
|
||||
(-89, ('[NPC][Abyssal][Firestorm T1/T2]Drifter', 0.422, 0.282, 0.422, 0.422)),
|
||||
(-90, ('[NPC][Abyssal][Firestorm T1/T2]Sleeper', 0.512, 0.402, 0.512, 0.512)),
|
||||
(-91, ('[NPC][Abyssal][Firestorm T1/T2]All', 0.541, 0.365, 0.524, 0.504)),
|
||||
(-92, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]Drones', 0.461, 0.36, 0.541, 0.443)),
|
||||
(-93, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]Overmind', 0.65, 0.391, 0.625, 0.633)),
|
||||
(-94, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]Seeker', 0.084, 0, 0.084, 0.084)),
|
||||
(-95, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]Triglavian', 0.534, 0.161, 0.484, 0.366)),
|
||||
(-96, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]Drifter', 0.422, 0.196, 0.422, 0.422)),
|
||||
(-97, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]Sleeper', 0.512, 0.337, 0.512, 0.512)),
|
||||
(-98, ('[NPC][Abyssal][Firestorm T3 (Some T5 Rooms)]All', 0.541, 0.284, 0.524, 0.504)),
|
||||
(-99, ('[NPC][Abyssal][Firestorm T4/T5]Drones', 0.461, 0.305, 0.541, 0.443)),
|
||||
(-100, ('[NPC][Abyssal][Firestorm T4/T5]Overmind', 0.65, 0.323, 0.625, 0.633)),
|
||||
(-101, ('[NPC][Abyssal][Firestorm T4/T5]Seeker', 0.084, 0, 0.084, 0.084)),
|
||||
(-102, ('[NPC][Abyssal][Firestorm T4/T5]Triglavian', 0.534, 0.082, 0.484, 0.366)),
|
||||
(-103, ('[NPC][Abyssal][Firestorm T4/T5]Drifter', 0.422, 0.114, 0.422, 0.422)),
|
||||
(-104, ('[NPC][Abyssal][Firestorm T4/T5]Sleeper', 0.512, 0.276, 0.512, 0.512)),
|
||||
(-105, ('[NPC][Abyssal][Firestorm T4/T5]All', 0.541, 0.214, 0.524, 0.504)),
|
||||
(-106, ('[NPC][Abyssal][Exotic T1/T2]Drones', 0.439, 0.522, 0.417, 0.435)),
|
||||
(-107, ('[NPC][Abyssal][Exotic T1/T2]Overmind', 0.626, 0.576, 0.496, 0.624)),
|
||||
(-108, ('[NPC][Abyssal][Exotic T1/T2]Seeker', 0.082, 0.082, 0, 0.082)),
|
||||
(-109, ('[NPC][Abyssal][Exotic T1/T2]Triglavian', 0.477, 0.401, 0.284, 0.37)),
|
||||
(-110, ('[NPC][Abyssal][Exotic T1/T2]Drifter', 0.403, 0.403, 0.267, 0.403)),
|
||||
(-111, ('[NPC][Abyssal][Exotic T1/T2]Sleeper', 0.435, 0.435, 0.329, 0.435)),
|
||||
(-112, ('[NPC][Abyssal][Exotic T1/T2]All', 0.507, 0.477, 0.373, 0.493)),
|
||||
(-113, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]Drones', 0.439, 0.522, 0.351, 0.435)),
|
||||
(-114, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]Overmind', 0.626, 0.576, 0.419, 0.624)),
|
||||
(-115, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]Seeker', 0.082, 0.082, 0, 0.082)),
|
||||
(-116, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]Triglavian', 0.477, 0.401, 0.176, 0.37)),
|
||||
(-117, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]Drifter', 0.403, 0.403, 0.184, 0.403)),
|
||||
(-118, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]Sleeper', 0.435, 0.435, 0.268, 0.435)),
|
||||
(-119, ('[NPC][Abyssal][Exotic T3 (Some T5 Rooms)]All', 0.507, 0.477, 0.293, 0.493)),
|
||||
(-120, ('[NPC][Abyssal][Exotic T4/T5]Drones', 0.439, 0.522, 0.293, 0.435)),
|
||||
(-121, ('[NPC][Abyssal][Exotic T4/T5]Overmind', 0.626, 0.576, 0.344, 0.624)),
|
||||
(-122, ('[NPC][Abyssal][Exotic T4/T5]Seeker', 0.082, 0.082, 0, 0.082)),
|
||||
(-123, ('[NPC][Abyssal][Exotic T4/T5]Triglavian', 0.477, 0.401, 0.107, 0.37)),
|
||||
(-124, ('[NPC][Abyssal][Exotic T4/T5]Drifter', 0.403, 0.403, 0.107, 0.403)),
|
||||
(-125, ('[NPC][Abyssal][Exotic T4/T5]Sleeper', 0.435, 0.435, 0.215, 0.435)),
|
||||
(-126, ('[NPC][Abyssal][Exotic T4/T5]All', 0.507, 0.477, 0.223, 0.493)),
|
||||
(-127, ('[NPC][Abyssal][Gamma T1/T2]Drones', 0.449, 0.54, 0.549, 0.336)),
|
||||
(-128, ('[NPC][Abyssal][Gamma T1/T2]Overmind', 0.6, 0.557, 0.601, 0.504)),
|
||||
(-129, ('[NPC][Abyssal][Gamma T1/T2]Seeker', 0.085, 0.085, 0.085, 0)),
|
||||
(-130, ('[NPC][Abyssal][Gamma T1/T2]Triglavian', 0.463, 0.392, 0.447, 0.193)),
|
||||
(-131, ('[NPC][Abyssal][Gamma T1/T2]Drifter', 0.428, 0.428, 0.428, 0.287)),
|
||||
(-132, ('[NPC][Abyssal][Gamma T1/T2]Sleeper', 0.435, 0.435, 0.435, 0.329)),
|
||||
(-133, ('[NPC][Abyssal][Gamma T1/T2]All', 0.493, 0.472, 0.5, 0.362)),
|
||||
(-134, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]Drones', 0.449, 0.54, 0.549, 0.264)),
|
||||
(-135, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]Overmind', 0.6, 0.557, 0.601, 0.428)),
|
||||
(-136, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]Seeker', 0.085, 0.085, 0.085, 0)),
|
||||
(-137, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]Triglavian', 0.463, 0.392, 0.447, 0.071)),
|
||||
(-138, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]Drifter', 0.428, 0.428, 0.428, 0.2)),
|
||||
(-139, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]Sleeper', 0.435, 0.435, 0.435, 0.268)),
|
||||
(-140, ('[NPC][Abyssal][Gamma T3 (Some T5 Rooms)]All', 0.493, 0.472, 0.5, 0.28)),
|
||||
(-141, ('[NPC][Abyssal][Gamma T4/T5]Drones', 0.449, 0.54, 0.549, 0.197)),
|
||||
(-142, ('[NPC][Abyssal][Gamma T4/T5]Overmind', 0.6, 0.557, 0.601, 0.356)),
|
||||
(-143, ('[NPC][Abyssal][Gamma T4/T5]Seeker', 0.085, 0.085, 0.085, 0)),
|
||||
(-144, ('[NPC][Abyssal][Gamma T4/T5]Triglavian', 0.463, 0.392, 0.447, 0.029)),
|
||||
(-145, ('[NPC][Abyssal][Gamma T4/T5]Drifter', 0.428, 0.428, 0.428, 0.117)),
|
||||
(-146, ('[NPC][Abyssal][Gamma T4/T5]Sleeper', 0.435, 0.435, 0.435, 0.215)),
|
||||
(-147, ('[NPC][Abyssal][Gamma T4/T5]All', 0.493, 0.472, 0.5, 0.21))])
|
||||
|
||||
|
||||
class TargetProfile:
|
||||
|
||||
# also determined import/export order - VERY IMPORTANT
|
||||
DAMAGE_TYPES = ('em', 'thermal', 'kinetic', 'explosive')
|
||||
_idealTarget = None
|
||||
_builtins = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.builtin = False
|
||||
self.update(*args, **kwargs)
|
||||
|
||||
@reconstructor
|
||||
def init(self):
|
||||
self.builtin = False
|
||||
|
||||
def update(self, emAmount=0, thermalAmount=0, kineticAmount=0, explosiveAmount=0, maxVelocity=None, signatureRadius=None, radius=None):
|
||||
self.emAmount = emAmount
|
||||
self.thermalAmount = thermalAmount
|
||||
self.kineticAmount = kineticAmount
|
||||
self.explosiveAmount = explosiveAmount
|
||||
self._maxVelocity = maxVelocity
|
||||
self._signatureRadius = signatureRadius
|
||||
self._radius = radius
|
||||
|
||||
@classmethod
|
||||
def getBuiltinList(cls):
|
||||
if cls._builtins is None:
|
||||
cls.__generateBuiltins()
|
||||
return list(cls._builtins.values())
|
||||
|
||||
@classmethod
|
||||
def getBuiltinById(cls, id):
|
||||
if cls._builtins is None:
|
||||
cls.__generateBuiltins()
|
||||
return cls._builtins.get(id)
|
||||
|
||||
@classmethod
|
||||
def __generateBuiltins(cls):
|
||||
cls._builtins = OrderedDict()
|
||||
for id, data in BUILTINS.items():
|
||||
rawName = data[0]
|
||||
data = data[1:]
|
||||
profile = TargetProfile(*data)
|
||||
profile.ID = id
|
||||
profile.rawName = rawName
|
||||
profile.builtin = True
|
||||
cls._builtins[id] = profile
|
||||
|
||||
@classmethod
|
||||
def getIdeal(cls):
|
||||
if cls._idealTarget is None:
|
||||
cls._idealTarget = cls(
|
||||
emAmount=0,
|
||||
thermalAmount=0,
|
||||
kineticAmount=0,
|
||||
explosiveAmount=0,
|
||||
maxVelocity=0,
|
||||
signatureRadius=None,
|
||||
radius=0)
|
||||
cls._idealTarget.rawName = 'Ideal Target'
|
||||
cls._idealTarget.ID = 0
|
||||
cls._idealTarget.builtin = True
|
||||
return cls._idealTarget
|
||||
|
||||
@property
|
||||
def maxVelocity(self):
|
||||
return self._maxVelocity or 0
|
||||
|
||||
@maxVelocity.setter
|
||||
def maxVelocity(self, val):
|
||||
self._maxVelocity = val
|
||||
|
||||
@property
|
||||
def signatureRadius(self):
|
||||
if self._signatureRadius is None or self._signatureRadius == -1:
|
||||
return math.inf
|
||||
return self._signatureRadius
|
||||
|
||||
@signatureRadius.setter
|
||||
def signatureRadius(self, val):
|
||||
if val is not None and math.isinf(val):
|
||||
val = None
|
||||
self._signatureRadius = val
|
||||
|
||||
@property
|
||||
def radius(self):
|
||||
return self._radius or 0
|
||||
|
||||
@radius.setter
|
||||
def radius(self, val):
|
||||
self._radius = val
|
||||
|
||||
@classmethod
|
||||
def importPatterns(cls, text):
|
||||
lines = re.split('[\n\r]+', text)
|
||||
patterns = []
|
||||
numPatterns = 0
|
||||
|
||||
# When we import damage profiles, we create new ones and update old ones. To do this, get a list of current
|
||||
# patterns to allow lookup
|
||||
lookup = {}
|
||||
current = eos.db.getTargetProfileList()
|
||||
for pattern in current:
|
||||
lookup[pattern.rawName] = pattern
|
||||
|
||||
for line in lines:
|
||||
try:
|
||||
if line.strip()[0] == "#": # comments
|
||||
continue
|
||||
line = line.split('#', 1)[0] # allows for comments
|
||||
type, data = line.rsplit('=', 1)
|
||||
type, data = type.strip(), [d.strip() for d in data.split(',')]
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
pyfalog.warning("Data isn't in correct format, continue to next line.")
|
||||
continue
|
||||
|
||||
if type not in ("TargetProfile", "TargetResists"):
|
||||
continue
|
||||
|
||||
numPatterns += 1
|
||||
name, dataRes, dataMisc = data[0], data[1:5], data[5:8]
|
||||
fields = {}
|
||||
|
||||
for index, val in enumerate(dataRes):
|
||||
val = float(val) if val else 0
|
||||
if math.isinf(val):
|
||||
val = 0
|
||||
try:
|
||||
assert 0 <= val <= 100
|
||||
fields["%sAmount" % cls.DAMAGE_TYPES[index]] = val / 100
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
pyfalog.warning("Caught unhandled exception in import patterns.")
|
||||
continue
|
||||
|
||||
if len(dataMisc) == 3:
|
||||
for index, val in enumerate(dataMisc):
|
||||
try:
|
||||
fieldName = ("maxVelocity", "signatureRadius", "radius")[index]
|
||||
except IndexError:
|
||||
break
|
||||
val = float(val) if val else 0
|
||||
if fieldName != "signatureRadius" and math.isinf(val):
|
||||
val = 0
|
||||
fields[fieldName] = val
|
||||
|
||||
if len(fields) in (4, 7): # Avoid possible blank lines
|
||||
if name.strip() in lookup:
|
||||
pattern = lookup[name.strip()]
|
||||
pattern.update(**fields)
|
||||
eos.db.save(pattern)
|
||||
else:
|
||||
pattern = TargetProfile(**fields)
|
||||
pattern.rawName = name.strip()
|
||||
eos.db.save(pattern)
|
||||
patterns.append(pattern)
|
||||
|
||||
eos.db.commit()
|
||||
|
||||
return patterns, numPatterns
|
||||
|
||||
EXPORT_FORMAT = "TargetProfile = %s,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f\n"
|
||||
|
||||
@classmethod
|
||||
def exportPatterns(cls, *patterns):
|
||||
out = "# Exported from pyfa\n#\n"
|
||||
out += "# Values are in following format:\n"
|
||||
out += "# TargetProfile = [name],[EM %],[Thermal %],[Kinetic %],[Explosive %],[Max velocity m/s],[Signature radius m],[Radius m]\n\n"
|
||||
for dp in patterns:
|
||||
out += cls.EXPORT_FORMAT % (
|
||||
dp.rawName,
|
||||
dp.emAmount * 100,
|
||||
dp.thermalAmount * 100,
|
||||
dp.kineticAmount * 100,
|
||||
dp.explosiveAmount * 100,
|
||||
dp.maxVelocity,
|
||||
dp.signatureRadius,
|
||||
dp.radius
|
||||
)
|
||||
|
||||
return out.strip()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.rawName
|
||||
|
||||
@property
|
||||
def fullName(self):
|
||||
categories, tail = self.__parseRawName()
|
||||
return '{}{}'.format(''.join('[{}]'.format(c) for c in categories), tail)
|
||||
|
||||
@property
|
||||
def shortName(self):
|
||||
return self.__parseRawName()[1]
|
||||
|
||||
@property
|
||||
def hierarchy(self):
|
||||
return self.__parseRawName()[0]
|
||||
|
||||
def __parseRawName(self):
|
||||
hierarchy = []
|
||||
remainingName = self.rawName.strip() if self.rawName else ''
|
||||
while True:
|
||||
start, end = remainingName.find('['), remainingName.find(']')
|
||||
if start == -1 or end == -1:
|
||||
return hierarchy, remainingName
|
||||
splitter = remainingName.find('|')
|
||||
if splitter != -1 and splitter == start - 1:
|
||||
return hierarchy, remainingName[1:]
|
||||
hierarchy.append(remainingName[start + 1:end])
|
||||
remainingName = remainingName[end + 1:].strip()
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
p = TargetProfile(
|
||||
self.emAmount, self.thermalAmount, self.kineticAmount, self.explosiveAmount,
|
||||
self._maxVelocity, self._signatureRadius, self._radius)
|
||||
p.rawName = "%s copy" % self.rawName
|
||||
return p
|
||||
@@ -1,116 +0,0 @@
|
||||
# ===============================================================================
|
||||
# Copyright (C) 2014 Ryan Holmes
|
||||
#
|
||||
# This file is part of eos.
|
||||
#
|
||||
# eos is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# eos is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
import re
|
||||
from logbook import Logger
|
||||
import eos.db
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class TargetResists(object):
|
||||
# also determined import/export order - VERY IMPORTANT
|
||||
DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.update(*args, **kwargs)
|
||||
|
||||
def update(self, emAmount=0, thermalAmount=0, kineticAmount=0, explosiveAmount=0):
|
||||
self.emAmount = emAmount
|
||||
self.thermalAmount = thermalAmount
|
||||
self.kineticAmount = kineticAmount
|
||||
self.explosiveAmount = explosiveAmount
|
||||
|
||||
@classmethod
|
||||
def importPatterns(cls, text):
|
||||
lines = re.split('[\n\r]+', text)
|
||||
patterns = []
|
||||
numPatterns = 0
|
||||
|
||||
# When we import damage profiles, we create new ones and update old ones. To do this, get a list of current
|
||||
# patterns to allow lookup
|
||||
lookup = {}
|
||||
current = eos.db.getTargetResistsList()
|
||||
for pattern in current:
|
||||
lookup[pattern.name] = pattern
|
||||
|
||||
for line in lines:
|
||||
try:
|
||||
if line.strip()[0] == "#": # comments
|
||||
continue
|
||||
line = line.split('#', 1)[0] # allows for comments
|
||||
type, data = line.rsplit('=', 1)
|
||||
type, data = type.strip(), data.split(',')
|
||||
except:
|
||||
pyfalog.warning("Data isn't in correct format, continue to next line.")
|
||||
continue
|
||||
|
||||
if type != "TargetResists":
|
||||
continue
|
||||
|
||||
numPatterns += 1
|
||||
name, data = data[0], data[1:5]
|
||||
fields = {}
|
||||
|
||||
for index, val in enumerate(data):
|
||||
val = float(val)
|
||||
try:
|
||||
assert 0 <= val <= 100
|
||||
fields["%sAmount" % cls.DAMAGE_TYPES[index]] = val / 100
|
||||
except:
|
||||
pyfalog.warning("Caught unhandled exception in import patterns.")
|
||||
continue
|
||||
|
||||
if len(fields) == 4: # Avoid possible blank lines
|
||||
if name.strip() in lookup:
|
||||
pattern = lookup[name.strip()]
|
||||
pattern.update(**fields)
|
||||
eos.db.save(pattern)
|
||||
else:
|
||||
pattern = TargetResists(**fields)
|
||||
pattern.name = name.strip()
|
||||
eos.db.save(pattern)
|
||||
patterns.append(pattern)
|
||||
|
||||
eos.db.commit()
|
||||
|
||||
return patterns, numPatterns
|
||||
|
||||
EXPORT_FORMAT = "TargetResists = %s,%.1f,%.1f,%.1f,%.1f\n"
|
||||
|
||||
@classmethod
|
||||
def exportPatterns(cls, *patterns):
|
||||
out = "# Exported from pyfa\n#\n"
|
||||
out += "# Values are in following format:\n"
|
||||
out += "# TargetResists = [name],[EM %],[Thermal %],[Kinetic %],[Explosive %]\n\n"
|
||||
for dp in patterns:
|
||||
out += cls.EXPORT_FORMAT % (
|
||||
dp.name,
|
||||
dp.emAmount * 100,
|
||||
dp.thermalAmount * 100,
|
||||
dp.kineticAmount * 100,
|
||||
dp.explosiveAmount * 100
|
||||
)
|
||||
|
||||
return out.strip()
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
p = TargetResists(self.emAmount, self.thermalAmount, self.kineticAmount, self.explosiveAmount)
|
||||
p.name = "%s copy" % self.name
|
||||
return p
|
||||
@@ -24,7 +24,7 @@ import string
|
||||
from sqlalchemy.orm import validates
|
||||
|
||||
|
||||
class User(object):
|
||||
class User:
|
||||
def __init__(self, username, password=None, admin=False):
|
||||
self.username = username
|
||||
if password is not None:
|
||||
|
||||
@@ -6,10 +6,11 @@ from utils.repr import makeReprStr
|
||||
|
||||
class CycleInfo:
|
||||
|
||||
def __init__(self, activeTime, inactiveTime, quantity):
|
||||
def __init__(self, activeTime, inactiveTime, quantity, isInactivityReload):
|
||||
self.activeTime = activeTime
|
||||
self.inactiveTime = inactiveTime
|
||||
self.quantity = quantity
|
||||
self.isInactivityReload = isInactivityReload
|
||||
|
||||
@property
|
||||
def averageTime(self):
|
||||
@@ -18,7 +19,7 @@ class CycleInfo:
|
||||
def iterCycles(self):
|
||||
i = 0
|
||||
while i < self.quantity:
|
||||
yield self.activeTime, self.inactiveTime
|
||||
yield self.activeTime, self.inactiveTime, self.isInactivityReload
|
||||
i += 1
|
||||
|
||||
def _getCycleQuantity(self):
|
||||
@@ -28,7 +29,7 @@ class CycleInfo:
|
||||
return (self.activeTime + self.inactiveTime) * self.quantity
|
||||
|
||||
def __repr__(self):
|
||||
spec = ['activeTime', 'inactiveTime', 'quantity']
|
||||
spec = ['activeTime', 'inactiveTime', 'quantity', 'isInactivityReload']
|
||||
return makeReprStr(self, spec)
|
||||
|
||||
|
||||
@@ -47,8 +48,8 @@ class CycleSequence:
|
||||
i = 0
|
||||
while i < self.quantity:
|
||||
for cycleInfo in self.sequence:
|
||||
for cycleTime, inactiveTime in cycleInfo.iterCycles():
|
||||
yield cycleTime, inactiveTime
|
||||
for cycleTime, inactiveTime, isInactivityReload in cycleInfo.iterCycles():
|
||||
yield cycleTime, inactiveTime, isInactivityReload
|
||||
i += 1
|
||||
|
||||
def _getCycleQuantity(self):
|
||||
|
||||
3
eos/utils/default.py
Normal file
3
eos/utils/default.py
Normal file
@@ -0,0 +1,3 @@
|
||||
class DEFAULT:
|
||||
"""Singleton class to signify default argument value."""
|
||||
pass
|
||||
@@ -18,7 +18,7 @@ keepDigits = int(sys.float_info.dig / 2)
|
||||
|
||||
def floatUnerr(value):
|
||||
"""Round possible float number error, killing some precision in process."""
|
||||
if value == 0:
|
||||
if value in (0, math.inf):
|
||||
return value
|
||||
# Find round factor, taking into consideration that we want to keep at least
|
||||
# predefined amount of significant digits
|
||||
|
||||
39
eos/utils/pyinst_support.py
Normal file
39
eos/utils/pyinst_support.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Slightly modified version of function taken from here:
|
||||
https://github.com/pyinstaller/pyinstaller/issues/1905#issuecomment-525221546
|
||||
"""
|
||||
|
||||
|
||||
import pkgutil
|
||||
|
||||
|
||||
def iterNamespace(name, path):
|
||||
"""Pyinstaller-compatible namespace iteration.
|
||||
|
||||
Yields the name of all modules found at a given Fully-qualified path.
|
||||
|
||||
To have it running with pyinstaller, it requires to ensure a hook inject the
|
||||
"hidden" modules from your plugins folder inside the executable:
|
||||
|
||||
- if your plugins are under the ``myappname/pluginfolder`` module
|
||||
- create a file ``specs/hook-<myappname.pluginfolder>.py``
|
||||
- content of this file should be:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from PyInstaller.utils.hooks import collect_submodules
|
||||
hiddenimports = collect_submodules('<myappname.pluginfolder>')
|
||||
"""
|
||||
prefix = name + "."
|
||||
for p in pkgutil.iter_modules(path, prefix):
|
||||
yield p[1]
|
||||
|
||||
# special handling when the package is bundled with PyInstaller 3.5
|
||||
# See https://github.com/pyinstaller/pyinstaller/issues/1905#issuecomment-445787510
|
||||
toc = set()
|
||||
for importer in pkgutil.iter_importers(name.partition(".")[0]):
|
||||
if hasattr(importer, 'toc'):
|
||||
toc |= importer.toc
|
||||
for name in toc:
|
||||
if name.startswith(prefix):
|
||||
yield name
|
||||
27
eos/utils/round.py
Normal file
27
eos/utils/round.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import math
|
||||
|
||||
|
||||
def roundToPrec(val, prec, nsValue=None):
|
||||
"""
|
||||
nsValue: custom value which should be used to determine normalization shift
|
||||
"""
|
||||
# We're not rounding integers anyway
|
||||
# Also make sure that we do not ask to calculate logarithm of zero
|
||||
if int(val) == val:
|
||||
return int(val)
|
||||
roundFactor = int(prec - math.floor(math.log10(abs(val if nsValue is None else nsValue))) - 1)
|
||||
# But we don't want to round integers
|
||||
if roundFactor < 0:
|
||||
roundFactor = 0
|
||||
# Do actual rounding
|
||||
val = round(val, roundFactor)
|
||||
# Make sure numbers with .0 part designating float don't get through
|
||||
if int(val) == val:
|
||||
val = int(val)
|
||||
return val
|
||||
|
||||
|
||||
def roundDec(val, prec):
|
||||
if int(val) == val:
|
||||
return int(val)
|
||||
return round(val, prec)
|
||||
@@ -18,6 +18,7 @@
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
import math
|
||||
from collections import namedtuple
|
||||
|
||||
from eos.const import SpoolType
|
||||
@@ -36,15 +37,33 @@ def calculateSpoolup(modMaxValue, modStepValue, modCycleTime, spoolType, spoolAm
|
||||
"""
|
||||
if not modMaxValue or not modStepValue:
|
||||
return 0, 0, 0
|
||||
if spoolType == SpoolType.SCALE:
|
||||
cycles = int(floatUnerr(spoolAmount * modMaxValue / modStepValue))
|
||||
return cycles * modStepValue, cycles, cycles * modCycleTime
|
||||
if spoolType == SpoolType.SPOOL_SCALE:
|
||||
# Find out at which point of spoolup scale we're on, find out how many cycles
|
||||
# is enough to reach it and recalculate spoolup value for that amount of cycles
|
||||
cycles = math.ceil(floatUnerr(modMaxValue * spoolAmount / modStepValue))
|
||||
spoolValue = min(modMaxValue, cycles * modStepValue)
|
||||
return spoolValue, cycles, cycles * modCycleTime
|
||||
elif spoolType == SpoolType.CYCLE_SCALE:
|
||||
# For cycle scale, find out max amount of cycles and scale against it
|
||||
cycles = round(spoolAmount * math.ceil(floatUnerr(modMaxValue / modStepValue)))
|
||||
spoolValue = min(modMaxValue, cycles * modStepValue)
|
||||
return spoolValue, cycles, cycles * modCycleTime
|
||||
elif spoolType == SpoolType.TIME:
|
||||
cycles = min(int(floatUnerr(spoolAmount / modCycleTime)), int(floatUnerr(modMaxValue / modStepValue)))
|
||||
return cycles * modStepValue, cycles, cycles * modCycleTime
|
||||
cycles = min(
|
||||
# How many full cycles mod had by passed time
|
||||
math.floor(floatUnerr(spoolAmount / modCycleTime)),
|
||||
# Max amount of cycles
|
||||
math.ceil(floatUnerr(modMaxValue / modStepValue)))
|
||||
spoolValue = min(modMaxValue, cycles * modStepValue)
|
||||
return spoolValue, cycles, cycles * modCycleTime
|
||||
elif spoolType == SpoolType.CYCLES:
|
||||
cycles = min(int(spoolAmount), int(floatUnerr(modMaxValue / modStepValue)))
|
||||
return cycles * modStepValue, cycles, cycles * modCycleTime
|
||||
cycles = min(
|
||||
# Consider full cycles only
|
||||
math.floor(spoolAmount),
|
||||
# Max amount of cycles
|
||||
math.ceil(floatUnerr(modMaxValue / modStepValue)))
|
||||
spoolValue = min(modMaxValue, cycles * modStepValue)
|
||||
return spoolValue, cycles, cycles * modCycleTime
|
||||
else:
|
||||
return 0, 0, 0
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
from eos.utils.float import floatUnerr
|
||||
from utils.repr import makeReprStr
|
||||
|
||||
|
||||
class DmgTypes:
|
||||
"""Container for damage data stats."""
|
||||
|
||||
@@ -39,12 +43,14 @@ class DmgTypes:
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, DmgTypes):
|
||||
return NotImplemented
|
||||
return all((
|
||||
self.em == other.em,
|
||||
self.thermal == other.thermal,
|
||||
self.kinetic == other.kinetic,
|
||||
self.explosive == other.explosive,
|
||||
self.total == other.total))
|
||||
# Round for comparison's sake because often damage profiles are
|
||||
# generated from data which includes float errors
|
||||
return (
|
||||
floatUnerr(self.em) == floatUnerr(other.em) and
|
||||
floatUnerr(self.thermal) == floatUnerr(other.thermal) and
|
||||
floatUnerr(self.kinetic) == floatUnerr(other.kinetic) and
|
||||
floatUnerr(self.explosive) == floatUnerr(other.explosive) and
|
||||
floatUnerr(self.total) == floatUnerr(other.total))
|
||||
|
||||
def __bool__(self):
|
||||
return any((
|
||||
@@ -68,3 +74,144 @@ class DmgTypes:
|
||||
self.explosive += other.explosive
|
||||
self._calcTotal()
|
||||
return self
|
||||
|
||||
def __mul__(self, mul):
|
||||
return type(self)(
|
||||
em=self.em * mul,
|
||||
thermal=self.thermal * mul,
|
||||
kinetic=self.kinetic * mul,
|
||||
explosive=self.explosive * mul)
|
||||
|
||||
def __imul__(self, mul):
|
||||
if mul == 1:
|
||||
return
|
||||
self.em *= mul
|
||||
self.thermal *= mul
|
||||
self.kinetic *= mul
|
||||
self.explosive *= mul
|
||||
self._calcTotal()
|
||||
return self
|
||||
|
||||
def __truediv__(self, div):
|
||||
return type(self)(
|
||||
em=self.em / div,
|
||||
thermal=self.thermal / div,
|
||||
kinetic=self.kinetic / div,
|
||||
explosive=self.explosive / div)
|
||||
|
||||
def __itruediv__(self, div):
|
||||
if div == 1:
|
||||
return
|
||||
self.em /= div
|
||||
self.thermal /= div
|
||||
self.kinetic /= div
|
||||
self.explosive /= div
|
||||
self._calcTotal()
|
||||
return self
|
||||
|
||||
def __repr__(self):
|
||||
spec = DmgTypes.names()
|
||||
spec.append('total')
|
||||
return makeReprStr(self, spec)
|
||||
|
||||
@staticmethod
|
||||
def names(short=None, postProcessor=None):
|
||||
value = ['em', 'th', 'kin', 'exp'] if short else ['em', 'thermal', 'kinetic', 'explosive']
|
||||
|
||||
if postProcessor:
|
||||
value = [postProcessor(x) for x in value]
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class RRTypes:
|
||||
"""Container for tank data stats."""
|
||||
|
||||
def __init__(self, shield, armor, hull, capacitor):
|
||||
self.shield = shield
|
||||
self.armor = armor
|
||||
self.hull = hull
|
||||
self.capacitor = capacitor
|
||||
|
||||
# Iterator is needed to support tuple-style unpacking
|
||||
def __iter__(self):
|
||||
yield self.shield
|
||||
yield self.armor
|
||||
yield self.hull
|
||||
yield self.capacitor
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, RRTypes):
|
||||
return NotImplemented
|
||||
# Round for comparison's sake because often tanking numbers are
|
||||
# generated from data which includes float errors
|
||||
return (
|
||||
floatUnerr(self.shield) == floatUnerr(other.shield) and
|
||||
floatUnerr(self.armor) == floatUnerr(other.armor) and
|
||||
floatUnerr(self.hull) == floatUnerr(other.hull) and
|
||||
floatUnerr(self.capacitor) == floatUnerr(other.capacitor))
|
||||
|
||||
def __bool__(self):
|
||||
return any((self.shield, self.armor, self.hull, self.capacitor))
|
||||
|
||||
def __add__(self, other):
|
||||
return type(self)(
|
||||
shield=self.shield + other.shield,
|
||||
armor=self.armor + other.armor,
|
||||
hull=self.hull + other.hull,
|
||||
capacitor=self.capacitor + other.capacitor)
|
||||
|
||||
def __iadd__(self, other):
|
||||
self.shield += other.shield
|
||||
self.armor += other.armor
|
||||
self.hull += other.hull
|
||||
self.capacitor += other.capacitor
|
||||
return self
|
||||
|
||||
def __mul__(self, mul):
|
||||
return type(self)(
|
||||
shield=self.shield * mul,
|
||||
armor=self.armor * mul,
|
||||
hull=self.hull * mul,
|
||||
capacitor=self.capacitor * mul)
|
||||
|
||||
def __imul__(self, mul):
|
||||
if mul == 1:
|
||||
return
|
||||
self.shield *= mul
|
||||
self.armor *= mul
|
||||
self.hull *= mul
|
||||
self.capacitor *= mul
|
||||
return self
|
||||
|
||||
def __truediv__(self, div):
|
||||
return type(self)(
|
||||
shield=self.shield / div,
|
||||
armor=self.armor / div,
|
||||
hull=self.hull / div,
|
||||
capacitor=self.capacitor / div)
|
||||
|
||||
def __itruediv__(self, div):
|
||||
if div == 1:
|
||||
return self
|
||||
self.shield /= div
|
||||
self.armor /= div
|
||||
self.hull /= div
|
||||
self.capacitor /= div
|
||||
return self
|
||||
|
||||
def __repr__(self):
|
||||
spec = RRTypes.names(False)
|
||||
return makeReprStr(self, spec)
|
||||
|
||||
@staticmethod
|
||||
def names(ehpOnly=True, postProcessor=None):
|
||||
value = ['shield', 'armor', 'hull']
|
||||
|
||||
if not ehpOnly:
|
||||
value.append('capacitor')
|
||||
|
||||
if postProcessor:
|
||||
value = [postProcessor(x) for x in value]
|
||||
|
||||
return value
|
||||
|
||||
22
graphs/__init__.py
Normal file
22
graphs/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from .gui.canvasPanel import graphFrame_enabled
|
||||
from .gui.frame import GraphFrame
|
||||
37
graphs/calc.py
Normal file
37
graphs/calc.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from service.settings import GraphSettings
|
||||
|
||||
|
||||
def checkLockRange(src, distance):
|
||||
if distance is None:
|
||||
return True
|
||||
if GraphSettings.getInstance().get('ignoreLockRange'):
|
||||
return True
|
||||
return distance <= src.item.maxTargetRange
|
||||
|
||||
|
||||
def checkDroneControlRange(src, distance):
|
||||
if distance is None:
|
||||
return True
|
||||
if GraphSettings.getInstance().get('ignoreDCR'):
|
||||
return True
|
||||
return distance <= src.item.extraAttributes['droneControlRange']
|
||||
30
graphs/data/__init__.py
Normal file
30
graphs/data/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from . import fitDamageStats
|
||||
from . import fitEwarStats
|
||||
from . import fitRemoteReps
|
||||
from . import fitShieldRegen
|
||||
from . import fitCapacitor
|
||||
from . import fitMobility
|
||||
from . import fitWarpTime
|
||||
from . import fitLockTime
|
||||
# Hidden graphs, available via ctrl-alt-g
|
||||
from . import fitEcmBurstScanresDamps
|
||||
23
graphs/data/base/__init__.py
Normal file
23
graphs/data/base/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
from .cache import FitDataCache
|
||||
from .defs import XDef, YDef, VectorDef, Input, InputCheckbox
|
||||
from .getter import PointGetter, SmoothPointGetter
|
||||
from .graph import FitGraph
|
||||
@@ -18,26 +18,14 @@
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class Graph(object):
|
||||
views = []
|
||||
|
||||
@classmethod
|
||||
def register(cls):
|
||||
Graph.views.append(cls)
|
||||
class FitDataCache:
|
||||
|
||||
def __init__(self):
|
||||
self.name = ""
|
||||
self._data = {}
|
||||
|
||||
def getFields(self, fit, fields):
|
||||
raise NotImplementedError()
|
||||
def clearForFit(self, fitID):
|
||||
if fitID in self._data:
|
||||
del self._data[fitID]
|
||||
|
||||
def getIcons(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def redrawOnEffectiveChange(self):
|
||||
return False
|
||||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from gui.builtinGraphs import *
|
||||
def clearAll(self):
|
||||
self._data.clear()
|
||||
136
graphs/data/base/defs.py
Normal file
136
graphs/data/base/defs.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label'))
|
||||
|
||||
|
||||
class YDef:
|
||||
|
||||
def __init__(self, handle, unit, label, selectorLabel=None, hidden=False):
|
||||
self.handle = handle
|
||||
self.unit = unit
|
||||
self.label = label
|
||||
self._selectorLabel = selectorLabel
|
||||
self.hidden = hidden
|
||||
|
||||
@property
|
||||
def selectorLabel(self):
|
||||
if self._selectorLabel is not None:
|
||||
return self._selectorLabel
|
||||
return self.label
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.handle, self.unit, self.label, self._selectorLabel))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, YDef):
|
||||
return False
|
||||
return all((
|
||||
self.handle == other.handle,
|
||||
self.unit == other.unit,
|
||||
self.label == other.label,
|
||||
self._selectorLabel == other._selectorLabel))
|
||||
|
||||
|
||||
class XDef:
|
||||
|
||||
def __init__(self, handle, unit, label, mainInput, selectorLabel=None, hidden=False):
|
||||
self.handle = handle
|
||||
self.unit = unit
|
||||
self.label = label
|
||||
self.mainInput = mainInput
|
||||
self._selectorLabel = selectorLabel
|
||||
self.hidden = hidden
|
||||
|
||||
@property
|
||||
def selectorLabel(self):
|
||||
if self._selectorLabel is not None:
|
||||
return self._selectorLabel
|
||||
return self.label
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.handle, self.unit, self.label, self.mainInput, self._selectorLabel))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, XDef):
|
||||
return False
|
||||
return all((
|
||||
self.handle == other.handle,
|
||||
self.unit == other.unit,
|
||||
self.label == other.label,
|
||||
self.mainInput == other.mainInput,
|
||||
self._selectorLabel == other._selectorLabel))
|
||||
|
||||
|
||||
class Input:
|
||||
|
||||
def __init__(self, handle, unit, label, iconID, defaultValue, defaultRange, mainTooltip=None, secondaryTooltip=None, conditions=()):
|
||||
self.handle = handle
|
||||
self.unit = unit
|
||||
self.label = label
|
||||
self.iconID = iconID
|
||||
self.defaultValue = defaultValue
|
||||
self.defaultRange = defaultRange
|
||||
self.mainTooltip = mainTooltip
|
||||
self.secondaryTooltip = secondaryTooltip
|
||||
# Format: ((x condition, y condition), (x condition, y condition), ...)
|
||||
self.conditions = tuple(conditions)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.handle, self.unit, self.label, self.iconID, self.defaultValue, self.defaultRange, self.mainTooltip, self.secondaryTooltip, self.conditions))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Input):
|
||||
return False
|
||||
return all((
|
||||
self.handle == other.handle,
|
||||
self.unit == other.unit,
|
||||
self.label == other.label,
|
||||
self.iconID == other.iconID,
|
||||
self.defaultValue == other.defaultValue,
|
||||
self.defaultRange == other.defaultRange,
|
||||
self.mainTooltip == other.mainTooltip,
|
||||
self.secondaryTooltip == other.secondaryTooltip,
|
||||
self.conditions == other.conditions))
|
||||
|
||||
|
||||
class InputCheckbox:
|
||||
|
||||
def __init__(self, handle, label, defaultValue, conditions=()):
|
||||
self.handle = handle
|
||||
self.label = label
|
||||
self.defaultValue = defaultValue
|
||||
# Format: ((x condition, y condition), (x condition, y condition), ...)
|
||||
self.conditions = tuple(conditions)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.handle, self.label, self.defaultValue, self.conditions))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Input):
|
||||
return False
|
||||
return all((
|
||||
self.handle == other.handle,
|
||||
self.label == other.label,
|
||||
self.defaultValue == other.defaultValue,
|
||||
self.conditions == other.conditions))
|
||||
95
graphs/data/base/getter.py
Normal file
95
graphs/data/base/getter.py
Normal file
@@ -0,0 +1,95 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
class PointGetter(metaclass=ABCMeta):
|
||||
|
||||
def __init__(self, graph):
|
||||
self.graph = graph
|
||||
|
||||
@abstractmethod
|
||||
def getRange(self, xRange, miscParams, src, tgt):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def getPoint(self, x, miscParams, src, tgt):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SmoothPointGetter(PointGetter, metaclass=ABCMeta):
|
||||
|
||||
_baseResolution = 200
|
||||
_extraDepth = 0
|
||||
|
||||
def getRange(self, xRange, miscParams, src, tgt):
|
||||
xs = []
|
||||
ys = []
|
||||
commonData = self._getCommonData(miscParams=miscParams, src=src, tgt=tgt)
|
||||
|
||||
def addExtraPoints(x1, y1, x2, y2, depth):
|
||||
if depth <= 0 or y1 == y2:
|
||||
return
|
||||
newX = (x1 + x2) / 2
|
||||
newY = self._calculatePoint(x=newX, miscParams=miscParams, src=src, tgt=tgt, commonData=commonData)
|
||||
addExtraPoints(x1=prevX, y1=prevY, x2=newX, y2=newY, depth=depth - 1)
|
||||
xs.append(newX)
|
||||
ys.append(newY)
|
||||
addExtraPoints(x1=newX, y1=newY, x2=x2, y2=y2, depth=depth - 1)
|
||||
|
||||
prevX = None
|
||||
prevY = None
|
||||
# Go through X points defined by our resolution setting
|
||||
for x in self._xIterLinear(xRange):
|
||||
y = self._calculatePoint(x=x, miscParams=miscParams, src=src, tgt=tgt, commonData=commonData)
|
||||
if prevX is not None and prevY is not None:
|
||||
# And if Y values of adjacent data points are not equal, add extra points
|
||||
# depending on extra depth setting
|
||||
addExtraPoints(x1=prevX, y1=prevY, x2=x, y2=y, depth=self._extraDepth)
|
||||
prevX = x
|
||||
prevY = y
|
||||
xs.append(x)
|
||||
ys.append(y)
|
||||
return xs, ys
|
||||
|
||||
def getPoint(self, x, miscParams, src, tgt):
|
||||
commonData = self._getCommonData(miscParams=miscParams, src=src, tgt=tgt)
|
||||
return self._calculatePoint(x=x, miscParams=miscParams, src=src, tgt=tgt, commonData=commonData)
|
||||
|
||||
def _xIterLinear(self, xRange):
|
||||
xLow = min(xRange)
|
||||
xHigh = max(xRange)
|
||||
# Resolution defines amount of ranges between points here,
|
||||
# not amount of points
|
||||
step = (xHigh - xLow) / self._baseResolution
|
||||
if step == 0 or math.isnan(step):
|
||||
yield xLow
|
||||
else:
|
||||
for i in range(self._baseResolution + 1):
|
||||
yield xLow + step * i
|
||||
|
||||
def _getCommonData(self, miscParams, src, tgt):
|
||||
return {}
|
||||
|
||||
@abstractmethod
|
||||
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
|
||||
raise NotImplementedError
|
||||
299
graphs/data/base/graph.py
Normal file
299
graphs/data/base/graph.py
Normal file
@@ -0,0 +1,299 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections import OrderedDict
|
||||
|
||||
from eos.utils.float import floatUnerr
|
||||
from service.const import GraphCacheCleanupReason
|
||||
|
||||
|
||||
class FitGraph(metaclass=ABCMeta):
|
||||
|
||||
# UI stuff
|
||||
hidden = False
|
||||
views = []
|
||||
viewMap = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls):
|
||||
FitGraph.views.append(cls)
|
||||
FitGraph.viewMap[cls.internalName] = cls
|
||||
|
||||
def __init__(self):
|
||||
# Format: {(fit ID, target type, target ID): {(xSpec, ySpec): (xs, ys)}}
|
||||
self._plotCache = {}
|
||||
# Format: {(fit ID, target type, target ID): {(xSpec, ySpec): {x: y}}}
|
||||
self._pointCache = {}
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def internalName(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def yDefs(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def yDefMap(self):
|
||||
return OrderedDict(((y.handle, y.unit), y) for y in self.yDefs)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def xDefs(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def xDefMap(self):
|
||||
return OrderedDict(((x.handle, x.unit), x) for x in self.xDefs)
|
||||
|
||||
@property
|
||||
def inputs(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def inputMap(self):
|
||||
return OrderedDict(((i.handle, i.unit), i) for i in self.inputs)
|
||||
|
||||
checkboxes = ()
|
||||
|
||||
@property
|
||||
def checkboxesMap(self):
|
||||
return OrderedDict((ec.handle, ec) for ec in self.checkboxes)
|
||||
|
||||
hasTargets = False
|
||||
srcVectorDef = None
|
||||
tgtVectorDef = None
|
||||
srcExtraCols = ()
|
||||
tgtExtraCols = ()
|
||||
usesHpEffectivity = False
|
||||
|
||||
def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, src, tgt=None):
|
||||
cacheKey = self._makeCacheKey(src=src, tgt=tgt)
|
||||
try:
|
||||
plotData = self._plotCache[cacheKey][(ySpec, xSpec)]
|
||||
except KeyError:
|
||||
plotData = self._calcPlotPoints(
|
||||
mainInput=mainInput, miscInputs=miscInputs,
|
||||
xSpec=xSpec, ySpec=ySpec, src=src, tgt=tgt)
|
||||
self._plotCache.setdefault(cacheKey, {})[(ySpec, xSpec)] = plotData
|
||||
return plotData
|
||||
|
||||
def getPoint(self, x, miscInputs, xSpec, ySpec, src, tgt=None):
|
||||
cacheKey = self._makeCacheKey(src=src, tgt=tgt)
|
||||
try:
|
||||
y = self._pointCache[cacheKey][(ySpec, xSpec)][x]
|
||||
except KeyError:
|
||||
y = self._calcPoint(x=x, miscInputs=miscInputs, xSpec=xSpec, ySpec=ySpec, src=src, tgt=tgt)
|
||||
self._pointCache.setdefault(cacheKey, {}).setdefault((ySpec, xSpec), {})[x] = y
|
||||
return y
|
||||
|
||||
def clearCache(self, reason, extraData=None):
|
||||
caches = (self._plotCache, self._pointCache)
|
||||
plotKeysToClear = set()
|
||||
# If fit changed - clear plots which concern this fit
|
||||
if reason in (GraphCacheCleanupReason.fitChanged, GraphCacheCleanupReason.fitRemoved):
|
||||
for cache in caches:
|
||||
for cacheKey in cache:
|
||||
cacheFitID, cacheTgtType, cacheTgtID = cacheKey
|
||||
if extraData == cacheFitID:
|
||||
plotKeysToClear.add(cacheKey)
|
||||
elif cacheTgtType == 'fit' and extraData == cacheTgtID:
|
||||
plotKeysToClear.add(cacheKey)
|
||||
# Same for profile
|
||||
elif reason in (GraphCacheCleanupReason.profileChanged, GraphCacheCleanupReason.profileRemoved):
|
||||
for cache in caches:
|
||||
for cacheKey in cache:
|
||||
cacheFitID, cacheTgtType, cacheTgtID = cacheKey
|
||||
if cacheTgtType == 'profile' and extraData == cacheTgtID:
|
||||
plotKeysToClear.add(cacheKey)
|
||||
# Target fit resist mode changed
|
||||
elif reason == GraphCacheCleanupReason.resistModeChanged:
|
||||
for cache in caches:
|
||||
for cacheKey in cache:
|
||||
cacheFitID, cacheTgtType, cacheTgtID = cacheKey
|
||||
if cacheTgtType == 'fit' and extraData == cacheTgtID:
|
||||
plotKeysToClear.add(cacheKey)
|
||||
# Wipe out whole plot cache otherwise
|
||||
else:
|
||||
for cache in caches:
|
||||
for cacheKey in cache:
|
||||
plotKeysToClear.add(cacheKey)
|
||||
# Do actual cleanup
|
||||
for cache in caches:
|
||||
for cacheKey in plotKeysToClear:
|
||||
try:
|
||||
del cache[cacheKey]
|
||||
except KeyError:
|
||||
pass
|
||||
# Process any internal caches graphs might have
|
||||
self._clearInternalCache(reason, extraData)
|
||||
|
||||
def _makeCacheKey(self, src, tgt):
|
||||
if tgt is not None and tgt.isFit:
|
||||
tgtType = 'fit'
|
||||
tgtItemID = tgt.item.ID
|
||||
elif tgt is not None and tgt.isProfile:
|
||||
tgtType = 'profile'
|
||||
tgtItemID = tgt.item.ID
|
||||
else:
|
||||
tgtType = None
|
||||
tgtItemID = None
|
||||
cacheKey = (src.item.ID, tgtType, tgtItemID)
|
||||
return cacheKey
|
||||
|
||||
def _clearInternalCache(self, reason, extraData):
|
||||
return
|
||||
|
||||
# Calculation stuff
|
||||
def _calcPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, src, tgt):
|
||||
mainParamRange = self._normalizeMain(mainInput=mainInput, src=src, tgt=tgt)
|
||||
miscParams = self._normalizeMisc(miscInputs=miscInputs, src=src, tgt=tgt)
|
||||
mainParamRange = self._limitMain(mainParamRange=mainParamRange, src=src, tgt=tgt)
|
||||
miscParams = self._limitMisc(miscParams=miscParams, src=src, tgt=tgt)
|
||||
xs, ys = self._getPlotPoints(
|
||||
xRange=mainParamRange[1], miscParams=miscParams,
|
||||
xSpec=xSpec, ySpec=ySpec, src=src, tgt=tgt)
|
||||
ys = self._denormalizeValues(values=ys, axisSpec=ySpec, src=src, tgt=tgt)
|
||||
# Sometimes x denormalizer may fail (e.g. during conversion of 0 ship speed to %).
|
||||
# If both inputs and outputs are in %, do some extra processing to at least have
|
||||
# proper graph which shows the same value over whole specified relative parameter
|
||||
# range
|
||||
try:
|
||||
xs = self._denormalizeValues(values=xs, axisSpec=xSpec, src=src, tgt=tgt)
|
||||
except ZeroDivisionError:
|
||||
if mainInput.unit == xSpec.unit == '%' and len(set(floatUnerr(y) for y in ys)) == 1:
|
||||
xs = [min(mainInput.value), max(mainInput.value)]
|
||||
ys = [ys[0], ys[0]]
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
# Same for NaN which means we tried to denormalize infinity values, which might be the
|
||||
# case for the ideal target profile with infinite signature radius
|
||||
if mainInput.unit == xSpec.unit == '%' and all(math.isnan(x) for x in xs):
|
||||
xs = [min(mainInput.value), max(mainInput.value)]
|
||||
ys = [ys[0], ys[0]]
|
||||
return xs, ys
|
||||
|
||||
def _calcPoint(self, x, miscInputs, xSpec, ySpec, src, tgt):
|
||||
x = self._normalizeValue(value=x, axisSpec=xSpec, src=src, tgt=tgt)
|
||||
miscParams = self._normalizeMisc(miscInputs=miscInputs, src=src, tgt=tgt)
|
||||
miscParams = self._limitMisc(miscParams=miscParams, src=src, tgt=tgt)
|
||||
y = self._getPoint(x=x, miscParams=miscParams, xSpec=xSpec, ySpec=ySpec, src=src, tgt=tgt)
|
||||
y = self._denormalizeValue(value=y, axisSpec=ySpec, src=src, tgt=tgt)
|
||||
return y
|
||||
|
||||
_normalizers = {}
|
||||
|
||||
def _normalizeMain(self, mainInput, src, tgt):
|
||||
key = (mainInput.handle, mainInput.unit)
|
||||
if key in self._normalizers:
|
||||
normalizer = self._normalizers[key]
|
||||
mainParamRange = (mainInput.handle, tuple(normalizer(v, src, tgt) for v in mainInput.value))
|
||||
else:
|
||||
mainParamRange = (mainInput.handle, mainInput.value)
|
||||
return mainParamRange
|
||||
|
||||
def _normalizeMisc(self, miscInputs, src, tgt):
|
||||
miscParams = {}
|
||||
for miscInput in miscInputs:
|
||||
key = (miscInput.handle, miscInput.unit)
|
||||
if key in self._normalizers:
|
||||
normalizer = self._normalizers[key]
|
||||
miscParams[miscInput.handle] = normalizer(miscInput.value, src, tgt)
|
||||
else:
|
||||
miscParams[miscInput.handle] = miscInput.value
|
||||
return miscParams
|
||||
|
||||
def _normalizeValue(self, value, axisSpec, src, tgt):
|
||||
key = (axisSpec.handle, axisSpec.unit)
|
||||
if key in self._normalizers:
|
||||
normalizer = self._normalizers[key]
|
||||
value = normalizer(value, src, tgt)
|
||||
return value
|
||||
|
||||
_limiters = {}
|
||||
|
||||
def _limitMain(self, mainParamRange, src, tgt):
|
||||
mainHandle, mainValue = mainParamRange
|
||||
if mainHandle in self._limiters:
|
||||
limiter = self._limiters[mainHandle]
|
||||
mainParamRange = (mainHandle, tuple(self.__limitToRange(v, limiter(src, tgt)) for v in mainValue))
|
||||
return mainParamRange
|
||||
|
||||
def _limitMisc(self, miscParams, src, tgt):
|
||||
for miscHandle in miscParams:
|
||||
if miscHandle in self._limiters:
|
||||
limiter = self._limiters[miscHandle]
|
||||
miscValue = miscParams[miscHandle]
|
||||
miscParams[miscHandle] = self.__limitToRange(miscValue, limiter(src, tgt))
|
||||
return miscParams
|
||||
|
||||
@staticmethod
|
||||
def __limitToRange(val, limitRange):
|
||||
if val is None:
|
||||
return None
|
||||
val = max(val, min(limitRange))
|
||||
val = min(val, max(limitRange))
|
||||
return val
|
||||
|
||||
_getters = {}
|
||||
|
||||
def _getPlotPoints(self, xRange, miscParams, xSpec, ySpec, src, tgt):
|
||||
try:
|
||||
getterClass = self._getters[(xSpec.handle, ySpec.handle)]
|
||||
except KeyError:
|
||||
return [], []
|
||||
else:
|
||||
getter = getterClass(graph=self)
|
||||
return getter.getRange(xRange=xRange, miscParams=miscParams, src=src, tgt=tgt)
|
||||
|
||||
def _getPoint(self, x, miscParams, xSpec, ySpec, src, tgt):
|
||||
try:
|
||||
getterClass = self._getters[(xSpec.handle, ySpec.handle)]
|
||||
except KeyError:
|
||||
return [], []
|
||||
else:
|
||||
getter = getterClass(graph=self)
|
||||
return getter.getPoint(x=x, miscParams=miscParams, src=src, tgt=tgt)
|
||||
|
||||
_denormalizers = {}
|
||||
|
||||
def _denormalizeValues(self, values, axisSpec, src, tgt):
|
||||
key = (axisSpec.handle, axisSpec.unit)
|
||||
if key in self._denormalizers:
|
||||
denormalizer = self._denormalizers[key]
|
||||
values = [denormalizer(v, src, tgt) for v in values]
|
||||
return values
|
||||
|
||||
def _denormalizeValue(self, value, axisSpec, src, tgt):
|
||||
key = (axisSpec.handle, axisSpec.unit)
|
||||
if key in self._denormalizers:
|
||||
denormalizer = self._denormalizers[key]
|
||||
value = denormalizer(value, src, tgt)
|
||||
return value
|
||||
24
graphs/data/fitCapacitor/__init__.py
Normal file
24
graphs/data/fitCapacitor/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from .graph import FitCapacitorGraph
|
||||
|
||||
|
||||
FitCapacitorGraph.register()
|
||||
197
graphs/data/fitCapacitor/getter.py
Normal file
197
graphs/data/fitCapacitor/getter.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import math
|
||||
|
||||
from graphs.data.base import SmoothPointGetter
|
||||
|
||||
|
||||
class Time2CapAmountGetter(SmoothPointGetter):
|
||||
|
||||
def getRange(self, xRange, miscParams, src, tgt):
|
||||
# Use smooth getter when we're not using cap sim
|
||||
if not miscParams['useCapsim']:
|
||||
return super().getRange(xRange=xRange, miscParams=miscParams, src=src, tgt=tgt)
|
||||
capAmountT0 = miscParams['capAmountT0'] or 0
|
||||
capSimDataRaw = src.item.getCapSimData(startingCap=capAmountT0)
|
||||
# Same here, no cap sim data - use smooth getter which considers only regen
|
||||
if not capSimDataRaw:
|
||||
return super().getRange(xRange=xRange, miscParams=miscParams, src=src, tgt=tgt)
|
||||
capSimDataMaxTime = capSimDataRaw[-1][0]
|
||||
minTime, maxTime = xRange
|
||||
maxTime = min(maxTime, capSimDataMaxTime)
|
||||
maxPointXDistance = (maxTime - minTime) / self._baseResolution
|
||||
capSimDataInRange = {k: v for k, v in capSimDataRaw if minTime <= k <= maxTime}
|
||||
prevTime = minTime
|
||||
xs = []
|
||||
ys = []
|
||||
capSimDataBefore = {k: v for k, v in capSimDataRaw if k < minTime}
|
||||
# When time range lies to the right of last cap sim data point, return nothing
|
||||
if len(capSimDataBefore) > 0 and max(capSimDataBefore) == capSimDataMaxTime:
|
||||
return xs, ys
|
||||
maxCapAmount = src.item.ship.getModifiedItemAttr('capacitorCapacity')
|
||||
capRegenTime = src.item.ship.getModifiedItemAttr('rechargeRate') / 1000
|
||||
|
||||
def plotCapRegen(prevTime, prevCap, currentTime):
|
||||
subrangeAmount = math.ceil((currentTime - prevTime) / maxPointXDistance)
|
||||
subrangeLength = (currentTime - prevTime) / subrangeAmount
|
||||
for i in range(1, subrangeAmount + 1):
|
||||
subrangeTime = prevTime + subrangeLength * i
|
||||
subrangeCap = calculateCapAmount(
|
||||
maxCapAmount=maxCapAmount,
|
||||
capRegenTime=capRegenTime,
|
||||
capAmountT0=prevCap,
|
||||
time=subrangeTime - prevTime)
|
||||
xs.append(subrangeTime)
|
||||
ys.append(subrangeCap)
|
||||
|
||||
# Calculate starting cap for first value seen in our range
|
||||
if capSimDataBefore:
|
||||
timeBefore = max(capSimDataBefore)
|
||||
capBefore = capSimDataBefore[timeBefore]
|
||||
prevCap = calculateCapAmount(
|
||||
maxCapAmount=maxCapAmount,
|
||||
capRegenTime=capRegenTime,
|
||||
capAmountT0=capBefore,
|
||||
time=prevTime - timeBefore)
|
||||
else:
|
||||
prevCap = calculateCapAmount(
|
||||
maxCapAmount=maxCapAmount,
|
||||
capRegenTime=capRegenTime,
|
||||
capAmountT0=capAmountT0,
|
||||
time=prevTime)
|
||||
xs.append(prevTime)
|
||||
ys.append(prevCap)
|
||||
for currentTime in sorted(capSimDataInRange):
|
||||
if currentTime > prevTime:
|
||||
plotCapRegen(prevTime=prevTime, prevCap=prevCap, currentTime=currentTime)
|
||||
currentCap = capSimDataInRange[currentTime]
|
||||
xs.append(currentTime)
|
||||
ys.append(currentCap)
|
||||
prevTime = currentTime
|
||||
prevCap = currentCap
|
||||
if maxTime > prevTime:
|
||||
plotCapRegen(prevTime=prevTime, prevCap=prevCap, currentTime=maxTime)
|
||||
return xs, ys
|
||||
|
||||
def getPoint(self, x, miscParams, src, tgt):
|
||||
# Use smooth getter when we're not using cap sim
|
||||
if not miscParams['useCapsim']:
|
||||
return super().getPoint(x=x, miscParams=miscParams, src=src, tgt=tgt)
|
||||
capAmountT0 = miscParams['capAmountT0'] or 0
|
||||
capSimDataRaw = src.item.getCapSimData(startingCap=capAmountT0)
|
||||
# Same here, no cap sim data - use smooth getter which considers only regen
|
||||
if not capSimDataRaw:
|
||||
return super().getPoint(x=x, miscParams=miscParams, src=src, tgt=tgt)
|
||||
currentTime = x
|
||||
capSimDataBefore = {k: v for k, v in capSimDataRaw if k <= currentTime}
|
||||
capSimDataMaxTime = capSimDataRaw[-1][0]
|
||||
# When time range lies to the right of last cap sim data point, return nothing
|
||||
if len(capSimDataBefore) > 0 and max(capSimDataBefore) == capSimDataMaxTime:
|
||||
return None
|
||||
maxCapAmount = src.item.ship.getModifiedItemAttr('capacitorCapacity')
|
||||
capRegenTime = src.item.ship.getModifiedItemAttr('rechargeRate') / 1000
|
||||
if capSimDataBefore:
|
||||
timeBefore = max(capSimDataBefore)
|
||||
capBefore = capSimDataBefore[timeBefore]
|
||||
if timeBefore == currentTime:
|
||||
currentCap = capBefore
|
||||
else:
|
||||
currentCap = calculateCapAmount(
|
||||
maxCapAmount=maxCapAmount,
|
||||
capRegenTime=capRegenTime,
|
||||
capAmountT0=capBefore,
|
||||
time=currentTime - timeBefore)
|
||||
else:
|
||||
currentCap = calculateCapAmount(
|
||||
maxCapAmount=maxCapAmount,
|
||||
capRegenTime=capRegenTime,
|
||||
capAmountT0=capAmountT0,
|
||||
time=currentTime)
|
||||
return currentCap
|
||||
|
||||
def _getCommonData(self, miscParams, src, tgt):
|
||||
return {
|
||||
'maxCapAmount': src.item.ship.getModifiedItemAttr('capacitorCapacity'),
|
||||
'capRegenTime': src.item.ship.getModifiedItemAttr('rechargeRate') / 1000}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
|
||||
time = x
|
||||
capAmount = calculateCapAmount(
|
||||
maxCapAmount=commonData['maxCapAmount'],
|
||||
capRegenTime=commonData['capRegenTime'],
|
||||
capAmountT0=miscParams['capAmountT0'] or 0,
|
||||
time=time)
|
||||
return capAmount
|
||||
|
||||
|
||||
class Time2CapRegenGetter(SmoothPointGetter):
|
||||
|
||||
def _getCommonData(self, miscParams, src, tgt):
|
||||
return {
|
||||
'maxCapAmount': src.item.ship.getModifiedItemAttr('capacitorCapacity'),
|
||||
'capRegenTime': src.item.ship.getModifiedItemAttr('rechargeRate') / 1000}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
|
||||
time = x
|
||||
capAmount = calculateCapAmount(
|
||||
maxCapAmount=commonData['maxCapAmount'],
|
||||
capRegenTime=commonData['capRegenTime'],
|
||||
capAmountT0=miscParams['capAmountT0'] or 0,
|
||||
time=time)
|
||||
capRegen = calculateCapRegen(
|
||||
maxCapAmount=commonData['maxCapAmount'],
|
||||
capRegenTime=commonData['capRegenTime'],
|
||||
currentCapAmount=capAmount)
|
||||
return capRegen
|
||||
|
||||
|
||||
# Useless, but valid combination of x and y
|
||||
class CapAmount2CapAmountGetter(SmoothPointGetter):
|
||||
|
||||
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
|
||||
capAmount = x
|
||||
return capAmount
|
||||
|
||||
|
||||
class CapAmount2CapRegenGetter(SmoothPointGetter):
|
||||
|
||||
def _getCommonData(self, miscParams, src, tgt):
|
||||
return {
|
||||
'maxCapAmount': src.item.ship.getModifiedItemAttr('capacitorCapacity'),
|
||||
'capRegenTime': src.item.ship.getModifiedItemAttr('rechargeRate') / 1000}
|
||||
|
||||
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
|
||||
capAmount = x
|
||||
capRegen = calculateCapRegen(
|
||||
maxCapAmount=commonData['maxCapAmount'],
|
||||
capRegenTime=commonData['capRegenTime'],
|
||||
currentCapAmount=capAmount)
|
||||
return capRegen
|
||||
|
||||
|
||||
def calculateCapAmount(maxCapAmount, capRegenTime, capAmountT0, time):
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
|
||||
return maxCapAmount * (1 + math.exp(5 * -time / capRegenTime) * (math.sqrt(capAmountT0 / maxCapAmount) - 1)) ** 2
|
||||
|
||||
|
||||
def calculateCapRegen(maxCapAmount, capRegenTime, currentCapAmount):
|
||||
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
|
||||
return 10 * maxCapAmount / capRegenTime * (math.sqrt(currentCapAmount / maxCapAmount) - currentCapAmount / maxCapAmount)
|
||||
62
graphs/data/fitCapacitor/graph.py
Normal file
62
graphs/data/fitCapacitor/graph.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from graphs.data.base import FitGraph, XDef, YDef, Input, InputCheckbox
|
||||
from .getter import CapAmount2CapAmountGetter, CapAmount2CapRegenGetter, Time2CapAmountGetter, Time2CapRegenGetter
|
||||
|
||||
|
||||
class FitCapacitorGraph(FitGraph):
|
||||
|
||||
# UI stuff
|
||||
internalName = 'capacitorGraph'
|
||||
name = 'Capacitor'
|
||||
xDefs = [
|
||||
XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')),
|
||||
XDef(handle='capAmount', unit='GJ', label='Cap amount', mainInput=('capAmount', '%')),
|
||||
XDef(handle='capAmount', unit='%', label='Cap amount', mainInput=('capAmount', '%'))]
|
||||
yDefs = [
|
||||
YDef(handle='capAmount', unit='GJ', label='Cap amount'),
|
||||
YDef(handle='capRegen', unit='GJ/s', label='Cap regen')]
|
||||
inputs = [
|
||||
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), conditions=[
|
||||
(('time', 's'), None)]),
|
||||
Input(handle='capAmount', unit='%', label='Cap amount', iconID=1668, defaultValue=25, defaultRange=(0, 100), conditions=[
|
||||
(('capAmount', 'GJ'), None),
|
||||
(('capAmount', '%'), None)]),
|
||||
Input(handle='capAmountT0', unit='%', label='Starting cap amount', iconID=1668, defaultValue=100, defaultRange=(0, 100), conditions=[
|
||||
(('time', 's'), None)])]
|
||||
checkboxes = [InputCheckbox(handle='useCapsim', label='Use capacitor simulator', defaultValue=True, conditions=[
|
||||
(('time', 's'), ('capAmount', 'GJ'))])]
|
||||
srcExtraCols = ('CapAmount', 'CapTime')
|
||||
|
||||
# Calculation stuff
|
||||
_normalizers = {
|
||||
('capAmount', '%'): lambda v, src, tgt: v / 100 * src.item.ship.getModifiedItemAttr('capacitorCapacity'),
|
||||
('capAmountT0', '%'): lambda v, src, tgt: None if v is None else v / 100 * src.item.ship.getModifiedItemAttr('capacitorCapacity')}
|
||||
_limiters = {
|
||||
'time': lambda src, tgt: (0, 3600),
|
||||
'capAmount': lambda src, tgt: (0, src.item.ship.getModifiedItemAttr('capacitorCapacity')),
|
||||
'capAmountT0': lambda src, tgt: (0, src.item.ship.getModifiedItemAttr('capacitorCapacity'))}
|
||||
_getters = {
|
||||
('time', 'capAmount'): Time2CapAmountGetter,
|
||||
('time', 'capRegen'): Time2CapRegenGetter,
|
||||
('capAmount', 'capAmount'): CapAmount2CapAmountGetter,
|
||||
('capAmount', 'capRegen'): CapAmount2CapRegenGetter}
|
||||
_denormalizers = {('capAmount', '%'): lambda v, src, tgt: v * 100 / src.item.ship.getModifiedItemAttr('capacitorCapacity')}
|
||||
24
graphs/data/fitDamageStats/__init__.py
Normal file
24
graphs/data/fitDamageStats/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from .graph import FitDamageStatsGraph
|
||||
|
||||
|
||||
FitDamageStatsGraph.register()
|
||||
22
graphs/data/fitDamageStats/cache/__init__.py
vendored
Normal file
22
graphs/data/fitDamageStats/cache/__init__.py
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from .projected import ProjectedDataCache
|
||||
from .time import TimeCache
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user