Compare commits

..

446 Commits

Author SHA1 Message Date
DarkPhoenix
a317dab9a8 Bump version 2020-08-15 03:56:54 +03:00
DarkPhoenix
db6d8b93e9 Re-enable meta storm menu 2020-08-15 03:39:48 +03:00
DarkPhoenix
7d585c1a62 Merge branch 'master' into singularity
# Conflicts:
#	staticdata/fsd_lite/evetypes.json
#	staticdata/phobos/metadata.json
2020-08-15 03:34:45 +03:00
DarkPhoenix
c45e84470e Add storm effects 2020-08-15 03:27:47 +03:00
DarkPhoenix
ab3b40e136 Rerun effectUsedBy 2020-08-15 02:08:09 +03:00
DarkPhoenix
b566a8bfa6 Update static data to 1786419 2020-08-15 02:06:19 +03:00
DarkPhoenix
ccb395d592 Add standard outpost rig effect 2020-08-13 13:17:50 +03:00
DarkPhoenix
3d039724c9 Relax ship name format restrictions even further 2020-08-13 01:18:03 +03:00
DarkPhoenix
d0e7e7eed5 Merge branch 'master' of github.com:pyfa-org/Pyfa into master 2020-08-13 01:16:08 +03:00
DarkPhoenix
a387bc8d09 Fix import of ship/structure names containing single quotes 2020-08-13 01:15:41 +03:00
Anton Vorobyov
94344bd432 Merge pull request #2254 from Neugeniko/Issue/2253
Updated 'Retaliating Caldari Entities' target profile
2020-08-12 12:14:48 +03:00
DarkPhoenix
a976fb33f0 Fix defender velocity hardwiring 2020-08-12 00:11:52 +03:00
DarkPhoenix
f850fdf0d5 Bump version 2020-08-11 19:59:46 +03:00
DarkPhoenix
0fcbedba45 Disable metaliminal storm context menu 2020-08-11 19:58:44 +03:00
DarkPhoenix
5707bddacd Update staticdata to 1782806 2020-08-11 19:56:33 +03:00
Neugeniko
8ed9257dfa Updated 'Retaliating Caldari Entities' target profile
2020/08/12 Eve Online Patch increased Em Resists for some npc in the group of  'Retaliating Caldari Entities'
The groups target profile has been recalculated and edited.
2020-08-12 02:56:13 +10:00
DarkPhoenix
056ae590cf Merge branch 'master' into singularity 2020-08-10 17:39:37 +03:00
DarkPhoenix
7733fd38c2 Do not set read-only property 2020-08-10 17:39:08 +03:00
DarkPhoenix
fef78c971f Add effects used by metaliminal storms
Most of them are stubs, so just 2 so far
2020-08-05 22:57:45 +03:00
DarkPhoenix
e52ceacdb9 Add liminal storms to projection menu 2020-08-05 22:46:43 +03:00
DarkPhoenix
85b2d7af8d Add item conversions
Based on my own assumptions about them
2020-08-05 22:21:27 +03:00
DarkPhoenix
3e658a31bb Process renamed items 2020-08-05 21:54:56 +03:00
DarkPhoenix
0d2a4d4d44 Update metadata to 1780302 2020-08-05 21:40:59 +03:00
Anton Vorobyov
bc2cdcdea7 Merge pull request #2249 from m-sasha/undup-tab-selection
_TabsContainer.CheckTabSelected no longer posts PageChanged event twice
2020-07-31 11:59:38 +03:00
Anton Vorobyov
30ad554cc0 Merge pull request #2248 from m-sasha/no-half-second-sleep
Do not sleep for half a second in OpenFitsThread.run()
2020-07-31 11:59:06 +03:00
Anton Vorobyov
4a85fd5d1b Merge pull request #2247 from m-sasha/fast-tabs-resize
Ignore _TabRenderer.SetSize when new size is the same as old size
2020-07-31 11:58:35 +03:00
Alexander Maryanovsky
4d8dbe74bd _TabsContainer.CheckTabSelected no longer posts PageChanged event twice 2020-07-30 16:03:00 +03:00
Alexander Maryanovsky
016f2b44ff Do not sleep for half a second in OpenFitsThread.run() 2020-07-30 11:35:30 +03:00
Alexander Maryanovsky
676794baed Ignore _TabRenderer.SetSize when new size is the same as old size. 2020-07-30 11:31:00 +03:00
Anton Vorobyov
ca488089fd Merge pull request #2231 from fonsuiplaysvideogames/master
Updates to Abyss room-wide and localized weather effects
2020-07-30 08:04:42 +03:00
Anton Vorobyov
96f9b9a719 Merge branch 'master' into master 2020-07-30 08:04:09 +03:00
fonsuiplaysvideogames
508572e08b Update fit.py 2020-07-29 21:33:15 -04:00
DarkPhoenix
f91e0b2e23 Do not crash on ship w/o labels and w/o slots 2020-07-29 09:02:46 +03:00
DarkPhoenix
93ae9e0891 Bump version 2020-07-28 17:39:20 +03:00
DarkPhoenix
2ed5dbc3c7 Avoid using newer matplotlib 2020-07-28 17:12:15 +03:00
DarkPhoenix
bca8ba3114 Bump version 2020-07-28 15:58:15 +03:00
DarkPhoenix
174ac97682 Implement new CS effects 2020-07-28 15:57:55 +03:00
DarkPhoenix
2d9e873d42 Update staticdata to 1777281 2020-07-28 15:21:08 +03:00
DarkPhoenix
33377357f6 Change dampener definition to include named variations 2020-07-26 10:36:55 +03:00
DarkPhoenix
3ee0ee7e40 Fix drone sorting in projected view 2020-07-19 23:31:00 +03:00
fonsuiplaysvideogames
2f02747b29 Update fit.py 2020-07-16 16:24:08 -04:00
fonsuiplaysvideogames
78b176a135 Update fit.py 2020-07-16 16:20:24 -04:00
DarkPhoenix
52063beea9 BCS affect defender missiles now 2020-07-16 00:40:04 +03:00
DarkPhoenix
03a55d94e9 Bump version 2020-07-14 20:29:47 +03:00
DarkPhoenix
db256f57d1 Adjust ANP jargon entries 2020-07-14 20:29:27 +03:00
DarkPhoenix
8de6d78be0 Update effect docstrings 2020-07-14 20:25:45 +03:00
DarkPhoenix
902a00d37d Add conversions 2020-07-14 20:24:42 +03:00
DarkPhoenix
523cb1467e Merge branch 'issue_2205' 2020-07-14 20:21:23 +03:00
DarkPhoenix
f75de70d79 Update staticdata to 1769230 2020-07-14 20:00:23 +03:00
DarkPhoenix
4b635f4d21 Remove www to make dropdown shorter on gtk 2020-07-14 17:31:19 +03:00
DarkPhoenix
63632e09b3 Group market sources up to avoid cross-server price data confusion 2020-07-14 17:25:30 +03:00
Ryan Holmes
df78eb5781 Merge pull request #2214 from copyliu/cevemarket
add market source ceve-market
2020-06-29 20:07:44 -04:00
Copy Liu
c1a5828d6b add market source ceve-market 2020-06-29 20:58:46 +08:00
blitzmann
5377210b89 Merge branch 'master' of https://github.com/pyfa-org/Pyfa 2020-06-26 23:30:51 -04:00
blitzmann
1936255f2c Merge branch 'issue2209' 2020-06-26 23:30:40 -04:00
blitzmann
b3e5763cfc Fix for #2209 - Flip the None check to happen first instead of second, otherwise if Notes is None then the first check will fail due to trying to len(None) 2020-06-26 23:30:15 -04:00
DarkPhoenix
7442f315c9 Do not activate scanners by default 2020-06-26 21:30:43 +03:00
blitzmann
433c9555bf Removed the util function - this would actually break older migrations if we added things that didn't exist in future iterations. Each migration should be self-contained 2020-06-24 15:18:43 -04:00
blitzmann
d1345fc71e Fixes #2205 - runs conversions on modules.baseItemID since the creation of baseItemID. Also, centralized the function to convert modules so that in case we ever add other things that need to be converted, they are all in one spot. 2020-06-24 15:09:51 -04:00
DarkPhoenix
31cae0e54b Bump version 2020-06-18 17:28:02 +03:00
DarkPhoenix
b2cc3ae600 Update static data to 1751741 2020-06-18 13:52:46 +03:00
DarkPhoenix
62d1d6a06d Avoid crash when nothing is selected 2020-06-18 06:04:23 +03:00
DarkPhoenix
83fa567321 Pass primitives around instead of SQL Alchemy objects 2020-06-18 05:33:12 +03:00
DarkPhoenix
9b315b5870 Fix a few mistypes 2020-06-17 21:53:24 +03:00
DarkPhoenix
3094fd32fc Add destructible beacons to projected effects 2020-06-17 17:07:34 +03:00
DarkPhoenix
c3f1824a84 Rename charge export button 2020-06-16 21:06:42 +03:00
DarkPhoenix
b558ae3810 Add an option not to export charges to ESI 2020-06-15 23:38:07 +03:00
DarkPhoenix
c38f05902a Add alternate redo shortcut 2020-06-15 17:20:20 +03:00
DarkPhoenix
181e1e1e30 Make scanners active and expose data to misc column 2020-06-15 16:37:52 +03:00
DarkPhoenix
3fc26254ae bump version 2020-06-15 16:14:53 +03:00
DarkPhoenix
ca8822c455 Fix upwell ships' race 2020-06-15 16:14:20 +03:00
DarkPhoenix
4c55d32086 Update icons 2020-06-15 15:54:28 +03:00
DarkPhoenix
c13817708e Add vorton damage booster effect 2020-06-15 15:20:59 +03:00
DarkPhoenix
39cab4be72 Remove DB TDs from seen items 2020-06-15 15:00:36 +03:00
DarkPhoenix
f80473d073 Update static data to 1748920 2020-06-15 14:50:58 +03:00
DarkPhoenix
0d9be1b174 Merge branch 'singularity' 2020-06-15 14:42:13 +03:00
DarkPhoenix
83b0b3ff0e Fix jargon definitions for missile damage types 2020-06-13 12:33:06 +03:00
DarkPhoenix
c4b490907d Add alternate DNA format import 2020-06-12 02:20:32 +03:00
DarkPhoenix
f89fe4d5d7 Fix merge issue 2020-06-09 19:11:11 +03:00
DarkPhoenix
81dced03bf Disable ammo graph 2020-06-09 19:07:23 +03:00
DarkPhoenix
27d3bea8d8 Merge branch 'ammo_graph' into singularity
# Conflicts:
#	gui/targetProfileEditor.py
2020-06-09 19:04:20 +03:00
DarkPhoenix
7eaeea325d Bump version 2020-06-09 10:54:21 +03:00
DarkPhoenix
9695526352 Update staticdata to 1744483 2020-06-09 10:51:28 +03:00
DarkPhoenix
4f35bc38db Fix structure module drag'n'drop 2020-06-09 01:56:31 +03:00
DarkPhoenix
d9c7c1a0fd Fix burst projectors' misc column 2020-06-06 19:22:55 +03:00
DarkPhoenix
a94e66e5ed Invuln - alias for old invulns 2020-06-06 02:25:08 +03:00
DarkPhoenix
e9c41c6439 Add renders of 2/3 of new ships 2020-06-06 01:42:57 +03:00
DarkPhoenix
b24ceb95f5 Add missing icons 2020-06-05 22:22:03 +03:00
DarkPhoenix
6ba6e0f69b Implement skill and booster effects, and bump version 2020-06-06 01:09:55 +03:00
DarkPhoenix
9aca49e539 Add damage mod effect and gun application readout 2020-06-06 00:25:19 +03:00
DarkPhoenix
01f1cdb175 Implement gun effect and its support in graphs 2020-06-06 00:11:36 +03:00
DarkPhoenix
c1356906bb Add ship effects 2020-06-05 23:36:39 +03:00
DarkPhoenix
8229537e5f Run effect used by script 2020-06-05 22:50:34 +03:00
DarkPhoenix
6cfa2161ec Add conversions for amplifier tiericide 2020-06-05 22:47:13 +03:00
DarkPhoenix
fa3cf46786 Update static data to 1742848 2020-06-05 22:28:25 +03:00
DarkPhoenix
ac40dfd018 Update version again 2020-05-26 16:07:01 +03:00
DarkPhoenix
4c32d559bb Bump version 2020-05-26 16:04:51 +03:00
DarkPhoenix
02ee12586f Adjust jargon definitions to new realities 2020-05-26 16:03:48 +03:00
DarkPhoenix
496f15a83e Add conversions/migrations 2020-05-26 15:50:52 +03:00
DarkPhoenix
acf6af8f95 Update effect definitions and headers to all the renamed items 2020-05-26 15:30:50 +03:00
DarkPhoenix
225b00b9ff Update staticdata to 1733182 and update DB generation script 2020-05-26 15:22:08 +03:00
DarkPhoenix
7f2b7415ea Disable evemarketdata price source 2020-05-14 15:10:04 +03:00
DarkPhoenix
d1d9ae4dac Add fuzzwork as price data source 2020-05-14 15:09:38 +03:00
DarkPhoenix
ac7ccfb6a3 Change how double-clicking ammo behaves 2020-05-04 19:18:14 +03:00
DarkPhoenix
05c2c41762 Destroy effective label when switching from firepower to mining view, and add it back to correct position when needed 2020-05-04 13:46:52 +03:00
DarkPhoenix
10f5133282 Revert "Disable system theme for listctrls on windows"
This reverts commit 42658a8167.
2020-04-27 13:55:30 +03:00
DarkPhoenix
42658a8167 Disable system theme for listctrls on windows 2020-04-26 23:27:28 +03:00
DarkPhoenix
6dbf38dbb4 Revert "Switch to wxpython 4.1.0". Refer to #2179 for more info on
reasons

This reverts commit 15fe8f6261.
2020-04-25 17:37:29 +03:00
DarkPhoenix
64306e2366 Align attribute icons the same way as input boxes in target profile
editor
2020-04-25 17:29:29 +03:00
DarkPhoenix
f259df85cd Fix editors 2020-04-25 17:27:29 +03:00
DarkPhoenix
1090589cb2 Fix preferences for wx 4.1.0 2020-04-25 17:19:05 +03:00
DarkPhoenix
29e09fcdda Fix abyssal item slider on windows 2020-04-25 12:36:45 +03:00
DarkPhoenix
36fd4aeaff Redo fix for 2176 2020-04-25 15:14:27 +03:00
DarkPhoenix
c46a59e3b1 Fix gauge bars 2020-04-25 09:57:49 +03:00
DarkPhoenix
8a3a21972a Fix error dialog layout 2020-04-25 09:40:50 +03:00
DarkPhoenix
6839669e04 Remove a few flags the new wx complains about 2020-04-25 09:36:40 +03:00
DarkPhoenix
5645e20505 Do not float auxiliary windows on parent if there is no parent 2020-04-25 09:29:14 +03:00
DarkPhoenix
15fe8f6261 Switch to wxpython 4.1.0 2020-04-25 09:26:47 +03:00
DarkPhoenix
855fafa94d Bump version 2020-04-24 15:35:52 +03:00
DarkPhoenix
4e10335ae7 Revert "Try resetting locale on wx 4.0.6 as an attempt to work around #2174"
This reverts commit ea07bbf4f9.
2020-04-24 15:10:33 +03:00
DarkPhoenix
21ea9ce579 "Move" attributes at DB generation time 2020-04-24 15:10:00 +03:00
DarkPhoenix
ea07bbf4f9 Try resetting locale on wx 4.0.6 as an attempt to work around #2174 2020-04-24 11:37:27 +03:00
DarkPhoenix
8eed6fbe21 Ignore mutations with 0 base value 2020-04-23 19:47:57 +03:00
DarkPhoenix
0859f2fbe9 Hide limited synth boosters 2020-04-22 02:27:20 +03:00
DarkPhoenix
71ba33edeb Fix tail clear method 2020-04-21 19:00:44 +03:00
DarkPhoenix
ce80d92b35 Limit amount of fits returned by search by 100 2020-04-21 11:16:06 +03:00
DarkPhoenix
f17cf9b736 Fix undo for fill with module (which was caused by module add command not being undone properly in case of failure) 2020-04-20 16:36:16 +03:00
DarkPhoenix
98579c774b Change shortcuts for undo/redo from built-in wx standards to ctrl-z and ctrl-y 2020-04-20 14:42:32 +03:00
DarkPhoenix
509fa279e7 Initialize session container with main thread session 2020-04-17 19:15:47 +03:00
DarkPhoenix
091ee87761 Update char list when new character gets added 2020-04-17 13:31:15 +03:00
DarkPhoenix
c0c20cc92e Remove skill refresh button 2020-04-17 13:27:51 +03:00
DarkPhoenix
1341f7bca1 Make some jargon replacements a bit looser 2020-04-17 11:27:28 +03:00
DarkPhoenix
fe93db1d4b Round booster side-effect context menu 2020-04-17 00:25:33 +03:00
DarkPhoenix
5db97ea773 Bump version 2020-04-16 22:46:45 +03:00
DarkPhoenix
1758e4f320 Rework fix for projection, so that it does not recalculate projected fits too early 2020-04-16 22:25:10 +03:00
DarkPhoenix
1a897c0419 Pass search results as set of item IDs 2020-04-16 15:13:32 +03:00
DarkPhoenix
32db3e3179 Create scoped gamedata sessions 2020-04-16 15:01:00 +03:00
DarkPhoenix
d830a8957a Bump version 2020-04-15 16:54:24 +03:00
DarkPhoenix
652ea48223 Accept 0 free slots for guns which have already been fitted 2020-04-15 16:53:03 +03:00
DarkPhoenix
8c25b2b8f5 Make sure it's impossible to add extra cap boosters via dragging too 2020-04-15 15:40:54 +03:00
DarkPhoenix
db4c56be8e Bump version 2020-04-15 15:27:19 +03:00
DarkPhoenix
f3bcffe2f9 Implement fax cap booster limit 2020-04-15 15:23:49 +03:00
DarkPhoenix
bc5786d099 Update static data to 1706308 2020-04-15 14:50:55 +03:00
DarkPhoenix
5959fe5daf Ignore case when searching for implant sets to allow some inconsistencies on CCP side 2020-04-14 15:28:40 +03:00
DarkPhoenix
649d338bb1 Split implants and boosters in EFT and multibuy exports 2020-04-14 11:36:08 +03:00
DarkPhoenix
dcb058a718 Update existing character if character with the same name exists 2020-04-13 12:52:02 +03:00
DarkPhoenix
1772bb5e7f Merge branch 'master' into singularity 2020-04-13 12:45:25 +03:00
Anton Vorobyov
30bd0adb06 Merge pull request #2159 from soro/auto_create_char
Regarding #2045: automatically create a character for a new sso character
2020-04-12 22:49:01 +03:00
DarkPhoenix
44dfcf771c Ensure projected fits get recalculated, since they get checkStates'd anyway 2020-04-11 02:26:17 +03:00
DarkPhoenix
a1f8a7a930 Fix escaping in regex search 2020-04-11 01:33:53 +03:00
Soeren Roerden
b22887dfad automatically create a character for a new sso character and add button to retrieve all chars directly linked to existing sso characters 2020-04-10 21:12:38 +02:00
DarkPhoenix
28137fa3f4 Remove excessive space 2020-04-10 13:11:04 +03:00
DarkPhoenix
9cbdc6055d Add search timer to attribute overrides to prevent hangs 2020-04-10 11:52:57 +03:00
DarkPhoenix
fc93c61fcf Add more entries 2020-04-10 11:31:17 +03:00
DarkPhoenix
3fa2e7ebd1 More additions to jargon 2020-04-10 06:07:33 +03:00
DarkPhoenix
818628da0c Finalize new jargon dictionary 2020-04-10 05:53:24 +03:00
DarkPhoenix
adf90a8263 Split basic and regex search functions 2020-04-10 00:57:41 +03:00
DarkPhoenix
362923ac64 Overhaul jargon dictionary (some entries are still missing) 2020-04-09 23:29:51 +03:00
DarkPhoenix
7d73838ce1 Change the way user definitions are used 2020-04-08 14:06:49 +03:00
DarkPhoenix
b3278ca9ec Tokenize regexp requests taking into consideration regexp context 2020-04-08 13:24:48 +03:00
DarkPhoenix
5707914ad5 Make use of regex for search 2020-04-08 03:17:25 +03:00
DarkPhoenix
9b697b24d8 Implement regexp function for gamedata 2020-04-08 02:34:28 +03:00
DarkPhoenix
098b088da6 Bump version 2020-04-07 21:39:24 +03:00
DarkPhoenix
14d62f31e1 Add conversions for names 2020-04-07 21:37:40 +03:00
DarkPhoenix
e7b1f55d08 Update static data to 1702461 2020-04-07 20:59:07 +03:00
DarkPhoenix
8f501896a1 Give separate axis selector and axis label names to make it clearer what the graphs do 2020-04-07 20:37:28 +03:00
DarkPhoenix
befcb9b874 Un-hide and un-fuck bumping graphs 2020-04-07 20:14:06 +03:00
DarkPhoenix
c70afa9a4c Hide experimental bumping graphs 2020-04-07 18:25:40 +03:00
DarkPhoenix
029c7dd4c2 Recalculate fit when overrides are toggled 2020-04-07 18:05:43 +03:00
DarkPhoenix
1f83dba1ac Add missing hidden imports to platform-specific specifications 2020-04-07 18:00:17 +03:00
DarkPhoenix
6eae3405fd Add hidden import for newer version of setuptools 2020-04-07 17:39:24 +03:00
DarkPhoenix
c0e1d9e4de Attempt to use newer version of setuptools 2020-04-07 17:09:59 +03:00
DarkPhoenix
7c0bd7aa88 Leave only setuptools override 2020-04-07 16:41:28 +03:00
DarkPhoenix
394583584c Remove some of hardcoded libraries for MacOS X build 2020-04-07 15:08:30 +03:00
DarkPhoenix
4112e2aa6b Debugging Mac OS X build, step 1 2020-04-07 14:17:55 +03:00
DarkPhoenix
aa19e0da72 Add bunch of experimental graphs related to bumping 2020-04-07 13:46:28 +03:00
DarkPhoenix
bba9be1598 Add momentum graphs 2020-04-07 10:50:12 +03:00
DarkPhoenix
17998916b4 Bump version 2020-04-06 22:24:06 +03:00
DarkPhoenix
543089bcd9 Add context menu support for predefined implant sets 2020-04-06 22:20:28 +03:00
DarkPhoenix
7f35c78a65 Add data about implant sets to eve.db 2020-04-06 21:01:20 +03:00
DarkPhoenix
b25798dd83 Update effects 2020-04-06 20:14:19 +03:00
DarkPhoenix
744b9ff78a Updated staticdata to 1701385 2020-04-06 19:19:06 +03:00
DarkPhoenix
8dc1457ebb Change the way zip file is generated 2020-04-06 15:09:06 +03:00
DarkPhoenix
982ad54fab Merge branch 'Metallicow-optimize-images' 2020-04-06 15:29:10 +03:00
DarkPhoenix
912192cd7d Merge branch 'optimize-images' of https://github.com/Metallicow/Pyfa into Metallicow-optimize-images 2020-04-06 15:26:32 +03:00
DarkPhoenix
74cd6d48da Show propmpt to creade damage patterns when there're none 2020-04-06 11:14:21 +03:00
DarkPhoenix
3467a7fe3f Update static data to 1684558 2020-03-11 17:01:34 +03:00
DarkPhoenix
eb269a05ed Bump version 2020-03-10 19:39:22 +03:00
DarkPhoenix
c63cf4b3b0 Add cap booster conversions 2020-03-10 19:19:13 +03:00
DarkPhoenix
11ed94454d Update staticdata to 1683031 2020-03-10 19:07:42 +03:00
DarkPhoenix
8df07645da Show RR power for uncharged ancillary RRs 2020-03-06 00:50:18 +03:00
DarkPhoenix
5666fdd250 Bump version 2020-02-27 15:24:45 +03:00
DarkPhoenix
0b55ae40fc Update staticdata to 1675961 2020-02-27 15:09:54 +03:00
DarkPhoenix
3726e84697 Add dbuffcollections (just to have overview of changed fleet buffs when stuff changes) 2020-02-20 13:31:18 +03:00
DarkPhoenix
a0c4341102 Fix ECM range rigs affecting ECM bursts with penalty 2020-02-17 23:42:48 +03:00
Metallicow
cd6b1038e8 optimize all pngs 2020-02-13 14:58:27 -06:00
DarkPhoenix
10583fd506 Update fighter additions tab icon 2020-02-13 17:25:40 +03:00
DarkPhoenix
713694be56 Bump version 2020-02-13 17:00:58 +03:00
DarkPhoenix
f50293cf77 Revert to wxpython 4.0.6 once again (see issue 2136) 2020-02-13 16:57:23 +03:00
DarkPhoenix
7c88fa477f Bump version 2020-02-13 12:20:03 +03:00
DarkPhoenix
f8df540fad Use newer wxpython version once again 2020-02-13 12:19:37 +03:00
DarkPhoenix
61a01805cc Reset locale and remove resolution data from all the images (wxpython phoenix issue #1515) 2020-02-13 12:14:26 +03:00
Metallicow
a93915cf04 trim trailing whitespace 2020-02-12 16:14:12 -06:00
DarkPhoenix
0f74c97fbf Temporarily revert python version to 3.7 2020-02-12 18:31:44 +03:00
DarkPhoenix
29713b69dc Fix the hack 2020-02-12 18:25:17 +03:00
DarkPhoenix
cc3c2cb9c8 Add hack to build wx on windows 2020-02-12 18:22:31 +03:00
DarkPhoenix
b36a3959da Revert wxpython version to 4.0.6 2020-02-12 18:18:18 +03:00
DarkPhoenix
3b26bfc0e9 Add missing entries to migration 2020-02-11 17:02:00 +03:00
DarkPhoenix
c59b621963 Add capacitor transfer tiericide conversion entries 2020-02-11 16:52:15 +03:00
DarkPhoenix
977a8fa329 Update staticdata to 1663997 2020-02-11 16:20:08 +03:00
DarkPhoenix
a5226fee83 Merge branch 'singularity' 2020-02-11 16:08:46 +03:00
DarkPhoenix
aec5a46452 Fix shebang line 2020-02-05 09:43:10 +03:00
DarkPhoenix
588f9a4490 Re-use market service item sorting for attribute overrides 2020-02-04 01:48:13 +03:00
DarkPhoenix
4ba948c6f9 Merge branch 'master' into ammo_graph
# Conflicts:
#	gui/builtinMarketBrowser/itemView.py
#	service/market.py
2020-02-04 01:43:52 +03:00
DarkPhoenix
e4069a3988 Update AppVeyor deploy oauth key after repo transfer 2020-02-04 00:41:03 +03:00
DarkPhoenix
6527f9e11e Attempt to terminate threads when pyfa is closed 2020-02-03 17:12:23 +03:00
DarkPhoenix
9ddfcc894f Make sure not to catch keyboard interrupts and system exits 2020-02-03 16:12:41 +03:00
DarkPhoenix
f22a4f13e5 Allow pasting non-mutated modules via ctrl-v 2020-02-03 15:54:22 +03:00
DarkPhoenix
fc9337df67 Add more info prints to mac os packaging scripts 2020-02-03 13:37:27 +03:00
DarkPhoenix
98a8332cfa Change the way we update pip for mac os builds 2020-02-03 13:33:26 +03:00
DarkPhoenix
a78eedad48 Remove unused files 2020-02-03 13:30:04 +03:00
DarkPhoenix
85e1a1bd06 Use newer image version for mac builds 2020-02-03 13:25:51 +03:00
DarkPhoenix
25b2c04309 Do not set environment variable as it's not used anymore 2020-02-03 13:23:56 +03:00
DarkPhoenix
54a84d0e42 Do not attempt to install custom python version, just use system default 2020-02-03 13:23:13 +03:00
DarkPhoenix
af0a4d1def Update travis deployment key 2020-02-03 13:11:48 +03:00
DarkPhoenix
97cdde84ce Uninstall not when setup just begins, but right before install begins 2020-02-03 06:17:28 +03:00
DarkPhoenix
a65129b277 Fix function definition order in inno setup script 2020-02-03 06:06:25 +03:00
DarkPhoenix
3b40a49918 Uninstall previous pyfa version unconditionally 2020-02-03 05:59:29 +03:00
DarkPhoenix
d95345b476 Insist on uninstalling old pyfa versions when next one is about to be installed 2020-02-02 23:48:06 +03:00
DarkPhoenix
2ba2d95017 Change clone depth to 400 on appveyor 2020-02-02 14:46:26 +03:00
DarkPhoenix
4d0ffedcc8 Install in 64 bit mode on Windows 2020-02-02 11:07:20 +03:00
DarkPhoenix
84abde4fc5 Some OCD in echoes + testing if build is started after webhook removal 2020-02-02 04:22:41 +03:00
DarkPhoenix
8897f1e4b1 Use my credentials when uploading builds to releases 2020-02-02 04:08:10 +03:00
DarkPhoenix
6f33cacb7a Clean up appveyor file 2020-02-02 03:54:52 +03:00
DarkPhoenix
b1a8c0ad09 Disable appveyor's RDP setup as it's a security hole 2020-02-02 03:40:20 +03:00
DarkPhoenix
13ed635803 We do not care if all PR builds will be built 2020-02-02 03:38:43 +03:00
DarkPhoenix
5a44909ebf Remove appveyor environment preparation which is not needed anymore 2020-02-02 03:13:57 +03:00
DarkPhoenix
6cbb80693d Switch to powershell echo 2020-02-02 03:04:53 +03:00
DarkPhoenix
0b90d254f9 Try another approach in echo'ing 2020-02-02 02:53:18 +03:00
DarkPhoenix
641d36205c Remove quotes from echo commands 2020-02-02 02:47:14 +03:00
DarkPhoenix
6e38b6ea4d Fix path to windows SDK once again 2020-02-02 02:38:26 +03:00
DarkPhoenix
84b4d8fabb Change windows SDK location 2020-02-02 02:03:23 +03:00
DarkPhoenix
c0e1f7e746 Fix mistype 2020-02-01 15:36:33 +03:00
DarkPhoenix
d734c12168 Try to find out when listing windows SDKs fails 2020-02-01 15:34:14 +03:00
DarkPhoenix
340d94eb3e Specify image in apprveyor file 2020-02-01 15:09:47 +03:00
DarkPhoenix
9b0b31648c Add debugging prints to see appveyor's OS version 2020-02-01 02:38:00 +03:00
DarkPhoenix
8346358c37 Fix appveyor script slashes 2020-02-01 02:24:09 +03:00
DarkPhoenix
b935700a12 Bump INNO setup version 2020-02-01 01:56:26 +03:00
DarkPhoenix
2f4a5a4830 Fix improper use of operator "is" when comparing to literal values 2020-01-31 18:16:36 +03:00
DarkPhoenix
e951ce8e9d Bump version 2020-01-31 18:06:28 +03:00
DarkPhoenix
fc6d42f483 Update effects 2020-01-31 17:59:50 +03:00
DarkPhoenix
7df1431ad2 Update staticdata to 1657583 2020-01-31 17:40:51 +03:00
DarkPhoenix
4d4680961e Bump versions of multiple packages 2020-01-31 16:08:24 +03:00
DarkPhoenix
1cae99b812 Remove VC 9.0 tools manifest
We're using newer version now, and we will see later if dependency section in manifest is needed
2020-01-31 16:07:26 +03:00
DarkPhoenix
3727d19311 In attribute editor what is you see is what you get now 2020-01-31 10:32:18 +03:00
DarkPhoenix
c45c093f4f Return search results as list to avoid various issues 2020-01-31 10:04:35 +03:00
DarkPhoenix
b613d3e312 Use market sorting in attribute override editor 2020-01-31 00:37:35 +03:00
DarkPhoenix
4cff9247b0 Always include ships and citadels in override editor output 2020-01-31 00:13:40 +03:00
DarkPhoenix
b9a26ec28d Do not return items from all the categories in overrides editor searches 2020-01-31 00:00:05 +03:00
DarkPhoenix
245f81e888 Actually make appveyor to use 64 bit python 2020-01-30 13:55:30 +03:00
DarkPhoenix
204c1ba9f2 Switch windows build to be 64 bits 2020-01-30 13:45:49 +03:00
DarkPhoenix
a7a4be133c Bump pyinstaller version once again 2020-01-30 13:04:49 +03:00
DarkPhoenix
14f9a46d7a Remove MPL hook 2020-01-30 11:59:04 +03:00
DarkPhoenix
003dd040dc Bump matplotlib version 2020-01-30 11:51:27 +03:00
DarkPhoenix
97cf53217e Bump python version to 3.8 2020-01-30 11:09:11 +03:00
DarkPhoenix
7c1805826d Fix mino and apostle RR effects affecting subcap modules 2020-01-29 16:11:30 +03:00
DarkPhoenix
d6a2b4dfc4 Fix projected system-wide effects menu after multiple renames 2020-01-29 15:58:02 +03:00
DarkPhoenix
d70952520a Update effects 2020-01-29 15:41:55 +03:00
DarkPhoenix
a57867b806 Update static data to 1655541 2020-01-29 15:18:07 +03:00
DarkPhoenix
17d7583cfc Remove overrides which are not needed 2020-01-29 12:54:23 +03:00
DarkPhoenix
55ad95081b Fix alternate tree display in case units did not have display names 2020-01-28 15:16:48 +03:00
DarkPhoenix
da8da1759f Bump version 2020-01-28 14:59:20 +03:00
DarkPhoenix
b4e758b9ee Add new and old but missing conversions 2020-01-28 14:58:54 +03:00
DarkPhoenix
3ba41db699 Update data to 1655072 2020-01-28 14:39:35 +03:00
DarkPhoenix
cfc47cf483 When learning skill in character editor, learn all the prerequisites 2020-01-27 11:44:20 +03:00
DarkPhoenix
38c8be995b Add even more condensed tooltip version (not used yet) 2020-01-24 17:56:07 +03:00
DarkPhoenix
492207700b Force pyinstaller to 3.3 on mac as well 2020-01-21 22:17:29 +03:00
DarkPhoenix
b69adefbf8 Restore matplotlib pyinstaller hook which is still needed in 3.3 2020-01-21 22:00:08 +03:00
DarkPhoenix
361299f51e Bump version 2020-01-21 21:51:02 +03:00
DarkPhoenix
025091c3f6 Roll back pyinstaller to 3.3 for windows builds 2020-01-21 21:50:10 +03:00
DarkPhoenix
a54d70036b Bump version number 2020-01-21 15:32:16 +03:00
DarkPhoenix
9e67b5962c Update the way we update pip on appveyor 2020-01-21 15:31:48 +03:00
Anton Vorobyov
ed0c080163 Merge pull request #2123 from Neugeniko/master
Update Invasion Damage Profiles due to npc changes.
2020-01-21 15:26:53 +03:00
DarkPhoenix
eacc97160b Merge branch 'master' into ammo_graph 2020-01-21 15:25:39 +03:00
Neugeniko
21389d84fc Update Invasion Damage Profiles due to npc changes.
#2122
2020-01-21 23:17:37 +11:00
DarkPhoenix
0fe54631ad Update staticdata to 1651039 2020-01-21 15:00:21 +03:00
DarkPhoenix
36ea5200f2 Remove pyinstaller from requirements as it's needed just for packaging 2020-01-21 13:33:17 +03:00
DarkPhoenix
d9934160a6 Compatibility with pyinstaller 2020-01-21 13:30:48 +03:00
DarkPhoenix
b9c92c48d3 Bump wx version 2020-01-17 17:15:21 +03:00
DarkPhoenix
6d23df6156 Fix on-effect resistance definition 2020-01-17 16:08:55 +03:00
DarkPhoenix
a913ab72c2 Bump version of pyinstaller to avoid security issues 2020-01-17 11:14:28 +03:00
DarkPhoenix
f6d33a2ac1 Use effect IDs rather than effect amount when deciding if we should rebuild abilities 2020-01-16 19:02:10 +03:00
DarkPhoenix
5a3fec33a7 Bump version 2020-01-16 16:26:19 +03:00
DarkPhoenix
95734aca2c Add fighter AB effect 2020-01-16 16:19:37 +03:00
DarkPhoenix
377254be9d Add nirvana effects 2020-01-16 16:09:17 +03:00
DarkPhoenix
ad9905a5e0 Add shield booster tieritice conversions 2020-01-16 15:46:30 +03:00
DarkPhoenix
6fc5a9699e Add slave as jargon entry for amulets 2020-01-16 15:39:54 +03:00
DarkPhoenix
0ee86f4836 Add slaves to renames dictionary 2020-01-16 15:34:56 +03:00
DarkPhoenix
85593367f7 Remove single-precition workaround and update data to 1646988 2020-01-16 15:19:39 +03:00
DarkPhoenix
465ea61b06 Merge branch 'singularity' 2020-01-16 14:57:44 +03:00
DarkPhoenix
2d5507e951 Force faction structure neut to be variation of t1 item 2020-01-08 00:47:56 +03:00
DarkPhoenix
84e20be153 Add auto targeting system to list of mods which do not get online by default 2020-01-04 19:32:31 +03:00
DarkPhoenix
60080c02b0 Fix red giant ecm bonus 2020-01-02 22:11:16 +03:00
DarkPhoenix
2a0d4179d0 Make script effect passive 2020-01-02 22:03:05 +03:00
DarkPhoenix
6dd55e0cd4 Move sansha incursion and sleeper profile one level up, as there are not too many 2019-12-28 21:48:56 +03:00
Anton Vorobyov
0adc6e0c6f Merge pull request #2113 from Neugeniko/master
Add Abyssal/Invasion Damage and Target Profiles.
2019-12-28 21:45:51 +03:00
Neugeniko
e155356701 Add Invasion & Abyssal Target Profiles 2019-12-29 00:28:48 +11:00
Neugeniko
ef44b50980 Add Abyssal & Invasion Damage Profile 2019-12-29 00:26:39 +11:00
DarkPhoenix
6785d16c87 More mods not activated by default 2019-12-28 04:22:11 +03:00
DarkPhoenix
76c670b7d5 Do not activate several categories of modules on fitting 2019-12-28 03:53:02 +03:00
DarkPhoenix
717303a72f Allow to put separate charges into citadel cargo 2019-12-27 12:39:37 +03:00
DarkPhoenix
e13125b174 Enable sigamp tooltip for citadels 2019-12-27 12:35:51 +03:00
DarkPhoenix
8aa7964b81 Do not show effective cap stats for ships with 100% neut resist (citadels) 2019-12-27 12:27:30 +03:00
DarkPhoenix
5e5f23d296 Show DG standup ECM as variation of t1 module 2019-12-26 12:28:01 +03:00
DarkPhoenix
05837f99ff Change order of structure meta levels 2019-12-26 12:26:25 +03:00
DarkPhoenix
70f620c2c3 Fix unit name processing 2019-12-25 01:33:43 +03:00
DarkPhoenix
10ffb987ea Detect concord race and show appropriate icon 2019-12-23 17:02:34 +03:00
DarkPhoenix
13ed785551 Drop tank stats on factor reload toggle 2019-12-23 13:26:23 +03:00
DarkPhoenix
69d2e38647 Use modified item attributes to show ancillary tank modules reload time 2019-12-23 13:17:08 +03:00
DarkPhoenix
c45abbdbcf Use new table as source for skill requirement data 2019-12-19 20:44:17 +03:00
DarkPhoenix
2655b001a9 Add new skillrequirement file 2019-12-19 19:56:23 +03:00
DarkPhoenix
3c7f4edb1b Fix replacements generation 2019-12-19 16:41:49 +03:00
DarkPhoenix
663cbab401 More updates to the script, and data update as well 2019-12-19 16:24:45 +03:00
DarkPhoenix
897ca9ca43 Update DB script to be able to work with sisi data 2019-12-19 15:56:57 +03:00
DarkPhoenix
b31a071859 Fix crashes related to target profiles / damage patterns 2019-12-17 13:10:41 +03:00
DarkPhoenix
8c2788fd78 Do not show implant set editor if user cancels addition of an implant set 2019-12-16 18:06:03 +03:00
DarkPhoenix
dee8fa400c Allow to override menu visibility settings with control key 2019-12-16 16:29:42 +03:00
DarkPhoenix
2339327473 Do some renames 2019-12-16 13:59:41 +03:00
Anton Vorobyov
f6f9239dd5 Merge pull request #2103 from Gochim/issue/2073
Issue #2073: Added ability to add a list of implants from implant view to a new implant set
2019-12-16 13:55:00 +03:00
DarkPhoenix
cd4c8c8c10 Do not save changes when changes were reverted 2019-12-16 13:42:49 +03:00
DarkPhoenix
c25eda8b64 Do not save mutated values when user did not touch them (and they were altered due to rounding) 2019-12-16 13:11:18 +03:00
Gochim
b6edf0e034 Issue #2073: Fixed a stuck in character editor 2019-12-12 18:03:45 +02:00
Gochim
af4277fc7e Issue #2073: Fixed name of a popup menu item for implant tab 2019-12-12 17:57:03 +02:00
Gochim
acd774abe5 Issue #2073: Fixed Codacy warning 2019-12-12 14:02:31 +02:00
Gochim
9081353634 Issue #2073: Final tweaks on implementation 2019-12-11 07:51:31 +02:00
Gochim
9c7fa37a72 Issue #2073: Added functionality to add all implants in fit implant tab to implant set list 2019-12-10 18:14:13 +02:00
Gochim
d92e11893a Issue #2073: Added mechanism to pass implant list to implant set manager 2019-12-10 08:50:21 +02:00
Gochim
f55ab00bf5 Fixed error message text in EntityEditor 2019-12-10 08:18:01 +02:00
DarkPhoenix
36265aa2a3 Add comment about built-in pattern order 2019-12-07 15:52:10 +03:00
DarkPhoenix
bfedcdbffd Simplify code a little 2019-12-07 15:42:26 +03:00
DarkPhoenix
a5d10c4a76 Change sorting of built-in damage profiles 2019-12-07 15:41:07 +03:00
DarkPhoenix
78423f67dd Merge branch 'master' into ammo_graph 2019-12-07 03:34:26 +03:00
DarkPhoenix
2962ce1945 Ensure 0%-100% spoolup is sorted properly 2019-12-07 03:07:49 +03:00
DarkPhoenix
0063d2955e Add info on invasion ships 2019-12-07 02:53:48 +03:00
DarkPhoenix
9787a18666 Fix issue with inability to right-click user-defined target profiles in graph window 2019-12-07 02:47:24 +03:00
DarkPhoenix
d8cd3197b5 Show short profile name in graphs 2019-12-07 02:43:53 +03:00
DarkPhoenix
e07c4f65ab Allow to have square brackets in profile name 2019-12-07 02:19:24 +03:00
DarkPhoenix
c3108f773a Use uniform as default resists profile, if no resist profile can be fetched 2019-12-07 01:29:15 +03:00
DarkPhoenix
a400821268 Remove "database defaults" 2019-12-07 01:21:46 +03:00
DarkPhoenix
ca4bac07da Implement switching between user and builtin patterns 2019-12-07 01:17:41 +03:00
DarkPhoenix
0038eacc3f Add database migration to add new columns which are used to refer builting profiles 2019-12-07 00:50:35 +03:00
DarkPhoenix
4bd633a0d4 Process names on pattern objects themselves 2019-12-06 23:24:29 +03:00
DarkPhoenix
ee837f0b04 Add comment about ideal target profile 2019-12-06 20:07:08 +03:00
DarkPhoenix
016e18cc89 Set negative IDs to all hardcoded entities 2019-12-06 20:04:56 +03:00
DarkPhoenix
1d6ac53183 Make target profile adder multi-level 2019-12-06 20:00:53 +03:00
DarkPhoenix
1e32dfa463 Always show target resist switcher 2019-12-06 19:35:20 +03:00
DarkPhoenix
4431753570 Reimplement switcher to use built-ins too and be multi-level 2019-12-06 17:59:44 +03:00
DarkPhoenix
6fdb57318c Expose builtin target profiles to service 2019-12-06 17:14:12 +03:00
DarkPhoenix
649db019de Add builtin fetcher for target profiles 2019-12-06 16:58:58 +03:00
DarkPhoenix
1d528be0ef Add target profile builtins as well 2019-12-06 14:45:46 +03:00
DarkPhoenix
2d6f6df83d Store built-in damage patterns in dictionary rather than in list 2019-12-06 14:25:27 +03:00
DarkPhoenix
7fa998f276 Avoid duplicating names in context menu 2019-12-06 02:59:14 +03:00
DarkPhoenix
6a3157a4c8 Add hardcoded damage patterns to damage pattern definition module 2019-12-06 02:47:11 +03:00
DarkPhoenix
74efa50576 Add multi-level menu support for incoming damage patterns 2019-12-06 02:11:32 +03:00
DarkPhoenix
61e32d0b1b Merge branch 'ammo_graph' of github.com:pyfa-org/Pyfa into ammo_graph 2019-12-06 01:09:16 +03:00
DarkPhoenix
bf04d26a7b Remove duplicate code 2019-12-06 01:08:11 +03:00
DarkPhoenix
e99be78c54 Partially rework damage pattern switcher 2019-12-05 20:04:50 +03:00
DarkPhoenix
e386232de1 Gather data on damage patterns into single object 2019-12-05 10:44:26 +03:00
DarkPhoenix
a15896044d Show cap delta detailed info even when fit has no cap use 2019-12-05 10:14:30 +03:00
DarkPhoenix
72c74513ce Add excessive effective cap gain to tooltip 2019-12-04 22:21:25 +03:00
DarkPhoenix
34d6d13cb2 Avoid extra indentation in non-spool version of the tooltip 2019-12-03 20:01:25 +03:00
DarkPhoenix
323bb8e064 Change layout once again 2019-12-03 19:57:40 +03:00
DarkPhoenix
b628f8ef9b Change layout of the tooltip 2019-12-03 19:45:49 +03:00
DarkPhoenix
1900216d36 Show spoolup part before damage distribution 2019-12-03 16:31:21 +03:00
DarkPhoenix
c045ed81c1 Do not show spoolup mark when fit has no spoolup weapons 2019-12-03 16:21:16 +03:00
DarkPhoenix
39edec60e3 Add damage type info to firepower panel tooltips 2019-12-03 16:15:05 +03:00
Gochim
259214e907 Issue #2073: Added popup menu item 2019-12-03 09:23:54 +02:00
DarkPhoenix
2e1c53392d Keep hull energizers online upon addition 2019-12-02 11:41:13 +03:00
DarkPhoenix
7a77d12686 Use built-in curl timeouts instead of coreutils 2019-11-27 16:35:22 +03:00
DarkPhoenix
763a7714ed Use proper timeout on Mac OS 2019-11-27 16:06:31 +03:00
DarkPhoenix
e89f35d654 Do not fail Mac OS build if upload fails 2019-11-27 15:56:57 +03:00
DarkPhoenix
3196751b7a Bump version 2019-11-27 15:11:26 +03:00
DarkPhoenix
f221024974 Update staticdata to 1618828 2019-11-27 15:10:49 +03:00
DarkPhoenix
66cab8a0d4 Bump version 2019-11-26 16:55:15 +03:00
DarkPhoenix
daa49a8cd4 Correctly detect zirnitra as amarr 2019-11-26 16:54:47 +03:00
DarkPhoenix
b8601ff240 Update icons/renders 2019-11-26 16:50:51 +03:00
DarkPhoenix
5df3eeca64 Add zirnitra renders 2019-11-26 16:34:28 +03:00
DarkPhoenix
274e4ac2ca Update effects 2019-11-26 16:27:06 +03:00
DarkPhoenix
f6485251ca Update staticdata to 1617768 2019-11-26 15:00:40 +03:00
DarkPhoenix
c03fb70def Merge branch 'singularity' 2019-11-26 14:55:51 +03:00
DarkPhoenix
2217aff5ab Add button to add custom drone groups 2019-11-26 14:53:47 +03:00
DarkPhoenix
400d0aaa22 Rework graph ammo picker to be dialog instead of frame 2019-11-25 16:54:25 +03:00
DarkPhoenix
bfc928934c Put entries into different sizers 2019-11-25 15:48:04 +03:00
DarkPhoenix
395c17270e Merge branch 'master' into ammo_graph 2019-11-25 13:56:16 +03:00
DarkPhoenix
8652a2891b Do not attempt to change skill levels of built-in all0/5 chars 2019-11-22 15:46:29 +03:00
DarkPhoenix
a09af0a9eb Propagate keys to upper windows if they were not processed in here 2019-11-22 15:45:01 +03:00
Anton Vorobyov
c7d4b93fba Merge pull request #2094 from RussianCow/level-keyboard-shortcuts
Add keyboard shortcuts to character level editor
2019-11-22 15:01:26 +03:00
Sasha
745c0db530 Add keyboard shortcuts to character level editor 2019-11-21 18:10:56 -08:00
DarkPhoenix
aa56ab8d6c Bump version 2019-11-21 13:01:14 +03:00
DarkPhoenix
043c430221 Update staticdata to 1610407 2019-11-21 13:00:36 +03:00
DarkPhoenix
dc5cc5855e Show range of bursts projectors, taking ship/citadel radius taken into consideration 2019-11-18 15:18:20 +03:00
DarkPhoenix
59d6266e2b Do not crash on exception classes without message attribute 2019-11-17 21:24:41 +03:00
DarkPhoenix
7495ba67f8 Revert change made for debugging 2019-11-17 17:40:07 +03:00
DarkPhoenix
e649683a4d Accept XML headers with extra info 2019-11-17 17:34:49 +03:00
DarkPhoenix
bec58a5772 Do not crash on cargo fetch failures 2019-11-17 17:29:16 +03:00
DarkPhoenix
e8f9ae8a9c Do not use hires assets on wxGTK 2019-11-16 19:22:37 +03:00
DarkPhoenix
679382e220 Do not separate number and unit in range tooltip, for consistency with other fields 2019-11-15 20:19:23 +03:00
DarkPhoenix
1b40467775 Merge branch 'master' into ammo_graph 2019-11-15 20:17:02 +03:00
DarkPhoenix
7f86782f54 Change range column tooltip for missiles 2019-11-15 13:23:27 +03:00
DarkPhoenix
f80b7d972f Implement hidden flight time bonus 2019-11-15 12:59:57 +03:00
Anton Vorobyov
c0bd489c1b Merge pull request #2085 from Gochim/master
Possible fix for #2084
2019-11-14 12:38:08 +03:00
Gochim
41e4c2107d Fix for #2084 2019-11-14 11:36:59 +02:00
Gochim
cfc95c272a Possible fix for #2084 2019-11-14 10:35:16 +02:00
DarkPhoenix
27e3e53868 Merge branch 'master' into ammo_graph 2019-11-13 22:03:01 +03:00
DarkPhoenix
f778f9ceae Take ship radius in consideration when displaying missile range in range column 2019-11-13 20:51:14 +03:00
DarkPhoenix
7fb6170bcb Implement missile "falloff" support in graphs 2019-11-13 20:28:52 +03:00
DarkPhoenix
00b9884c68 Remove unnecessary color setting, as aux window already does it for us 2019-11-13 19:56:49 +03:00
DarkPhoenix
b05bd04801 Do not allow to open more than 1 ammo pickers 2019-11-13 18:45:43 +03:00
DarkPhoenix
7ba5585b83 Set min window size of ammo picker as well 2019-11-13 17:07:32 +03:00
DarkPhoenix
9386ba3fb9 Fix initial window sizing 2019-11-13 17:03:45 +03:00
DarkPhoenix
1321e70035 Add info on drones/fighters to ammo picker 2019-11-13 16:46:47 +03:00
DarkPhoenix
e2d943b0b0 Gray out items for inactive radiobuttons 2019-11-13 16:33:23 +03:00
DarkPhoenix
424b769ba9 Fix ammo section labels 2019-11-13 16:14:00 +03:00
DarkPhoenix
3671f10c9c Resize ammo picker window 2019-11-13 16:08:06 +03:00
DarkPhoenix
935ecd0ea7 Make structure scrollable 2019-11-13 15:55:02 +03:00
DarkPhoenix
92e15ece72 Add structure with checkboxes to ammo switcher window 2019-11-13 15:20:43 +03:00
DarkPhoenix
a5c1875a29 Make sure to cache ammo set to reuse it when applying ammo to multiple modules 2019-11-13 13:48:13 +03:00
DarkPhoenix
9146c0f2c6 Use ammo service to generate ammo switcher context menu 2019-11-13 13:31:40 +03:00
DarkPhoenix
a917207e07 Reimplement some of logic used in ammo picker in a service package 2019-11-12 19:39:52 +03:00
DarkPhoenix
c21b92945f Merge branch 'master' into ammo_graph 2019-11-12 16:58:34 +03:00
DarkPhoenix
049bd10797 Merge branch 'master' into ammo_graph 2019-11-07 19:02:12 +03:00
DarkPhoenix
bbb96a73b7 Add alias file for trig dread-related items 2019-11-06 23:09:28 +03:00
DarkPhoenix
b486276167 Merge branch 'master' into ammo_graph 2019-10-31 11:29:31 +03:00
DarkPhoenix
e70ebad3f7 Bump version 2019-10-22 15:37:25 +03:00
DarkPhoenix
4dfe5c3abf Update database and effects to 1589866 2019-10-22 15:36:37 +03:00
DarkPhoenix
e4df215e47 Change order of market group sorting for ammo picker 2019-10-18 12:10:02 +03:00
DarkPhoenix
33cb332978 Plug market sort into module grouping 2019-10-17 18:21:03 +03:00
DarkPhoenix
7c8f4d51bb Move sorting from item view to market service 2019-10-17 17:39:44 +03:00
DarkPhoenix
66bf7b3dc6 Merge branch 'master' into ammo_graph 2019-10-17 17:19:40 +03:00
DarkPhoenix
8bb1d43d0c Show module groups / drone header / fighter header in dialog 2019-10-11 13:23:18 +03:00
DarkPhoenix
3d70ca941c Change ammo picker window to be auxiliary frame rather than modal dialog 2019-10-10 19:10:28 +03:00
DarkPhoenix
04a1e95730 Get list of fighters and drones as well 2019-10-10 16:03:56 +03:00
DarkPhoenix
9fddb64ef9 Get list of damage dealer mods with ammo data 2019-10-10 15:40:39 +03:00
DarkPhoenix
e883035120 Do not show menu item for non-dps graphs 2019-10-10 13:38:42 +03:00
DarkPhoenix
4d2da8e52e Merge branch 'master' into ammo_graph 2019-10-10 12:41:30 +03:00
DarkPhoenix
c1df335f08 Add context menu item which will handle fit duplication 2019-10-08 15:39:16 +03:00
671 changed files with 2468567 additions and 3272747 deletions

View File

@@ -1,126 +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:"
- "python -m pip install -r requirements.txt"
- "python -m 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:"
- ps: echo("Build pyfa:")
# Build gamedata DB
- "python db_update.py"
##########
# PyInstaller - create binaries for pyfa
##########
- 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)
@@ -128,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

View File

@@ -3,13 +3,12 @@ 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:
@@ -22,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

View File

@@ -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, targetProfile, 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:":

View File

@@ -233,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([

View File

@@ -23,6 +23,7 @@ import functools
import itertools
import json
import os
import re
import sqlite3
import sys
@@ -32,7 +33,7 @@ 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 = 1
GAMEDATA_SCHEMA_VERSION = 4
def db_needs_update():
@@ -40,7 +41,7 @@ def db_needs_update():
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:
except (KeyboardInterrupt, SystemExit):
raise
# If we have no source data - return None; should not update in this case
except:
@@ -61,7 +62,7 @@ def db_needs_update():
db_schema_version = int(row[0])
cursor.close()
db.close()
except KeyboardInterrupt:
except (KeyboardInterrupt, SystemExit):
raise
except:
print('Error when fetching gamedata DB metadata')
@@ -88,52 +89,177 @@ def update_db():
# Create the database tables
eos.db.gamedata_meta.create_all()
# Config dict
tables = {
'clonegrades': ('fsd_lite', eos.gamedata.AlphaCloneSkill),
'dogmaattributes': ('bulkdata', eos.gamedata.AttributeInfo),
'dogmaeffects': ('bulkdata', eos.gamedata.Effect),
'dogmatypeattributes': ('bulkdata', eos.gamedata.Attribute),
'dogmatypeeffects': ('bulkdata', eos.gamedata.ItemEffect),
'dogmaunits': ('bulkdata', eos.gamedata.Unit),
'evecategories': ('fsd_lite', eos.gamedata.Category),
'evegroups': ('fsd_lite', eos.gamedata.Group),
'metagroups': ('fsd_binary', eos.gamedata.MetaGroup),
'evetypes': ('fsd_lite', eos.gamedata.Item),
'traits': ('phobos', eos.gamedata.Traits),
'metadata': ('phobos', eos.gamedata.MetaData),
'marketgroups': ('fsd_binary', eos.gamedata.MarketGroup)}
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
fieldMapping = {
'marketgroups': {
'id': 'marketGroupID',
'name': 'marketGroupName'},
'metagroups': {
'id': 'metaGroupID'}}
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)
rowsInValues = (
'evetypes',
'evegroups',
'evecategories',
'marketgroups',
'metagroups')
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'] == 'Capsule' or
row['groupID'] == 4033 # destructible effect beacons
):
row['published'] = True
# Nearly useless and clutter search results too much
elif row['typeName'].startswith('Limited Synth '):
row['published'] = False
def convertIcons(data):
new = []
for k, v in list(data.items()):
v['iconID'] = k
new.append(v)
return new
def convertClones(data):
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,
1983) # the "container" for the abyssal environments
):
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({
@@ -144,18 +270,25 @@ def update_db():
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')
return newData
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 convertTraits(data):
def processTraits():
print('processing traits')
data = _readData('phobos', 'traits')
def convertSection(sectionData):
sectionLines = []
@@ -182,9 +315,42 @@ def update_db():
traitLine = '<br />\n<br />\n'.join(typeLines)
newRow = {'typeID': typeId, 'traitText': traitLine}
newData.append(newRow)
return newData
def fillReplacements(tables):
_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, skillLevel in raw.items():
reqSkills[int(skillTypeID)] = skillLevel
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
@@ -196,7 +362,6 @@ def update_db():
return True
return False
print('finding replacements')
skillReqAttribs = {
182: 277,
183: 278,
@@ -208,18 +373,18 @@ def update_db():
# Get data on type groups
# Format: {type ID: group ID}
typesGroups = {}
for row in tables['evetypes']:
for row in eveTypesData:
typesGroups[row['typeID']] = row['groupID']
# Get data on item effects
# Format: {type ID: set(effect, IDs)}
typesEffects = {}
for row in tables['dogmatypeeffects']:
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 tables['dogmatypeattributes']:
for row in dogmaTypeAttributesData:
attributeID = row['attributeID']
if attributeID in skillReqAttribsFlat:
typeSkillAttribs = typesSkillAttribs.setdefault(row['typeID'], {})
@@ -258,13 +423,13 @@ def update_db():
typeSkillReqs[skillType] = skillLevel
# Format: {group ID: category ID}
groupCategories = {}
for row in tables['evegroups']:
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 tables['evetypes']:
for row in eveTypesData:
typeID = row['typeID']
# Ignore items outside of categories we need
if groupCategories[typesGroups[typeID]] not in (
@@ -301,134 +466,63 @@ def update_db():
if compareAttrs(type1[1], type2[1]):
replacements.setdefault(type1[0], set()).add(type2[0])
replacements.setdefault(type2[0], set()).add(type1[0])
# Put this data into types table so that normal process hooks it up
for row in tables['evetypes']:
row['replacements'] = ','.join('{}'.format(tid) for tid in sorted(replacements.get(row['typeID'], ())))
# 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))
data = {}
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)
# Dump all data to memory so we can easely cross check ignored rows
for jsonName, (minerName, cls) in tables.items():
with open(os.path.join(JSON_DIR, minerName, '{}.json'.format(jsonName)), encoding='utf-8') as f:
tableData = json.load(f)
if jsonName in rowsInValues:
newTableData = []
for k, v in tableData.items():
row = {}
row.update(v)
if 'id' not in row:
row['id'] = int(k)
newTableData.append(row)
tableData = newTableData
if jsonName == 'icons':
tableData = convertIcons(tableData)
if jsonName == 'traits':
tableData = convertTraits(tableData)
if jsonName == 'clonegrades':
tableData = convertClones(tableData)
data[jsonName] = tableData
eveTypesData = processEveTypes()
eveGroupsData = processEveGroups()
processEveCategories()
processDogmaAttributes()
dogmaTypeAttributesData = processDogmaTypeAttributes(eveTypesData)
processDynamicItemAttributes()
processDogmaEffects()
dogmaTypeEffectsData = processDogmaTypeEffects(eveTypesData)
processDogmaUnits()
processMarketGroups()
processMetaGroups()
processCloneGrades()
processTraits()
processMetadata()
fillReplacements(data)
# Set with typeIDs which we will have in our database
# Sometimes CCP unpublishes some items we want to have published, we
# can do it here - just add them to initial set
eveTypes = set()
for row in data['evetypes']:
if (
row['published'] or
row['typeName'] == 'Capsule' or
# group Ship Modifiers, for items like tactical t3 ship modes
row['groupID'] == 1306 or
# Civilian weapons
(row['typeName'].startswith('Civilian') and "Shuttle" not in row['typeName']) 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) or
# Dark Blood Tracking Disruptor (drops, but rarely)
row['typeID'] == 32416
):
eveTypes.add(row['typeID'])
# ignore checker
def isIgnored(file, row):
if file in ('evetypes', 'dogmatypeeffects', 'dogmatypeattributes') and row['typeID'] not in eveTypes:
return True
return False
# Loop through each json file and write it away, checking ignored rows
for jsonName, table in data.items():
fieldMap = fieldMapping.get(jsonName, {})
tmp = []
print('processing {}'.format(jsonName))
for row in table:
# We don't care about some kind of rows, filter it out if so
if not isIgnored(jsonName, row):
if (
jsonName == 'evetypes' and (
# 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
instance = tables[jsonName][1]()
# fix for issue 80
if jsonName is 'icons' and 'res:/ui/texture/icons/' in str(row['iconFile']).lower():
row['iconFile'] = row['iconFile'].lower().replace('res:/ui/texture/icons/', '').replace('.png', '')
# with res:/ui... references, it points to the actual icon file (including it's size variation of #_size_#)
# strip this info out and get the identifying info
split = row['iconFile'].split('_')
if len(split) == 3:
row['iconFile'] = '{}_{}'.format(split[0], split[2])
if jsonName is 'icons' and 'modules/' in str(row['iconFile']).lower():
row['iconFile'] = row['iconFile'].lower().replace('modules/', '').replace('.png', '')
if jsonName is 'clonegrades':
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'])
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)
# quick and dirty hack to get this data in
with open(os.path.join(JSON_DIR, 'fsd_binary', 'dynamicitemattributes.json'), encoding='utf-8') as f:
bulkdata = json.load(f)
for mutaID, data in bulkdata.items():
muta = eos.gamedata.DynamicItem()
muta.typeID = mutaID
muta.resultingTypeID = data['inputOutputMapping'][0]['resultingType']
eos.db.gamedata_session.add(muta)
for x in data['inputOutputMapping'][0]['applicableTypes']:
item = eos.gamedata.DynamicItemItem()
item.typeID = mutaID
item.applicableTypeID = x
eos.db.gamedata_session.add(item)
for attrID, attrData in data['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)
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()
@@ -436,7 +530,7 @@ def update_db():
metadata_schema_version.field_value = GAMEDATA_SCHEMA_VERSION
eos.db.gamedata_session.add(metadata_schema_version)
eos.db.gamedata_session.commit()
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

View File

@@ -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")

View File

@@ -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

View File

@@ -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>

View File

@@ -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")

View File

@@ -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;

View File

@@ -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}"/>

View File

@@ -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

View File

@@ -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',
)

View File

@@ -17,24 +17,37 @@
# 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):
@@ -42,9 +55,26 @@ if callable(gamedata_connectionstring):
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
@@ -56,6 +86,8 @@ 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)
@@ -82,10 +114,10 @@ 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, \
from eos.db.saveddata import booster, cargo, character, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, \
miscData, mutator, module, override, price, queries, skill, targetProfile, user
pyfalog.debug('Importing gamedata queries')

View File

@@ -1,2 +1,2 @@
__all__ = ["attribute", "category", "effect", "group", "metaData", "dynamicAttributes",
"item", "marketGroup", "metaGroup", "unit", "alphaClones"]
"item", "marketGroup", "metaGroup", "unit", "alphaClones", "implantSet"]

View 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")})

View File

@@ -33,9 +33,6 @@ 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),
@@ -44,7 +41,9 @@ items_table = Table("invtypes", gamedata_meta,
Column("metaLevel", Integer),
Column("metaGroupID", Integer, ForeignKey("invmetagroups.metaGroupID"), index=True),
Column("variationParentTypeID", Integer, ForeignKey("invtypes.typeID"), index=True),
Column("replacements", String))
Column("replacements", String),
Column("reqskills", String),
Column("requiredfor", String))
from .traits import traits_table # noqa

View File

@@ -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.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,29 +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()
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
@@ -109,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.
@@ -125,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
@@ -148,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
@@ -162,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
@@ -172,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
@@ -183,19 +193,19 @@ 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()
group = get_gamedata_session().query(Group).options(*processEager(eager)).filter(Group.name == lookfor).first()
if group is not None:
groupNameMap[lookfor] = group.ID
else:
@@ -210,21 +220,21 @@ 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()
if category is not None:
categoryNameMap[lookfor] = category.ID
@@ -240,21 +250,21 @@ 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()
if metaGroup is not None:
metaGroupNameMap[lookfor] = metaGroup.ID
@@ -264,16 +274,16 @@ def getMetaGroup(lookfor, eager=None):
def getMetaGroups():
return gamedata_session.query(MetaGroup).all()
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")
@@ -285,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
@@ -299,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()
@@ -314,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:
@@ -325,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:
@@ -352,7 +383,7 @@ def getVariations(itemids, groupIDs=None, where=None, eager=None):
itemfilter = or_(*(items_table.c.variationParentTypeID == itemid for itemid in itemids))
filter = processWhere(itemfilter, where)
vars = gamedata_session.query(Item).options(*processEager(eager)).filter(filter).all()
vars = get_gamedata_session().query(Item).options(*processEager(eager)).filter(filter).all()
if vars:
return vars
@@ -360,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
@@ -375,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
@@ -384,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
@@ -403,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")
@@ -416,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:
@@ -426,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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -6,16 +6,16 @@ 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"),
"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")
)
"""

View 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;")

View 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))

View 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))

View File

@@ -0,0 +1,42 @@
"""
Migration 38
- Armor hardener tiericide
"""
CONVERSIONS = {
16357: ( # Experimental Enduring EM Armor Hardener I
16353, # Upgraded Armor EM Hardener I
),
16365: ( # Experimental Enduring Explosive Armor Hardener I
16361, # Upgraded Armor Explosive Hardener I
),
16373: ( # Experimental Enduring Kinetic Armor Hardener I
16369, # Upgraded Armor Kinetic Hardener I
),
16381: ( # Experimental Enduring Thermal Armor Hardener I
16377, # Upgraded Armor Thermal Hardener I
),
16359: ( # Prototype Compact EM Armor Hardener I
16355, # Limited Armor EM Hardener I
),
16367: ( # Prototype Compact Explosive Armor Hardener I
16363, # Limited Armor Explosive Hardener I
),
16375: ( # Prototype Compact Kinetic Armor Hardener I
16371, # Limited Armor Kinetic Hardener I
),
16383: ( # Prototype Compact Thermal Armor Hardener I
16379, # Limited Armor Thermal Hardener I
)
}
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))

View File

@@ -0,0 +1,34 @@
"""
Migration 39
- Shield amplifier tiericide
- CCP getting rid of DB TDs due to exploits
"""
CONVERSIONS = {
1798: ( # 'Basic' EM Shield Amplifier
9562, # Supplemental EM Ward Amplifier
),
1804: ( # 'Basic' Explosive Shield Amplifier
9574, # Supplemental Explosive Deflection Amplifier
),
1802: ( # 'Basic' Kinetic Shield Amplifier
9570, # Supplemental Kinetic Deflection Amplifier
),
1800: ( # 'Basic' Thermal Shield Amplifier
9566, # Supplemental Thermal Dissipation Amplifier
),
22933: ( # 'Investor' Tracking Disruptor I
32416, # Dark Blood Tracking Disruptor
)
}
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))

View File

@@ -0,0 +1,18 @@
"""
Migration 40
Imports all item conversions since Migration 28 and runs them against module.baseItemID. This column seems to have been
forgotten about since it's been added.
"""
from .upgrade36 import CONVERSIONS as u36
from .upgrade37 import CONVERSIONS as u37
from .upgrade38 import CONVERSIONS as u38
from .upgrade39 import CONVERSIONS as u39
def upgrade(saveddata_engine):
for conversions in [u36, u37, u38, u39]:
for replacement_item, list in conversions.items():
for retired_item in list:
saveddata_engine.execute('UPDATE "modules" SET "baseItemID" = ? WHERE "baseItemID" = ?',
(replacement_item, retired_item))

View File

@@ -0,0 +1,50 @@
"""
Migration 41
- Resistance plating tiericide
"""
CONVERSIONS = {
16345: ( # Upgraded Layered Coating I
16347, # Limited Layered Plating I
16349, # 'Scarab' Layered Plating I
16351, # 'Grail' Layered Plating I
),
16305: ( # Upgraded Multispectrum Coating I
16307, # Limited Adaptive Nano Plating I
16309, # 'Collateral' Adaptive Nano Plating I
16311, # 'Refuge' Adaptive Nano Plating I
),
16329: ( # Upgraded EM Coating I
16331, # Limited EM Plating I
16333, # 'Contour' EM Plating I
16335, # 'Spiegel' EM Plating I
),
16321: ( # Upgraded Explosive Coating I
16323, # Limited Explosive Plating I
16325, # Experimental Explosive Plating I
16319, # 'Aegis' Explosive Plating I
),
16313: ( # Upgraded Kinetic Coating I
16315, # Limited Kinetic Plating I
16317, # Experimental Kinetic Plating I
16327, # 'Element' Kinetic Plating I
),
16337: ( # Upgraded Thermal Coating I
16339, # Limited Thermal Plating I
16341, # Experimental Thermal Plating I
16343, # Prototype Thermal Plating I
)
}
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 "modules" SET "baseItemID" = ? WHERE "baseItemID" = ?',
(replacement_item, retired_item))
saveddata_engine.execute('UPDATE "cargo" SET "itemID" = ? WHERE "itemID" = ?',
(replacement_item, retired_item))

View File

@@ -0,0 +1,50 @@
"""
Migration 42
- Resistance membrane tiericide
"""
CONVERSIONS = {
16391: ( # Compact Multispectrum Energized Membrane
16389, # Experimental Energized Adaptive Nano Membrane I
16387, # Limited Energized Adaptive Nano Membrane I
16385, # Upgraded Energized Adaptive Nano Membrane I
),
16423: ( # Compact Layered Energized Membrane
16421, # Experimental Energized Armor Layering Membrane I
16419, # Limited Energized Armor Layering Membrane I
16417, # Upgraded Energized Armor Layering Membrane I
),
16415: ( # Compact EM Energized Membrane
16413, # Experimental Energized EM Membrane I
16411, # Limited Energized EM Membrane I
16409, # Upgraded Energized EM Membrane I
),
16407: ( # Compact Explosive Energized Membrane
16405, # Experimental Energized Explosive Membrane I
16403, # Limited Energized Explosive Membrane I
16401, # Upgraded Energized Explosive Membrane I
),
16399: ( # Compact Kinetic Energized Membrane
16397, # Experimental Energized Kinetic Membrane I
16395, # Limited Energized Kinetic Membrane I
16393, # Upgraded Energized Kinetic Membrane I
),
16431: ( # Compact Thermal Energized Membrane
16429, # Experimental Energized Thermal Membrane I
16427, # Limited Energized Thermal Membrane I
16425, # Upgraded Energized Thermal Membrane I
)
}
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 "modules" SET "baseItemID" = ? WHERE "baseItemID" = ?',
(replacement_item, retired_item))
saveddata_engine.execute('UPDATE "cargo" SET "itemID" = ? WHERE "itemID" = ?',
(replacement_item, retired_item))

View File

@@ -13,6 +13,5 @@ __all__ = [
"miscData",
"targetProfile",
"override",
"implantSet",
"loadDefaultDatabaseValues"
"implantSet"
]

View File

@@ -24,16 +24,20 @@ 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", 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)
)
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})

View File

@@ -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),
@@ -233,8 +235,10 @@ mapper(es_Fit, fits_table,
"_Fit__character": relation(
Character,
backref="fits"),
"_Fit__damagePattern": relation(DamagePattern),
"_Fit__targetProfile": relation(TargetProfile),
"_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,

View File

@@ -1,248 +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.targetProfile import TargetProfile as es_TargetProfile
class ImportError(Exception):
pass
class DefaultDatabaseValues:
def __init__(self):
pass
instance = None
@classmethod
def importDamageProfileDefaults(cls):
damageProfileList = [["Uniform", 25, 25, 25, 25],
["[Generic]EM", 1, 0, 0, 0],
["[Generic]Thermal", 0, 1, 0, 0],
["[Generic]Kinetic", 0, 0, 1, 0],
["[Generic]Explosive", 0, 0, 0, 1],
["[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", 7.7, 7.7, 0, 0],
["[Frequency Crystals][T2] Scorch", 9, 2, 0, 0],
["[Frequency Crystals][T2] Gleam", 7, 7, 0, 0],
["[Frequency Crystals][T2] Aurora", 5, 3, 0, 0],
["[Frequency Crystals]Multifrequency", 7, 5, 0, 0],
["[Frequency Crystals]Gamma", 7, 4, 0, 0],
["[Frequency Crystals]Xray", 6, 4, 0, 0],
["[Frequency Crystals]Ultraviolet", 6, 3, 0, 0],
["[Frequency Crystals]Standard", 5, 3, 0, 0],
["[Frequency Crystals]Infrared", 5, 2, 0, 0],
["[Frequency Crystals]Microwave", 4, 2, 0, 0],
["[Frequency Crystals]Radio", 5, 0, 0, 0],
["[Hybrid Charges][T2] Void", 0, 7.7, 7.7, 0],
["[Hybrid Charges][T2] Null", 0, 6, 5, 0],
["[Hybrid Charges][T2] Javelin", 0, 8, 6, 0],
["[Hybrid Charges][T2] Spike", 0, 4, 4, 0],
["[Hybrid Charges]Antimatter", 0, 5, 7, 0],
["[Hybrid Charges]Plutonium", 0, 5, 6, 0],
["[Hybrid Charges]Uranium", 0, 4, 6, 0],
["[Hybrid Charges]Thorium", 0, 4, 5, 0],
["[Hybrid Charges]Lead", 0, 3, 5, 0],
["[Hybrid Charges]Iridium", 0, 3, 4, 0],
["[Hybrid Charges]Tungsten", 0, 2, 4, 0],
["[Hybrid Charges]Iron", 0, 2, 3, 0],
["[Missiles]Mjolnir", 1, 0, 0, 0],
["[Missiles]Inferno", 0, 1, 0, 0],
["[Missiles]Scourge", 0, 0, 1, 0],
["[Missiles]Nova", 0, 0, 0, 1],
["[Missiles][Structure] Standup Missile", 1, 1, 1, 1],
["[Projectile Ammo][T2] Hail", 0, 0, 3.3, 12.1],
["[Projectile Ammo][T2] Barrage", 0, 0, 5, 6],
["[Projectile Ammo][T2] Quake", 0, 0, 5, 9],
["[Projectile Ammo][T2] Tremor", 0, 0, 3, 5],
["[Projectile Ammo]EMP", 9, 0, 1, 2],
["[Projectile Ammo]Phased Plasma", 0, 10, 2, 0],
["[Projectile Ammo]Fusion", 0, 0, 2, 10],
["[Projectile Ammo]Depleted Uranium", 0, 3, 2, 3],
["[Projectile Ammo]Titanium Sabot", 0, 0, 6, 2],
["[Projectile Ammo]Proton", 3, 0, 2, 0],
["[Projectile Ammo]Carbonized Lead", 0, 0, 4, 1],
["[Projectile Ammo]Nuclear", 0, 0, 1, 4],
# 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.add(damageProfile)
else:
damageProfile.emAmount = em
damageProfile.thermalAmount = therm
damageProfile.kineticAmount = kin
damageProfile.explosiveAmount = exp
eos.db.commit()
@classmethod
def importTargetProfileDefaults(cls):
targetProfileList = [["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 targetProfileRow in targetProfileList:
name = targetProfileRow[0]
em = targetProfileRow[1]
therm = targetProfileRow[2]
kin = targetProfileRow[3]
exp = targetProfileRow[4]
try:
maxVel = targetProfileRow[5]
except IndexError:
maxVel = None
try:
sigRad = targetProfileRow[6]
except IndexError:
sigRad = None
try:
radius = targetProfileRow[7]
except:
radius = None
targetProfile = eos.db.getTargetProfile(name)
if targetProfile is None:
targetProfile = es_TargetProfile(em, therm, kin, exp, maxVel, sigRad, radius)
targetProfile.name = name
eos.db.add(targetProfile)
else:
targetProfile.emAmount = em
targetProfile.thermalAmount = therm
targetProfile.kineticAmount = kin
targetProfile.explosiveAmount = exp
targetProfile.maxVelocity = maxVel
targetProfile.signatureRadius = sigRad
targetProfile.radius = radius
eos.db.commit()
@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.add(damageProfile)
else:
damageProfile.emAmount = em
damageProfile.thermalAmount = therm
damageProfile.kineticAmount = kin
damageProfile.explosiveAmount = exp
eos.db.commit()

View File

@@ -413,7 +413,7 @@ 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
@@ -434,7 +434,7 @@ def getTargetProfile(lookfor, eager=None):
eager = processEager(eager)
with sd_lock:
pattern = saveddata_session.query(TargetProfile).options(*eager).filter(
TargetProfile.name == lookfor).first()
TargetProfile.rawName == lookfor).first()
else:
raise TypeError("Need integer or string as argument")
return pattern
@@ -470,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
@@ -560,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()
@@ -570,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()

View File

@@ -24,23 +24,28 @@ import datetime
from eos.db import saveddata_meta
from eos.saveddata.targetProfile import TargetProfile
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={
"_maxVelocity": targetProfiles_table.c.maxVelocity,
"_signatureRadius": targetProfiles_table.c.signatureRadius,
"_radius": targetProfiles_table.c.radius})
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})

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@
# ===============================================================================
import json
from collections import OrderedDict
from logbook import Logger
@@ -145,6 +146,12 @@ class Effect(EqBase):
return self.__effectDef is not None
@property
def dealsDamage(self):
if not self.__generated:
self.__generateHandler()
return self.__dealsDamage
def isType(self, type):
"""
Check if this effect is of the passed type
@@ -166,6 +173,7 @@ class Effect(EqBase):
self.__handler = getattr(effectDef, "handler", eos.effects.BaseEffect.handler)
self.__runTime = getattr(effectDef, "runTime", "normal")
self.__activeByDefault = getattr(effectDef, "activeByDefault", True)
self.__dealsDamage = effectDef.dealsDamage
effectType = getattr(effectDef, "type", None)
effectType = effectType if isinstance(effectType, tuple) or effectType is None else (effectType,)
self.__type = effectType
@@ -174,6 +182,7 @@ class Effect(EqBase):
self.__handler = eos.effects.DummyEffect.handler
self.__runTime = "normal"
self.__activeByDefault = True
self.__dealsDamage = False
self.__type = None
pyfalog.debug("ImportError generating handler: {0}", e)
except AttributeError as e:
@@ -181,12 +190,16 @@ class Effect(EqBase):
self.__handler = eos.effects.DummyEffect.handler
self.__runTime = "normal"
self.__activeByDefault = True
self.__dealsDamage = False
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"
self.__activeByDefault = True
self.__dealsDamage = False
self.__type = None
pyfalog.critical("Exception generating handler:")
pyfalog.critical(e)
@@ -199,45 +212,20 @@ 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
@@ -259,9 +247,6 @@ class Item(EqBase):
@property
def attributes(self):
if not self.__moved:
self.moveAttrs()
return self.__attributes
@property
@@ -314,50 +299,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 = {
@@ -373,7 +334,8 @@ class Item(EqBase):
500016: "sisters",
500018: "mordu",
500019: "sansha",
500020: "serpentis"
500020: "serpentis",
500026: "triglavian"
}
@property
@@ -381,7 +343,11 @@ class Item(EqBase):
if self.__race is None:
try:
if self.category.categoryName == 'Structure':
if (
self.category.categoryName == 'Structure' or
# Here until CCP puts their shit together
self.name in ("Thunderchild", "Stormbringer", "Skybreaker")
):
self.__race = "upwell"
else:
self.__race = self.factionMap[self.factionID]
@@ -399,6 +365,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",
@@ -740,5 +707,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)

View File

@@ -21,6 +21,9 @@ from logbook import Logger
from sqlalchemy.orm import reconstructor
from eos.utils.round import roundToPrec
pyfalog = Logger(__name__)
@@ -56,9 +59,8 @@ class BoosterSideEffect:
@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):

View File

@@ -18,21 +18,174 @@
# ===============================================================================
import re
from collections import OrderedDict
from sqlalchemy.orm import reconstructor
import eos.db
# 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")
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')):
@@ -98,7 +251,7 @@ class DamagePattern:
lookup = {}
current = eos.db.getDamagePatternList()
for pattern in current:
lookup[pattern.name] = pattern
lookup[pattern.rawName] = pattern
for line in lines:
try:
@@ -107,6 +260,8 @@ class DamagePattern:
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
@@ -121,6 +276,8 @@ class DamagePattern:
for index, val in enumerate(data):
try:
fields["%sAmount" % cls.DAMAGE_TYPES[index]] = int(val)
except (KeyboardInterrupt, SystemExit):
raise
except:
continue
@@ -131,7 +288,7 @@ class DamagePattern:
eos.db.save(pattern)
else:
pattern = DamagePattern(**fields)
pattern.name = name.strip()
pattern.rawName = name.strip()
eos.db.save(pattern)
patterns.append(pattern)
@@ -147,11 +304,41 @@ class DamagePattern:
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

View File

@@ -364,3 +364,11 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if self.item.groupID in fitDroneGroupLimits:
return True
return False
def canDealDamage(self, ignoreState=False):
if self.item is None:
return False
for effect in self.item.effects.values():
if effect.dealsDamage and (ignoreState or self.amountActive > 0):
return True
return False

View File

@@ -99,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)
@@ -441,3 +441,15 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return False
return True
def canDealDamage(self, ignoreState=False, ignoreAbilityState=False):
if self.item is None:
return False
if not self.active and not ignoreState:
return False
for ability in self.abilities:
if not ability.active and not ignoreAbilityState:
continue
if ability.effect.dealsDamage:
return True
return False

View File

@@ -21,22 +21,25 @@ import datetime
import time
from copy import deepcopy
from itertools import chain
from math import log, sqrt
from math import floor, log, sqrt
from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
import eos.db
from eos import capSim
from eos.calc import calculateMultiplier, calculateLockTime
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.saveddata.targetProfile import TargetProfile
from eos.utils.float import floatUnerr
from eos.utils.stats import DmgTypes, RRTypes
@@ -163,14 +166,29 @@ class Fit:
self.__capUsed = None
self.__capRecharge = None
self.__savedCapSimData.clear()
# Ancillary tank modules affect this
self.__sustainableTank = None
self.__effectiveSustainableTank = None
@property
def targetProfile(self):
return self.__targetProfile
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):
self.__targetProfile = 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
@@ -178,11 +196,25 @@ class Fit:
@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
@@ -347,8 +379,9 @@ class Fit:
@property
def maxTargets(self):
return min(self.extraAttributes["maxTargetsLockedFromSkills"],
self.ship.getModifiedItemAttr("maxLockedTargets"))
maxTargets = min(self.extraAttributes["maxTargetsLockedFromSkills"],
self.ship.getModifiedItemAttr("maxLockedTargets"))
return floor(floatUnerr(maxTargets))
@property
def maxTargetRange(self):
@@ -545,11 +578,15 @@ class Fit:
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)
@@ -559,12 +596,16 @@ class Fit:
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)
@@ -711,6 +752,8 @@ class Fit:
if warfareBuffID == 79: # AOE_Beacon_bioluminescence_cloud
self.ship.boostItemAttr("signatureRadius", value, stackingPenalties=True)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"signatureRadius", value, stackingPenalties=True)
if warfareBuffID == 80: # AOE_Beacon_caustic_cloud_local_repair
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Repair Systems"),
@@ -733,7 +776,11 @@ class Fit:
if warfareBuffID == 90: # Weather_electric_storm_EM_resistance_penalty
for tankType in ("shield", "armor"):
self.ship.boostItemAttr("{}EmDamageResonance".format(tankType), value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"{}EmDamageResonance".format(tankType), value)
self.ship.boostItemAttr("emDamageResonance", value) # for hull
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"emDamageResonance", value) #for hull
if warfareBuffID == 92: # Weather_electric_storm_capacitor_recharge_bonus
self.ship.boostItemAttr("rechargeRate", value, stackingPenalties=True)
@@ -741,32 +788,54 @@ class Fit:
if warfareBuffID == 93: # Weather_xenon_gas_explosive_resistance_penalty
for tankType in ("shield", "armor"):
self.ship.boostItemAttr("{}ExplosiveDamageResonance".format(tankType), value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"{}ExplosiveDamageResonance".format(tankType), value)
self.ship.boostItemAttr("explosiveDamageResonance", value) # for hull
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"explosiveDamageResonance", value) # for hull
if warfareBuffID == 94: # Weather_xenon_gas_shield_hp_bonus
self.ship.boostItemAttr("shieldCapacity", value) # for hull
self.ship.boostItemAttr("shieldCapacity", value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"shieldCapacity", value)
if warfareBuffID == 95: # Weather_infernal_thermal_resistance_penalty
for tankType in ("shield", "armor"):
self.ship.boostItemAttr("{}ThermalDamageResonance".format(tankType), value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"{}ThermalDamageResonance".format(tankType), value)
self.ship.boostItemAttr("thermalDamageResonance", value) # for hull
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"thermalDamageResonance", value) # for hull
if warfareBuffID == 96: # Weather_infernal_armor_hp_bonus
self.ship.boostItemAttr("armorHP", value) # for hull
self.ship.boostItemAttr("armorHP", value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"armorHP", value)
if warfareBuffID == 97: # Weather_darkness_turret_range_penalty
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"),
"maxRange", value, stackingPenalties=True)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"maxRange", value, stackingPenalties=True)
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"),
"falloff", value, stackingPenalties=True)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"falloff", value, stackingPenalties=True)
if warfareBuffID == 98: # Weather_darkness_velocity_bonus
self.ship.boostItemAttr("maxVelocity", value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"maxVelocity", value)
if warfareBuffID == 99: # Weather_caustic_toxin_kinetic_resistance_penalty
for tankType in ("shield", "armor"):
self.ship.boostItemAttr("{}KineticDamageResonance".format(tankType), value)
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"{}KineticDamageResonance".format(tankType), value)
self.ship.boostItemAttr("kineticDamageResonance", value) # for hull
self.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"),
"kineticDamageResonance", value) # for hull
if warfareBuffID == 100: # Weather_caustic_toxin_scan_resolution_bonus
self.ship.boostItemAttr("scanResolution", value, stackingPenalties=True)
@@ -987,6 +1056,16 @@ class Fit:
if mod.isEmpty:
del self.modules[i]
def clearTail(self):
tailPositions = {}
for mod in reversed(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
@@ -1096,7 +1175,7 @@ class Fit:
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
@@ -1104,7 +1183,7 @@ class Fit:
def fighterBayUsed(self):
amount = 0
for f in self.fighters:
amount += f.item.volume * f.amount
amount += f.item.attributes['volume'].value * f.amount
return amount

View File

@@ -64,7 +64,9 @@ ProjectedSystem = {
class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
"""An instance of this class represents a module together with its charge and modified attributes"""
MINING_ATTRIBUTES = ("miningAmount",)
SYSTEM_GROUPS = ("Effect Beacon", "MassiveEnvironments", "Abyssal Hazards", "Non-Interactable Object")
SYSTEM_GROUPS = (
"Effect Beacon", "MassiveEnvironments", "Abyssal Hazards",
"Non-Interactable Object", "Destructible Effect Beacon")
def __init__(self, item, baseItem=None, mutaplasmid=None):
"""Initialize a module from the program"""
@@ -214,8 +216,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:
@@ -318,36 +320,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)
maxRange = duringAcceleration + fullSpeed
if 'fofMissileLaunching' in self.charge.effects:
rangeLimit = self.getModifiedChargeAttr("maxFOFTargetRange")
if rangeLimit:
maxRange = min(maxRange, rangeLimit)
return maxRange
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):
@@ -429,6 +463,20 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return True
return False
def canDealDamage(self, ignoreState=False):
if self.isEmpty:
return False
for effect in self.item.effects.values():
if effect.dealsDamage and (
ignoreState or
effect.isType('offline') or
(effect.isType('passive') and self.state >= FittingModuleState.ONLINE) or
(effect.isType('active') and self.state >= FittingModuleState.ACTIVE) or
(effect.isType('overheat') and self.state >= FittingModuleState.OVERHEATED)
):
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)}
@@ -664,7 +712,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
@@ -680,6 +728,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return False
elif state == FittingModuleState.OVERHEATED and not self.item.isType("overheat"):
return False
# Some destructible effect beacons contain active effects, hardcap those at online state
elif state > FittingModuleState.ONLINE and self.slot == FittingSlot.SYSTEM:
return False
else:
return True
@@ -746,8 +797,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
# 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
@@ -1005,7 +1056,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
elif click == "ctrl":
state = FittingModuleState.OFFLINE
else:
state = transitionMap[currState]
try:
state = transitionMap[currState]
except KeyError:
state = min(transitionMap)
# If passive module tries to transition into online and fails,
# put it to passive instead
if not mod.isValidState(state) and currState == FittingModuleState.ONLINE:

View File

@@ -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:

View File

@@ -19,8 +19,10 @@
import math
import re
from collections import OrderedDict
from logbook import Logger
from sqlalchemy.orm import reconstructor
import eos.db
@@ -28,13 +30,173 @@ 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.303, 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")
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
@@ -44,7 +206,29 @@ class TargetProfile:
self._signatureRadius = signatureRadius
self._radius = radius
_idealTarget = None
@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):
@@ -57,8 +241,9 @@ class TargetProfile:
maxVelocity=0,
signatureRadius=None,
radius=0)
cls._idealTarget.name = 'Ideal Target'
cls._idealTarget.ID = -1
cls._idealTarget.rawName = 'Ideal Target'
cls._idealTarget.ID = 0
cls._idealTarget.builtin = True
return cls._idealTarget
@property
@@ -100,7 +285,7 @@ class TargetProfile:
lookup = {}
current = eos.db.getTargetProfileList()
for pattern in current:
lookup[pattern.name] = pattern
lookup[pattern.rawName] = pattern
for line in lines:
try:
@@ -109,6 +294,8 @@ class TargetProfile:
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
@@ -127,6 +314,8 @@ class TargetProfile:
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
@@ -149,7 +338,7 @@ class TargetProfile:
eos.db.save(pattern)
else:
pattern = TargetProfile(**fields)
pattern.name = name.strip()
pattern.rawName = name.strip()
eos.db.save(pattern)
patterns.append(pattern)
@@ -166,7 +355,7 @@ class TargetProfile:
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.name,
dp.rawName,
dp.emAmount * 100,
dp.thermalAmount * 100,
dp.kineticAmount * 100,
@@ -178,9 +367,39 @@ class TargetProfile:
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.name = "%s copy" % self.name
p.rawName = "%s copy" % self.rawName
return p

View 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
View 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)

View File

@@ -26,11 +26,12 @@ VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle'
class YDef:
def __init__(self, handle, unit, label, selectorLabel=None):
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):
@@ -53,12 +54,13 @@ class YDef:
class XDef:
def __init__(self, handle, unit, label, mainInput, selectorLabel=None):
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):

View File

@@ -37,7 +37,14 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
for mod in src.item.activeModulesIter():
if not mod.isDealingDamage():
continue
if mod.hardpoint == FittingHardpoint.TURRET:
if "ChainLightning" in mod.item.effects:
if inLockRange:
applicationMap[mod] = getVortonMult(
mod=mod,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
elif mod.hardpoint == FittingHardpoint.TURRET:
if inLockRange:
applicationMap[mod] = getTurretMult(
mod=mod,
@@ -56,7 +63,6 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
if inLockRange or (mod.charge is not None and 'fofMissileLaunching' in mod.charge.effects):
applicationMap[mod] = getLauncherMult(
mod=mod,
src=src,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
@@ -151,19 +157,39 @@ def getTurretMult(mod, src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngl
return mult
def getLauncherMult(mod, src, distance, tgtSpeed, tgtSigRadius):
modRange = mod.maxRange
if modRange is None:
def getVortonMult(mod, distance, tgtSpeed, tgtSigRadius):
rangeFactor = calculateRangeFactor(
mod.getModifiedItemAttr('maxRange'),
0,
distance)
applicationFactor = _calcMissileFactor(
atkEr=mod.getModifiedItemAttr('aoeCloudSize'),
atkEv=mod.getModifiedItemAttr('aoeVelocity'),
atkDrf=mod.getModifiedItemAttr('aoeDamageReductionFactor'),
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
return rangeFactor * applicationFactor
def getLauncherMult(mod, distance, tgtSpeed, tgtSigRadius):
missileMaxRangeData = mod.missileMaxRangeData
if missileMaxRangeData is None:
return 0
if distance is not None and distance + src.getRadius() > modRange:
return 0
mult = _calcMissileFactor(
# The ranges already consider ship radius
lowerRange, higherRange, higherChance = missileMaxRangeData
if distance is None or distance <= lowerRange:
distanceFactor = 1
elif lowerRange < distance <= higherRange:
distanceFactor = higherChance
else:
distanceFactor = 0
applicationFactor = _calcMissileFactor(
atkEr=mod.getModifiedChargeAttr('aoeCloudSize'),
atkEv=mod.getModifiedChargeAttr('aoeVelocity'),
atkDrf=mod.getModifiedChargeAttr('aoeDamageReductionFactor'),
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
return mult
return distanceFactor * applicationFactor
def getSmartbombMult(mod, distance):

View File

@@ -31,8 +31,8 @@ from .getter import (
class FitDamageStatsGraph(FitGraph):
def __init__(self):
super().__init__()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._timeCache = TimeCache()
self._projectedCache = ProjectedDataCache()

View File

@@ -23,24 +23,6 @@ import math
from graphs.data.base import SmoothPointGetter
class Time2SpeedGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxSpeed': src.getMaxVelocity(),
'mass': src.item.ship.getModifiedItemAttr('mass'),
'agility': src.item.ship.getModifiedItemAttr('agility')}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
maxSpeed = commonData['maxSpeed']
mass = commonData['mass']
agility = commonData['agility']
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass)))
return speed
class Time2DistanceGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
@@ -60,3 +42,62 @@ class Time2DistanceGetter(SmoothPointGetter):
distance_0 = maxSpeed * 0 + (maxSpeed * agility * mass * math.exp((-0 * 1000000) / (agility * mass)) / 1000000)
distance = distance_t - distance_0
return distance
class Time2SpeedGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, src, tgt):
return {
'maxSpeed': src.getMaxVelocity(),
'mass': src.item.ship.getModifiedItemAttr('mass'),
'agility': src.item.ship.getModifiedItemAttr('agility')}
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
time = x
maxSpeed = commonData['maxSpeed']
mass = commonData['mass']
agility = commonData['agility']
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass)))
return speed
class Time2MomentumGetter(Time2SpeedGetter):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
mass = commonData['mass']
speed = Time2SpeedGetter._calculatePoint(
self, x=x, miscParams=miscParams,
src=src, tgt=tgt, commonData=commonData)
momentum = speed * mass
return momentum
class Time2BumpSpeedGetter(Time2SpeedGetter):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
# S. Santorine, Ship Motion in EVE-Online, p3, Collisions & Bumping section
# https://docs.google.com/document/d/1rwVWjTvzVdPEFETf0vwm649AFb4bgRBaNLpRPaoB03o
# Internally, Santorine's formulas are using millions of kilograms, so we normalize to them here
bumperMass = commonData['mass'] / 10 ** 6
bumperSpeed = Time2SpeedGetter._calculatePoint(
self, x=x, miscParams=miscParams,
src=src, tgt=tgt, commonData=commonData)
tgtMass = miscParams['tgtMass'] / 10 ** 6
tgtSpeed = (2 * bumperSpeed * bumperMass) / (bumperMass + tgtMass)
return tgtSpeed
class Time2BumpDistanceGetter(Time2BumpSpeedGetter):
def _calculatePoint(self, x, miscParams, src, tgt, commonData):
# S. Santorine, Ship Motion in EVE-Online, p3, Collisions & Bumping section
# https://docs.google.com/document/d/1rwVWjTvzVdPEFETf0vwm649AFb4bgRBaNLpRPaoB03o
# Internally, Santorine's formulas are using millions of kilograms, so we normalize to them here
tgtMass = miscParams['tgtMass'] / 10 ** 6
tgtInertia = miscParams['tgtInertia']
tgtSpeed = Time2BumpSpeedGetter._calculatePoint(
self, x=x, miscParams=miscParams,
src=src, tgt=tgt, commonData=commonData)
tgtDistance = tgtSpeed * tgtMass * tgtInertia
return tgtDistance

View File

@@ -19,7 +19,7 @@
from graphs.data.base import FitGraph, XDef, YDef, Input
from .getter import Time2SpeedGetter, Time2DistanceGetter
from .getter import Time2SpeedGetter, Time2DistanceGetter, Time2MomentumGetter, Time2BumpSpeedGetter, Time2BumpDistanceGetter
class FitMobilityGraph(FitGraph):
@@ -30,12 +30,26 @@ class FitMobilityGraph(FitGraph):
xDefs = [XDef(handle='time', unit='s', label='Time', mainInput=('time', 's'))]
yDefs = [
YDef(handle='speed', unit='m/s', label='Speed'),
YDef(handle='distance', unit='km', label='Distance')]
inputs = [Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=10, defaultRange=(0, 30))]
YDef(handle='distance', unit='km', label='Distance'),
YDef(handle='momentum', unit='Gkg⋅m/s', label='Momentum'),
YDef(handle='bumpSpeed', unit='m/s', label='Target speed', selectorLabel='Bump speed'),
YDef(handle='bumpDistance', unit='km', label='Target distance traveled', selectorLabel='Bump distance')]
inputs = [
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=10, defaultRange=(0, 30)),
# Default values in target fields correspond to a random carrier/fax
Input(handle='tgtMass', unit='Mkg', label='Target mass', iconID=76, defaultValue=1300, defaultRange=(100, 2500), conditions=[(None, ('bumpSpeed', 'm/s')), (None, ('bumpDistance', 'km'))], secondaryTooltip='Defined in millions of kilograms'),
Input(handle='tgtInertia', unit=None, label='Target inertia factor', iconID=1401, defaultValue=0.015, defaultRange=(0.03, 0.1), conditions=[(None, ('bumpDistance', 'km'))], secondaryTooltip='Inertia Modifier attribute value of the target ship')]
srcExtraCols = ('Speed', 'Agility')
# Calculation stuff
_normalizers = {('tgtMass', 'Mkg'): lambda v, src, tgt: None if v is None else v * 10 ** 6}
_getters = {
('time', 'speed'): Time2SpeedGetter,
('time', 'distance'): Time2DistanceGetter}
_denormalizers = {('distance', 'km'): lambda v, src, tgt: v / 1000}
('time', 'distance'): Time2DistanceGetter,
('time', 'momentum'): Time2MomentumGetter,
('time', 'bumpSpeed'): Time2BumpSpeedGetter,
('time', 'bumpDistance'): Time2BumpDistanceGetter}
_denormalizers = {
('distance', 'km'): lambda v, src, tgt: v / 1000,
('momentum', 'Gkg⋅m/s'): lambda v, src, tgt: v / 10 ** 9,
('bumpDistance', 'km'): lambda v, src, tgt: v / 1000}

View File

@@ -26,8 +26,8 @@ from .getter import Distance2RpsGetter, Distance2RepAmountGetter, Time2RpsGetter
class FitRemoteRepsGraph(FitGraph):
def __init__(self):
super().__init__()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._timeCache = TimeCache()
def _clearInternalCache(self, reason, extraData):

View File

@@ -27,6 +27,10 @@ from .getter import (
class FitShieldRegenGraph(FitGraph):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.isEffective = gui.mainFrame.MainFrame.getInstance().statsPane.nameViewMap['resistancesViewFull'].showEffective
# UI stuff
internalName = 'shieldRegenGraph'
name = 'Shield Regeneration'
@@ -73,7 +77,3 @@ class FitShieldRegenGraph(FitGraph):
('shieldAmount', '%'): lambda v, src, tgt: v * 100 / src.item.ship.getModifiedItemAttr('shieldCapacity'),
('shieldAmount', 'EHP'): lambda v, src, tgt: src.item.damagePattern.effectivify(src.item, v, 'shield'),
('shieldRegen', 'EHP/s'): lambda v, src, tgt: src.item.damagePattern.effectivify(src.item, v, 'shield')}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.isEffective = gui.mainFrame.MainFrame.getInstance().statsPane.nameViewMap['resistancesViewFull'].showEffective

View File

@@ -26,8 +26,8 @@ from .getter import AU_METERS, Distance2TimeGetter
class FitWarpTimeGraph(FitGraph):
def __init__(self):
super().__init__()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._subspeedCache = SubwarpSpeedCache()
def _clearInternalCache(self, reason, extraData):

View File

@@ -54,6 +54,8 @@ try:
except ImportError as e:
pyfalog.warning('Matplotlib failed to import. Likely missing or incompatible version.')
graphFrame_enabled = False
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
# We can get exceptions deep within matplotlib. Catch those. See GH #1046
tb = traceback.format_exc()
@@ -71,6 +73,8 @@ class GraphCanvasPanel(wx.Panel):
# Remove matplotlib font cache, see #234
try:
cache_dir = mpl._get_cachedir()
except (KeyboardInterrupt, SystemExit):
raise
except:
cache_dir = os.path.expanduser(os.path.join('~', '.matplotlib'))
cache_file = os.path.join(cache_dir, 'fontList.cache')
@@ -168,6 +172,8 @@ class GraphCanvasPanel(wx.Panel):
legendData.append((color, lineStyle, source.shortName))
else:
legendData.append((color, lineStyle, '{} vs {}'.format(source.shortName, target.shortName)))
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
pyfalog.warning('Failed to plot "{}" vs "{}"'.format(source.name, '' if target is None else target.name))
self.canvas.draw()
@@ -241,6 +247,8 @@ class GraphCanvasPanel(wx.Panel):
src=source,
tgt=target)
addYMark(y)
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
pyfalog.warning('Failed to get X mark for "{}" vs "{}"'.format(source.name, '' if target is None else target.name))
# Silently skip this mark, otherwise other marks and legend display will fail

View File

@@ -235,7 +235,7 @@ class GraphControlPanel(wx.Panel):
fieldTextBox = FloatBox(self, self._storedConsts.get((inputDef.handle, inputDef.unit), inputDef.defaultValue))
fieldTextBox.Bind(wx.EVT_TEXT, self.OnNonMainInputChanged)
fieldTextBox.SetToolTip(wx.ToolTip(tooltipText))
fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.RIGHT, 5)
fieldIcon = None
if inputDef.iconID is not None:
icon = BitmapLoader.getBitmap(inputDef.iconID, 'icons')
@@ -295,12 +295,16 @@ class GraphControlPanel(wx.Panel):
self.ySubSelection.Clear()
for yDef in view.yDefs:
if yDef.hidden and not self.graphFrame.includeHidden:
continue
self.ySubSelection.Append(self.formatLabel(yDef, selector=True), yDef)
self.ySubSelection.Enable(len(view.yDefs) > 1)
self.ySubSelection.SetSelection(selectedY)
self.xSubSelection.Clear()
for xDef in view.xDefs:
if xDef.hidden and not self.graphFrame.includeHidden:
continue
self.xSubSelection.Append(self.formatLabel(xDef, selector=True), xDef)
self.xSubSelection.Enable(len(view.xDefs) > 1)
self.xSubSelection.SetSelection(selectedX)

View File

@@ -27,7 +27,7 @@ import gui.globalEvents as GE
import gui.mainFrame
from graphs.data.base import FitGraph
from graphs.events import RESIST_MODE_CHANGED
from gui.auxFrame import AuxiliaryFrame
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from service.const import GraphCacheCleanupReason
from service.settings import GraphSettings
@@ -50,6 +50,7 @@ class GraphFrame(AuxiliaryFrame):
super().__init__(parent, title='Graphs', size=(520, 390), resizeable=True)
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.includeHidden = includeHidden
self.SetIcon(wx.Icon(BitmapLoader.getBitmap('graphs_small', 'gui')))
@@ -74,7 +75,7 @@ class GraphFrame(AuxiliaryFrame):
# Setup - graph selector
for view in FitGraph.views:
if view.hidden and not includeHidden:
if view.hidden and not self.includeHidden:
continue
self.graphSelection.Append(view.name, view())
self.graphSelection.SetSelection(0)

View File

@@ -43,7 +43,7 @@ class BaseWrapper:
if self.isFit:
return '{} ({})'.format(self.item.name, self.item.ship.item.name)
elif self.isProfile:
return self.item.name
return self.item.fullName
return ''
@property
@@ -51,7 +51,7 @@ class BaseWrapper:
if self.isFit:
return '{} ({})'.format(self.item.name, self.item.ship.item.getShortName())
elif self.isProfile:
return self.item.name
return self.item.shortName
return ''
def getMaxVelocity(self, extraMultipliers=None, ignoreAfflictors=()):

View File

@@ -22,12 +22,14 @@
import wx
class AuxiliaryFrame(wx.Frame):
class AuxiliaryMixin:
_instance = None
def __init__(self, parent, id=None, title=None, pos=None, size=None, style=None, name=None, resizeable=False):
baseStyle = wx.FRAME_NO_TASKBAR | wx.FRAME_FLOAT_ON_PARENT | wx.CAPTION | wx.CLOSE_BOX | wx.SYSTEM_MENU
baseStyle = wx.FRAME_NO_TASKBAR | wx.CAPTION | wx.CLOSE_BOX | wx.SYSTEM_MENU
if parent is not None:
baseStyle = baseStyle | wx.FRAME_FLOAT_ON_PARENT
if resizeable:
baseStyle = baseStyle | wx.RESIZE_BORDER | wx.MAXIMIZE_BOX
kwargs = {
@@ -53,14 +55,26 @@ class AuxiliaryFrame(wx.Frame):
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
@classmethod
def openOne(cls, parent, *args, **kwargs):
def openOne(cls, parent, *args, forceReopen=False, **kwargs):
"""If window is open and alive - raise it, open otherwise"""
if not cls._instance:
if not cls._instance or forceReopen:
if cls._instance:
cls._instance.Close()
frame = cls(parent, *args, **kwargs)
cls._instance = frame
frame.Show()
else:
cls._instance.Raise()
return cls._instance
def OnSuppressedAction(self, event):
return
class AuxiliaryFrame(AuxiliaryMixin, wx.Frame):
pass
class AuxiliaryDialog(AuxiliaryMixin, wx.Dialog):
pass

View File

@@ -89,7 +89,7 @@ class BitmapLoader:
@classmethod
def loadBitmap(cls, name, location):
if cls.scaling_factor is None:
cls.scaling_factor = int(wx.GetApp().GetTopWindow().GetContentScaleFactor())
cls.scaling_factor = 1 if 'wxGTK' in wx.PlatformInfo else int(wx.GetApp().GetTopWindow().GetContentScaleFactor())
scale = cls.scaling_factor
filename, img = cls.loadScaledBitmap(name, location, scale)

View File

@@ -24,6 +24,7 @@ import gui.display as d
import gui.fitCommands as cmd
import gui.globalEvents as GE
from gui.contextMenu import ContextMenu
from gui.builtinMarketBrowser.events import ITEM_SELECTED, ItemSelected
from gui.utils.staticHelpers import DragDropHelper
from service.fit import Fit
from service.market import Market
@@ -58,6 +59,7 @@ class CargoView(d.Display):
self.lastFitId = None
self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged)
self.mainFrame.Bind(ITEM_SELECTED, self.addItem)
self.Bind(wx.EVT_LEFT_DCLICK, self.onLeftDoubleClick)
self.Bind(wx.EVT_KEY_UP, self.kbEvent)
@@ -66,6 +68,31 @@ class CargoView(d.Display):
self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu)
def addItem(self, event):
item = Market.getInstance().getItem(event.itemID, eager='group')
if item is None or not item.isCharge:
event.Skip()
return
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
if not fit:
event.Skip()
return
modifiers = wx.GetMouseState().GetModifiers()
amount = 1
if modifiers == wx.MOD_CONTROL:
amount = 10
elif modifiers == wx.MOD_ALT:
amount = 100
elif modifiers == wx.MOD_CONTROL | wx.MOD_ALT:
amount = 1000
self.mainFrame.command.Submit(cmd.GuiAddCargoCommand(
fitID=fitID, itemID=item.ID, amount=amount))
self.mainFrame.additionsPane.select('Cargo')
event.Skip()
def handleListDrag(self, x, y, data):
"""
Handles dragging of items from various pyfa displays which support it

View File

@@ -36,6 +36,11 @@ import gui.fitCommands as cmd
from gui.fitCommands.helpers import droneStackLimit
DRONE_ORDER = ('Light Scout Drones', 'Medium Scout Drones',
'Heavy Attack Drones', 'Sentry Drones', 'Combat Utility Drones',
'Electronic Warfare Drones', 'Logistic Drones', 'Mining Drones', 'Salvage Drones')
class DroneViewDrop(wx.DropTarget):
def __init__(self, dropFn, *args, **kwargs):
super(DroneViewDrop, self).__init__(*args, **kwargs)
@@ -186,17 +191,13 @@ class DroneView(Display):
self.mainFrame.command.Submit(cmd.GuiMergeLocalDroneStacksCommand(
fitID=fitID, srcPosition=srcPosition, dstPosition=dstPosition))
DRONE_ORDER = ('Light Scout Drones', 'Medium Scout Drones',
'Heavy Attack Drones', 'Sentry Drones', 'Combat Utility Drones',
'Electronic Warfare Drones', 'Logistic Drones', 'Mining Drones', 'Salvage Drones')
def droneKey(self, drone):
@staticmethod
def droneKey(drone):
sMkt = Market.getInstance()
groupName = sMkt.getMarketGroupByItem(drone.item).name
return (self.DRONE_ORDER.index(groupName),
drone.item.name)
return (DRONE_ORDER.index(groupName), drone.item.name)
def fitChanged(self, event):
event.Skip()

View File

@@ -34,6 +34,9 @@ from service.fit import Fit
from service.market import Market
FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter')
class FighterViewDrop(wx.DropTarget):
def __init__(self, dropFn, *args, **kwargs):
super(FighterViewDrop, self).__init__(*args, **kwargs)
@@ -250,11 +253,10 @@ class FighterDisplay(d.Display):
def _merge(src, dst):
return
FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter')
def fighterKey(self, fighter):
@staticmethod
def fighterKey(fighter):
groupName = Market.getInstance().getGroupByItem(fighter.item).name
orderPos = self.FIGHTER_ORDER.index(groupName)
orderPos = FIGHTER_ORDER.index(groupName)
# Sort support fighters by name, ignore their abilities
if groupName == 'Support Fighter':
abilityEffectIDs = ()

View File

@@ -299,7 +299,10 @@ class ImplantDisplay(d.Display):
sourceContext1 = "implantItem" if fit.implantSource == ImplantLocation.FIT else "implantItemChar"
sourceContext2 = "implantItemMisc" if fit.implantSource == ImplantLocation.FIT else "implantItemMiscChar"
itemContext = None if mainImplant is None else Market.getInstance().getCategoryByItem(mainImplant.item).name
menu = ContextMenu.getMenu(self, mainImplant, selection, (sourceContext1, itemContext), (sourceContext2, itemContext))
menu = ContextMenu.getMenu(self, mainImplant, selection,
(sourceContext1, itemContext),
(sourceContext2, itemContext)
)
if menu:
self.PopupMenu(menu)
@@ -313,7 +316,7 @@ class ImplantDisplay(d.Display):
implants.append(implant)
return implants
def addImplantSet(self, impSet):
def addImplants(self, implants):
self.mainFrame.command.Submit(cmd.GuiAddImplantSetCommand(
fitID=self.mainFrame.getActiveFit(),
itemIDs=[i.itemID for i in impSet.implants]))
itemIDs=[i.itemID for i in implants]))

View File

@@ -90,8 +90,6 @@ class ProjectedView(d.Display):
self.Bind(wx.EVT_LEFT_DCLICK, self.onLeftDoubleClick)
self.Bind(wx.EVT_KEY_UP, self.kbEvent)
self.droneView = gui.builtinAdditionPanes.droneView.DroneView
self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu)
self.SetDropTarget(ProjectedViewDrop(self.handleListDrag))
@@ -119,12 +117,12 @@ class ProjectedView(d.Display):
fitID=fitID, itemID=fit.modules[int(data[1])].itemID))
elif data[0] == 'market':
itemID = int(data[1])
category = Market.getInstance().getItem(itemID, eager=('group.category')).category.name
if category == 'Module':
item = Market.getInstance().getItem(itemID)
if item.isModule:
self.mainFrame.command.Submit(cmd.GuiAddProjectedModuleCommand(fitID=fitID, itemID=itemID))
elif category == 'Drone':
elif item.isDrone:
self.mainFrame.command.Submit(cmd.GuiAddProjectedDroneCommand(fitID=fitID, itemID=itemID))
elif category == 'Fighter':
elif item.isFighter:
self.mainFrame.command.Submit(cmd.GuiAddProjectedFighterCommand(fitID=fitID, itemID=itemID))
def kbEvent(self, event):
@@ -162,7 +160,7 @@ class ProjectedView(d.Display):
if item.marketGroup is None:
item = item.metaGroup.parent
return (self.droneView.DRONE_ORDER.index(item.marketGroup.name),
return (gui.builtinAdditionPanes.droneView.DRONE_ORDER.index(item.marketGroup.name),
drone.item.name)
@staticmethod

View File

@@ -6,6 +6,7 @@ from gui.builtinContextMenus import fitAddCurrentlyOpen
from gui.builtinContextMenus import envEffectAdd
from gui.builtinContextMenus import commandFitAdd
from gui.builtinContextMenus.targetProfile import adder
from gui.builtinContextMenus import graphFitAmmoPicker
# Often-used item manipulations
from gui.builtinContextMenus import shipModeChange
from gui.builtinContextMenus import moduleAmmoChange
@@ -36,7 +37,8 @@ from gui.builtinContextMenus import cargoAdd
from gui.builtinContextMenus import cargoAddAmmo
from gui.builtinContextMenus import itemProject
from gui.builtinContextMenus import ammoToDmgPattern
from gui.builtinContextMenus import implantSetAdd
from gui.builtinContextMenus import implantSetApply
from gui.builtinContextMenus import implantSetSave
# Price
from gui.builtinContextMenus import priceOptions
# Resistance panel

View File

@@ -3,7 +3,6 @@ from gui.contextMenu import ContextMenuUnconditional
from gui.utils.clipboard import toClipboard
from service.fit import Fit
from service.port.eft import exportDrones, exportFighters, exportCargo, exportImplants, exportBoosters
from service.settings import ContextMenuSettings
viewSpecMap = {
@@ -17,12 +16,12 @@ viewSpecMap = {
class AdditionsExportAll(ContextMenuUnconditional):
visibilitySetting = 'additionsCopyPaste'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext):
if not ContextMenuSettings.getInstance().get('additionsCopyPaste'):
return False
if srcContext not in viewSpecMap:
return False
fit = Fit.getInstance().getFit(self.mainFrame.getActiveFit())

View File

@@ -3,7 +3,6 @@ from gui.contextMenu import ContextMenuSelection
from gui.utils.clipboard import toClipboard
from service.fit import Fit
from service.port.eft import exportDrones, exportFighters, exportCargo, exportImplants, exportBoosters
from service.settings import ContextMenuSettings
viewSpecMap = {
@@ -17,12 +16,12 @@ viewSpecMap = {
class AdditionsExportAll(ContextMenuSelection):
visibilitySetting = 'additionsCopyPaste'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext, selection):
if not ContextMenuSettings.getInstance().get('additionsCopyPaste'):
return False
if srcContext not in viewSpecMap:
return False
if not selection:

View File

@@ -4,7 +4,6 @@ from gui.contextMenu import ContextMenuUnconditional
from gui.utils.clipboard import fromClipboard
from service.fit import Fit
from service.port.eft import parseAdditions
from service.settings import ContextMenuSettings
viewSpecMap = {
@@ -18,12 +17,12 @@ viewSpecMap = {
class AdditionsImport(ContextMenuUnconditional):
visibilitySetting = 'additionsCopyPaste'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext):
if not ContextMenuSettings.getInstance().get('additionsCopyPaste'):
return False
if srcContext not in viewSpecMap:
return False
fit = Fit.getInstance().getFit(self.mainFrame.getActiveFit())

View File

@@ -5,19 +5,16 @@ import gui.globalEvents as GE
import gui.mainFrame
from gui.contextMenu import ContextMenuSingle
from service.fit import Fit
from service.settings import ContextMenuSettings
class AmmoToDmgPattern(ContextMenuSingle):
visibilitySetting = 'ammoPattern'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if not self.settings.get('ammoPattern'):
return False
if srcContext not in ("marketItemGroup", "marketItemMisc") or self.mainFrame.getActiveFit() is None:
return False

View File

@@ -19,8 +19,8 @@ class AddToCargo(ContextMenuSingle):
sFit = Fit.getInstance()
fitID = self.mainFrame.getActiveFit()
fit = sFit.getFit(fitID)
# Make sure context menu registers in the correct view
if not fit or fit.isStructure:
if not fit or (fit.isStructure and mainItem.category.ID != 8):
return False
return True

View File

@@ -1,4 +1,5 @@
from collections import OrderedDict
from itertools import chain
# noinspection PyPackageRequirements
import wx
@@ -6,7 +7,8 @@ import wx
import gui.globalEvents as GE
import gui.mainFrame
from gui.contextMenu import ContextMenuUnconditional
from service.damagePattern import DamagePattern as import_DamagePattern
from gui.utils.sorter import smartSort
from service.damagePattern import DamagePattern as DmgPatternSvc
from service.fit import Fit
@@ -23,97 +25,90 @@ class ChangeDamagePattern(ContextMenuUnconditional):
return self.mainFrame.getActiveFit() is not None
def getText(self, callingWindow, itmContext):
sDP = import_DamagePattern.getInstance()
sDP = DmgPatternSvc.getInstance()
sFit = Fit.getInstance()
fitID = self.mainFrame.getActiveFit()
self.fit = sFit.getFit(fitID)
self.patterns = sDP.getDamagePatternList()
self.patterns.sort(key=lambda p: (p.name not in ["Uniform", "Selected Ammo"], p.name))
builtinPatterns = sDP.getBuiltinDamagePatternList()
userPatterns = sorted(sDP.getUserDamagePatternList(), key=lambda p: smartSort(p.fullName))
# Order here is important: patterns with duplicate names from the latter will overwrite
# patterns from the former
self.patterns = sorted(
chain(builtinPatterns, userPatterns),
key=lambda p: p.fullName not in ["Uniform", "Selected Ammo"])
self.patternIds = {}
self.subMenus = OrderedDict()
self.singles = []
# iterate and separate damage patterns based on "[Parent] Child"
self.patternEventMap = {}
self.items = (OrderedDict(), OrderedDict())
for pattern in self.patterns:
start, end = pattern.name.find('['), pattern.name.find(']')
if start is not -1 and end is not -1:
currBase = pattern.name[start + 1:end]
name = pattern.name[end + 1:].strip()
if not name:
self.singles.append(pattern)
continue
# set helper attr
setattr(pattern, "_name", name)
if currBase not in self.subMenus:
self.subMenus[currBase] = []
self.subMenus[currBase].append(pattern)
else:
self.singles.append(pattern)
container = self.items
for categoryName in pattern.hierarchy:
container = container[1].setdefault(categoryName, (OrderedDict(), OrderedDict()))
container[0][pattern.shortName] = pattern
# return list of names, with singles first followed by submenu names
self.m = [p.name for p in self.singles] + list(self.subMenus.keys())
return self.m
return list(self.items[0].keys()) + list(self.items[1].keys())
def addPattern(self, rootMenu, pattern):
def _addPattern(self, parentMenu, pattern, name):
id = ContextMenuUnconditional.nextID()
name = getattr(pattern, "_name", pattern.name) if pattern is not None else "No Profile"
self.patternIds[id] = pattern
menuItem = wx.MenuItem(rootMenu, id, name, kind=wx.ITEM_CHECK)
rootMenu.Bind(wx.EVT_MENU, self.handlePatternSwitch, menuItem)
# set pattern attr to menu item
menuItem.pattern = pattern
self.patternEventMap[id] = pattern
menuItem = wx.MenuItem(parentMenu, id, name, kind=wx.ITEM_CHECK)
parentMenu.Bind(wx.EVT_MENU, self.handlePatternSwitch, menuItem)
# determine active pattern
sFit = Fit.getInstance()
fitID = self.mainFrame.getActiveFit()
fit = sFit.getFit(fitID)
if fit:
dp = fit.damagePattern
checked = dp is pattern
else:
checked = False
checked = fit.damagePattern is pattern if fit else False
return menuItem, checked
def _addCategory(self, parentMenu, name):
id = ContextMenuUnconditional.nextID()
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handlePatternSwitch, menuItem)
return menuItem
def isChecked(self, i):
try:
single = self.singles[i]
patternName = list(self.items[0].keys())[i]
except IndexError:
return super().isChecked(i)
if self.fit and single is self.fit.damagePattern:
pattern = self.items[0][patternName]
if self.fit and pattern is self.fit.damagePattern:
return True
return False
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
# Attempt to remove attribute which carries info if non-sub-items should
# be checked or not
self.checked = None
if self.m[i] not in self.subMenus:
# if we're trying to get submenu to something that shouldn't have one,
# redirect event of the item to handlePatternSwitch and put pattern in
# our patternIds mapping, then return None for no submenu
# Pattern as menu item
if i < len(self.items[0]):
id = pitem.GetId()
self.patternIds[id] = self.singles[i]
self.patternEventMap[id] = list(self.items[0].values())[i]
rootMenu.Bind(wx.EVT_MENU, self.handlePatternSwitch, pitem)
return False
sub = wx.Menu()
# Items that have a parent
# Category as menu item - expands further
msw = "wxMSW" in wx.PlatformInfo
for pattern in self.subMenus[self.m[i]]:
mitem, checked = self.addPattern(rootMenu if msw else sub, pattern)
sub.Append(mitem)
mitem.Check(checked)
return sub
def makeMenu(container, parentMenu):
menu = wx.Menu()
for name, subcontainer in container[1].items():
menuItem = self._addCategory(rootMenu if msw else parentMenu, name)
subMenu = makeMenu(subcontainer, menu)
menuItem.SetSubMenu(subMenu)
menu.Append(menuItem)
for name, pattern in container[0].items():
menuItem, checked = self._addPattern(rootMenu if msw else parentMenu, pattern, name)
menu.Append(menuItem)
menuItem.Check(checked)
menu.Bind(wx.EVT_MENU, self.handlePatternSwitch)
return menu
container = list(self.items[1].values())[i - len(self.items[0])]
subMenu = makeMenu(container, rootMenu)
return subMenu
def handlePatternSwitch(self, event):
pattern = self.patternIds.get(event.Id, False)
pattern = self.patternEventMap.get(event.Id, False)
if pattern is False:
event.Skip()
return

View File

@@ -1,4 +1,5 @@
import re
from collections import OrderedDict
from itertools import chain
# noinspection PyPackageRequirements
@@ -10,6 +11,28 @@ from gui.contextMenu import ContextMenuUnconditional
from service.market import Market
class Group:
def __init__(self):
self.groups = OrderedDict()
self.items = []
def sort(self):
self.groups = OrderedDict((k, self.groups[k]) for k in sorted(self.groups))
for group in self.groups.values():
group.sort()
self.items.sort(key=lambda e: e.shortName)
class Entry:
def __init__(self, itemID, name, shortName):
self.itemID = itemID
self.name = name
self.shortName = shortName
class AddEnvironmentEffect(ContextMenuUnconditional):
# CCP doesn't currently provide a mapping between the general Environment, and the specific environment effect
@@ -32,107 +55,78 @@ class AddEnvironmentEffect(ContextMenuUnconditional):
def getText(self, callingWindow, itmContext):
return "Add Environmental Effect"
def _addGroup(self, parentMenu, name):
id = ContextMenuUnconditional.nextID()
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleSelection, menuItem)
return menuItem
def _addEffect(self, parentMenu, typeID, name):
id = ContextMenuUnconditional.nextID()
self.idmap[id] = typeID
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleSelection, menuItem)
return menuItem
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
msw = True if "wxMSW" in wx.PlatformInfo else False
# Wormholes
self.idmap = {}
sub = wx.Menu()
data = self.getData()
msw = "wxMSW" in wx.PlatformInfo
wormhole_item = wx.MenuItem(sub, wx.ID_ANY, "Wormhole")
wormhole_menu = wx.Menu()
wormhole_item.SetSubMenu(wormhole_menu)
sub.Append(wormhole_item)
grouped_data, flat_data = self.getEffectBeacons()
self.buildMenu(grouped_data, flat_data, wormhole_menu, rootMenu, msw)
# Incursions
grouped_data, flat_data = self.getEffectBeacons(incursions=True)
self.buildMenu(grouped_data, flat_data, sub, rootMenu, msw)
# Abyssal Weather
abyssal_item = wx.MenuItem(sub, wx.ID_ANY, "Abyssal Weather")
abyssal_menu = wx.Menu()
abyssal_item.SetSubMenu(abyssal_menu)
sub.Append(abyssal_item)
grouped_data, flat_data = self.getAbyssalWeather()
self.buildMenu(grouped_data, flat_data, abyssal_menu, rootMenu, msw)
# Localized Weather
local_item = wx.MenuItem(sub, wx.ID_ANY, "Localized")
local_menu = wx.Menu()
local_item.SetSubMenu(local_menu)
sub.Append(local_item)
grouped_data, flat_data = self.getLocalizedEnvironments()
self.buildMenu(grouped_data, flat_data, local_menu, rootMenu, msw)
def makeMenu(data, parentMenu):
menu = wx.Menu()
for group_name in data.groups:
menuItem = self._addGroup(rootMenu if msw else parentMenu, group_name)
subMenu = makeMenu(data.groups[group_name], menu)
menuItem.SetSubMenu(subMenu)
menu.Append(menuItem)
for entry in data.items:
menuItem = self._addEffect(rootMenu if msw else parentMenu, entry.itemID, entry.shortName)
menu.Append(menuItem)
menu.Bind(wx.EVT_MENU, self.handleSelection)
return menu
sub = makeMenu(data, rootMenu)
return sub
def handleSelection(self, event):
# Skip events ids that aren't mapped
swObj, swName = self.idmap.get(event.Id, (False, False))
if not swObj and not swName:
swObj = self.idmap.get(event.Id, False)
if not swObj:
event.Skip()
return
fitID = self.mainFrame.getActiveFit()
self.mainFrame.command.Submit(cmd.GuiAddProjectedModuleCommand(fitID, swObj.ID))
self.mainFrame.command.Submit(cmd.GuiAddProjectedModuleCommand(fitID, swObj))
def buildMenu(self, grouped_data, flat_data, local_menu, rootMenu, msw):
def getData(self):
data = Group()
data.groups['Metaliminal Storm'] = self.getEffectBeacons(
'Electrical', 'Exotic', 'Gamma', 'Plasma',
extra_garbage=('Metaliminal', 'Storm', 'Matter', 'Ray', 'Firestorm'))
data.groups['Wormhole'] = self.getEffectBeacons(
'Black Hole', 'Cataclysmic Variable', 'Magnetar',
'Pulsar', 'Red Giant', 'Wolf Rayet')
data.groups['Abyssal Weather'] = self.getAbyssalWeather()
data.groups['Sansha Incursion'] = self.getEffectBeacons('Sansha Incursion')
data.groups['Triglavian Invasion'] = self.getEffectBeacons('Triglavian Invasion')
data.groups['Triglavian Invasion'].groups['Destructible Beacons'] = self.getDestructibleBeacons()
return data
def processFlat(data, root, sub):
for swData in sorted(data, key=lambda tpl: tpl[2]):
wxid = ContextMenuUnconditional.nextID()
swObj, swName, swClass = swData
self.idmap[wxid] = (swObj, swName)
subItem = wx.MenuItem(sub, wxid, swClass)
if msw:
root.Bind(wx.EVT_MENU, self.handleSelection, subItem)
else:
sub.Bind(wx.EVT_MENU, self.handleSelection, subItem)
sub.Append(subItem)
for swType in sorted(grouped_data):
subItem = wx.MenuItem(local_menu, wx.ID_ANY, swType)
grandSub = wx.Menu()
subItem.SetSubMenu(grandSub)
local_menu.Append(subItem)
processFlat(grouped_data[swType], rootMenu, grandSub)
processFlat(flat_data, rootMenu, local_menu)
def getEffectBeacons(self, incursions=False):
def getEffectBeacons(self, *groups, extra_garbage=()):
"""
Get dictionary with wormhole system-wide effects
Get dictionary with system-wide effects
"""
compacted = len(groups) <= 1
sMkt = Market.getInstance()
# todo: rework this
# Container for system-wide effects
grouped = {}
# Expressions for matching when detecting effects we're looking for
if incursions:
validgroups = ("Incursion ship attributes effects",
"Invasion Effects")
else:
validgroups = ("Black Hole Effect Beacon",
"Cataclysmic Variable Effect Beacon",
"Magnetar Effect Beacon",
"Pulsar Effect Beacon",
"Red Giant Beacon",
"Wolf Rayet Effect Beacon")
data = Group()
# Stuff we don't want to see in names
garbages = ("Effects?", "Beacon", "ship attributes effects")
garbages = ["System Effects", "Effects"]
garbages.extend(extra_garbage)
# Get group with all the system-wide beacons
grp = sMkt.getGroup("Effect Beacon")
@@ -140,9 +134,9 @@ class AddEnvironmentEffect(ContextMenuUnconditional):
# Cycle through them
for beacon in sMkt.getItemsByGroup(grp):
# Check if it belongs to any valid group
for group in validgroups:
for group in groups:
# Check beginning of the name only
if re.match(group, beacon.name):
if re.search(group, beacon.name):
# Get full beacon name
beaconname = beacon.name
for garbage in garbages:
@@ -159,64 +153,65 @@ class AddEnvironmentEffect(ContextMenuUnconditional):
groupname = re.sub(garbage, "", groupname)
groupname = re.sub(" {2,}", " ", groupname).strip()
# Add stuff to dictionary
if groupname not in grouped:
grouped[groupname] = set()
grouped[groupname].add((beacon, beaconname, shortname))
if compacted:
container = data.items
else:
container = data.groups.setdefault(groupname, Group()).items
container.append(Entry(beacon.ID, beaconname, shortname))
# Break loop on 1st result
break
return grouped, ()
data.sort()
return data
def getAbyssalWeather(self):
sMkt = Market.getInstance()
data = Group()
environments = {x.ID: x for x in sMkt.getGroup("Abyssal Environment").items}
items = chain(sMkt.getGroup("MassiveEnvironments").items, sMkt.getGroup("Non-Interactable Object").items)
grouped = {}
flat = set()
items = chain(
sMkt.getGroup("MassiveEnvironments").items,
sMkt.getGroup("Non-Interactable Object").items)
for beacon in items:
if not beacon.isType('projected'):
continue
type = self.__class__.abyssal_mapping.get(beacon.name[0:-2], None)
type = environments.get(type, None)
if type is None:
continue
if type.name not in grouped:
grouped[type.name] = set()
subdata = data.groups.setdefault(type.name, Group())
display_name = "{} {}".format(type.name, beacon.name[-1:])
grouped[type.name].add((beacon, display_name, display_name))
subdata.items.append(Entry(beacon.ID, display_name, display_name))
data.sort()
# Localized abyssal hazards
items = sMkt.getGroup("Abyssal Hazards").items
if items:
subdata = data.groups.setdefault('Localized', Group())
for beacon in sMkt.getGroup("Abyssal Hazards").items:
if not beacon.isType('projected'):
continue
# Localized effects, currently, have a name like "(size) (type) Cloud"
# Until this inevitably changes, do a simple split
name_parts = beacon.name.split(" ")
key = name_parts[1].strip()
subsubdata = subdata.groups.setdefault(key, Group())
subsubdata.items.append(Entry(beacon.ID, beacon.name, beacon.name))
subdata.sort()
# PVP weather
flat.add((sMkt.getItem(49766), 'PvP Weather', 'PvP Weather'))
data.items.append(Entry(49766, 'PvP Weather', 'PvP Weather'))
return grouped, flat
return data
def getLocalizedEnvironments(self):
def getDestructibleBeacons(self):
data = Group()
sMkt = Market.getInstance()
grp = sMkt.getGroup("Abyssal Hazards")
grouped = dict()
for beacon in grp.items:
if not beacon.isType('projected'):
for item in sMkt.getItemsByGroup(sMkt.getGroup('Destructible Effect Beacon')):
if not item.isType('projected'):
continue
# Localized effects, currently, have a name like "(size) (type) Cloud"
# Until this inevitably changes, do a simple split
name_parts = beacon.name.split(" ")
key = name_parts[1].strip()
if key not in grouped:
grouped[key] = set()
grouped[key].add((beacon, beacon.name, beacon.name))
return grouped, ()
data.items.append(Entry(item.ID, item.name, item.name))
data.sort()
return data
AddEnvironmentEffect.register()

View File

@@ -0,0 +1,241 @@
# noinspection PyPackageRequirements
import wx
import gui.mainFrame
from gui.auxWindow import AuxiliaryDialog
from gui.contextMenu import ContextMenuSingle
from service.ammo import Ammo
from service.market import Market
class GraphFitAmmoPicker(ContextMenuSingle):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if srcContext != 'graphFitList':
return False
if mainItem is None or not mainItem.isFit:
return False
if callingWindow.graphFrame.getView().internalName != 'dmgStatsGraph':
return False
return True
def getText(self, callingWindow, itmContext, mainItem):
return 'Plot with Different Ammo...'
def activate(self, callingWindow, fullContext, mainItem, i):
AmmoPickerFrame.openOne(callingWindow, mainItem.item, forceReopen=True)
# GraphFitAmmoPicker.register()
class AmmoPickerFrame(AuxiliaryDialog):
def __init__(self, parent, fit):
super().__init__(parent, title='Choose Different Ammo', style=wx.DEFAULT_DIALOG_STYLE, resizeable=True)
padding = 5
mainSizer = wx.BoxSizer(wx.VERTICAL)
contents = AmmoPickerContents(self, fit)
mainSizer.Add(contents, 1, wx.EXPAND | wx.ALL, padding)
buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
if buttonSizer:
mainSizer.Add(buttonSizer, 0, wx.EXPAND | wx.ALL, padding)
self.SetSizer(mainSizer)
self.Layout()
contW, contH = contents.GetVirtualSize()
bestW = contW + padding * 2
bestH = contH + padding * 2
if buttonSizer:
# Yeah right... whatever
buttW, buttH = buttonSizer.GetSize()
bestW = max(bestW, buttW + padding * 2)
bestH += buttH + padding * 2
bestW = min(1000, bestW)
bestH = min(700, bestH)
self.SetSize(bestW, bestH)
self.SetMinSize(wx.Size(int(bestW * 0.7), int(bestH * 0.7)))
self.CenterOnParent()
self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent)
def kbEvent(self, event):
if event.GetKeyCode() == wx.WXK_ESCAPE and event.GetModifiers() == wx.MOD_NONE:
self.Close()
return
event.Skip()
class AmmoPickerContents(wx.ScrolledCanvas):
indent = 15
def __init__(self, parent, fit):
wx.ScrolledCanvas.__init__(self, parent)
self.SetScrollRate(0, 15)
mods = self.getMods(fit)
drones = self.getDrones(fit)
fighters = self.getFighters(fit)
self.rbLabelMap = {}
self.rbCheckboxMap = {}
mainSizer = wx.BoxSizer(wx.VERTICAL)
moduleSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(moduleSizer, 0, wx.ALL, 0)
self.droneSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(self.droneSizer, 0, wx.ALL, 0)
fighterSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(fighterSizer, 0, wx.ALL, 0)
firstRadio = True
for modInfo, modAmmo in mods:
text = '\n'.join('{}x {}'.format(amount, item.name) for item, amount in modInfo)
modRb = self.addRadioButton(moduleSizer, text, firstRadio)
firstRadio = False
# Get actual module, as ammo getters need it
mod = next((m for m in fit.modules if m.itemID == next(iter(modInfo))[0].ID), None)
_, ammoTree = Ammo.getInstance().getModuleStructuredAmmo(mod)
if len(ammoTree) == 1:
for ammoCatName, ammos in ammoTree.items():
for ammo in ammos:
self.addCheckbox(moduleSizer, ammo.name, modRb, indentLvl=1)
else:
for ammoCatName, ammos in ammoTree.items():
if len(ammos) == 1:
ammo = next(iter(ammos))
self.addCheckbox(moduleSizer, ammo.name, modRb, indentLvl=1)
else:
self.addLabel(moduleSizer, '{}:'.format(ammoCatName), modRb, indentLvl=1)
for ammo in ammos:
self.addCheckbox(moduleSizer, ammo.name, modRb, indentLvl=2)
if drones:
droneRb = self.addRadioButton(self.droneSizer, 'Drones', firstRadio)
from gui.builtinAdditionPanes.droneView import DroneView
for drone in sorted(drones, key=DroneView.droneKey):
self.addCheckbox(self.droneSizer, '{}x {}'.format(drone.amount, drone.item.name), droneRb, indentLvl=1)
addBtn = wx.Button(self, wx.ID_ANY, '+', style=wx.BU_EXACTFIT)
addBtn.Bind(wx.EVT_BUTTON, self.OnDroneGroupAdd)
mainSizer.Add(addBtn, 0, wx.LEFT, self.indent)
if fighters:
fighterRb = self.addRadioButton(fighterSizer, 'Fighters', firstRadio)
from gui.builtinAdditionPanes.fighterView import FighterDisplay
for fighter in sorted(fighters, key=FighterDisplay.fighterKey):
self.addCheckbox(fighterSizer, '{}x {}'.format(fighter.amount, fighter.item.name), fighterRb, indentLvl=1)
self.SetSizer(mainSizer)
self.refreshStatus()
def addRadioButton(self, sizer, text, firstRadio=False):
if firstRadio:
rb = wx.RadioButton(self, wx.ID_ANY, text, style=wx.RB_GROUP)
rb.SetValue(True)
else:
rb = wx.RadioButton(self, wx.ID_ANY, text)
rb.SetValue(False)
rb.Bind(wx.EVT_RADIOBUTTON, self.rbSelected)
sizer.Add(rb, 0, wx.EXPAND | wx.ALL, 0)
return rb
def addCheckbox(self, sizer, text, currentRb, indentLvl=0):
cb = wx.CheckBox(self, -1, text)
sizer.Add(cb, 0, wx.EXPAND | wx.LEFT, self.indent * indentLvl)
if currentRb is not None:
self.rbCheckboxMap.setdefault(currentRb, []).append(cb)
def addLabel(self, sizer, text, currentRb, indentLvl=0):
text = text[0].capitalize() + text[1:]
label = wx.StaticText(self, wx.ID_ANY, text)
sizer.Add(label, 0, wx.EXPAND | wx.LEFT, self.indent * indentLvl)
if currentRb is not None:
self.rbLabelMap.setdefault(currentRb, []).append(label)
def getMods(self, fit):
sMkt = Market.getInstance()
sAmmo = Ammo.getInstance()
loadableChargesCache = {}
# Modules, format: {frozenset(ammo): {item: count}}
modsPrelim = {}
if fit is not None:
for mod in fit.modules:
if not mod.canDealDamage():
continue
typeID = mod.item.ID
if typeID not in loadableChargesCache:
loadableChargesCache[typeID] = sAmmo.getModuleFlatAmmo(mod)
charges = loadableChargesCache[typeID]
# We're not interested in modules which contain no charges
if charges:
data = modsPrelim.setdefault(frozenset(charges), {})
if mod.item not in data:
data[mod.item] = 0
data[mod.item] += 1
# Format: [([(item, count), ...], frozenset(ammo)), ...]
modsFinal = []
for charges, itemCounts in modsPrelim.items():
modsFinal.append((
# Sort items within group
sorted(itemCounts.items(), key=lambda i: sMkt.itemSort(i[0], reverseMktGrp=True), reverse=True),
charges))
# Sort item groups
modsFinal.sort(key=lambda i: sMkt.itemSort(i[0][0][0], reverseMktGrp=True), reverse=True)
return modsFinal
def getDrones(self, fit):
drones = []
if fit is not None:
for drone in fit.drones:
if drone.item is None:
continue
# Drones are our "ammo", so we want to pick even those which are inactive
if drone.canDealDamage(ignoreState=True):
drones.append(drone)
continue
if {'remoteWebifierEntity', 'remoteTargetPaintEntity'}.intersection(drone.item.effects):
drones.append(drone)
continue
return drones
def getFighters(self, fit):
fighters = []
if fit is not None:
for fighter in fit.fighters:
if fighter.item is None:
continue
# Fighters are our "ammo" as well
if fighter.canDealDamage(ignoreState=True):
fighters.append(fighter)
continue
for ability in fighter.abilities:
if not ability.active:
continue
if ability.effect.name == 'fighterAbilityStasisWebifier':
fighters.append(fighter)
break
return fighters
def OnDroneGroupAdd(self, event):
event.Skip()
sizer = wx.BoxSizer(wx.HORIZONTAL)
label = wx.StaticText()
self.droneSizer.Add(sizer, 0, wx.EXPAND | wx.LEFT, self.indent)
def refreshStatus(self):
for map in (self.rbLabelMap, self.rbCheckboxMap):
for rb, items in map.items():
for item in items:
item.Enable(rb.GetValue())
def rbSelected(self, event):
event.Skip()
self.refreshStatus()

View File

@@ -1,52 +0,0 @@
# noinspection PyPackageRequirements
import wx
from gui.contextMenu import ContextMenuUnconditional
from service.implantSet import ImplantSets as s_ImplantSets
class AddImplantSet(ContextMenuUnconditional):
def display(self, callingWindow, srcContext):
sIS = s_ImplantSets.getInstance()
implantSets = sIS.getImplantSetList()
if len(implantSets) == 0:
return False
return srcContext in ("implantItemMisc", "implantEditor")
def getText(self, callingWindow, itmContext):
return "Add Implant Set"
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
m = wx.Menu()
bindmenu = rootMenu if "wxMSW" in wx.PlatformInfo else m
sIS = s_ImplantSets.getInstance()
implantSets = sIS.getImplantSetList()
self.context = context
self.callingWindow = callingWindow
self.idmap = {}
for set in sorted(implantSets, key=lambda i: i.name):
id = ContextMenuUnconditional.nextID()
mitem = wx.MenuItem(rootMenu, id, set.name)
bindmenu.Bind(wx.EVT_MENU, self.handleSelection, mitem)
self.idmap[id] = set
m.Append(mitem)
return m
def handleSelection(self, event):
impSet = self.idmap.get(event.Id, None)
if impSet is None:
event.Skip()
return
self.callingWindow.addImplantSet(impSet)
AddImplantSet.register()

View File

@@ -0,0 +1,108 @@
# noinspection PyPackageRequirements
import wx
from gui.contextMenu import ContextMenuUnconditional
from service.market import Market
from service.implantSet import ImplantSets as UserImplantSets
from service.precalcImplantSet import PrecalcedImplantSets
class ImplantSetApply(ContextMenuUnconditional):
def display(self, callingWindow, srcContext):
self.userImplantSets = UserImplantSets.getInstance().getImplantSetList()
self.structedImplantSets = PrecalcedImplantSets.getStructuredSets()
if len(self.userImplantSets) == 0 and len(self.structedImplantSets) == 0:
return False
return srcContext in ("implantItemMisc", "implantEditor")
def getText(self, callingWindow, context):
return "Apply Implant Set"
def _addSeparator(self, m, text):
id_ = ContextMenuUnconditional.nextID()
m.Append(id_, '%s' % text)
m.Enable(id_, False)
def _addSet(self, parentMenu, profile, name):
id = ContextMenuUnconditional.nextID()
self.eventSetMap[id] = profile
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleSelection, menuItem)
return menuItem
def _addCategory(self, parentMenu, name):
id = ContextMenuUnconditional.nextID()
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleSelection, menuItem)
return menuItem
def _gradeSorter(self, item):
order = ['low-grade', 'mid-grade', 'high-grade']
try:
pos = order.index(item.lower())
except IndexError:
pos = len(order)
return pos, item
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
msw = "wxMSW" in wx.PlatformInfo
menu_lvl1 = wx.Menu()
self.context = context
self.callingWindow = callingWindow
self.eventSetMap = {}
# Auto-generated sets
for setName in sorted(self.structedImplantSets):
setData = self.structedImplantSets[setName]
if len(setData) == 1:
for implantIDs in setData.values():
menuitem_lvl1 = self._addSet(rootMenu, implantIDs, setName)
menu_lvl1.Append(menuitem_lvl1)
else:
menuitem_lvl1 = self._addCategory(rootMenu, setName)
menu_lvl2 = wx.Menu()
for gradeName in sorted(setData, key=self._gradeSorter):
implantIDs = setData[gradeName]
menuitem_lvl2 = self._addSet(rootMenu if msw else menu_lvl1, implantIDs, gradeName)
menu_lvl2.Append(menuitem_lvl2)
menu_lvl2.Bind(wx.EVT_MENU, self.handleSelection)
menuitem_lvl1.SetSubMenu(menu_lvl2)
menu_lvl1.Append(menuitem_lvl1)
# Separator
if self.userImplantSets and self.structedImplantSets:
menu_lvl1.AppendSeparator()
# Saved sets
if self.userImplantSets:
menuitem_lvl1 = self._addCategory(rootMenu, 'Saved Sets')
menu_lvl2 = wx.Menu()
for implantSet in sorted(self.userImplantSets, key=lambda i: i.name):
menuitem_lvl2 = self._addSet(rootMenu if msw else menu_lvl1, implantSet, implantSet.name)
menu_lvl2.Append(menuitem_lvl2)
menu_lvl2.Bind(wx.EVT_MENU, self.handleSelection)
menuitem_lvl1.SetSubMenu(menu_lvl2)
menu_lvl1.Append(menuitem_lvl1)
menu_lvl1.Bind(wx.EVT_MENU, self.handleSelection)
return menu_lvl1
def handleSelection(self, event):
impSet = self.eventSetMap.get(event.Id, None)
if impSet is None:
event.Skip()
return
if isinstance(impSet, str):
implants = PrecalcedImplantSets.stringToImplants(impSet)
else:
implants = impSet.implants
self.callingWindow.addImplants(implants)
ImplantSetApply.register()

View File

@@ -0,0 +1,77 @@
import wx
import gui.mainFrame
from gui.contextMenu import ContextMenuUnconditional
from service.fit import Fit
class ImplantSetSave(ContextMenuUnconditional):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext):
if srcContext not in ('implantItemMisc', 'implantItemMiscChar'):
return False
fit = Fit.getInstance().getFit(self.mainFrame.getActiveFit())
self.implants = fit.appliedImplants[:]
if not self.implants:
return False
return True
def getText(self, callingWindow, context):
return 'Save as New Implant Set'
def activate(self, callingWindow, fullContext, i):
with NameDialog(self.mainFrame, '') as dlg:
if dlg.ShowModal() == wx.ID_OK:
name = dlg.input.GetLineText(0).strip()
if name == '':
return
from gui.setEditor import ImplantSetEditor
ImplantSetEditor.openOne(parent=self.mainFrame, dataToAdd=(name, self.implants))
ImplantSetSave.register()
class NameDialog(wx.Dialog):
def __init__(self, parent, value):
super().__init__(parent, title='New Implant Set', style=wx.DEFAULT_DIALOG_STYLE)
self.SetMinSize((346, 156))
bSizer1 = wx.BoxSizer(wx.VERTICAL)
bSizer2 = wx.BoxSizer(wx.VERTICAL)
text = wx.StaticText(self, wx.ID_ANY, 'Enter a name for your new Implant Set:')
bSizer2.Add(text, 0)
bSizer1.Add(bSizer2, 0, wx.ALL, 10)
self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
if value is None:
value = ''
else:
value = str(value)
self.input.SetValue(value)
self.input.SelectAll()
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
bSizer3 = wx.BoxSizer(wx.VERTICAL)
bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 15)
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
self.input.SetFocus()
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1)
self.CenterOnParent()
self.Fit()
def processEnter(self, evt):
self.EndModal(wx.ID_OK)

View File

@@ -1,19 +1,16 @@
import gui.fitCommands as cmd
import gui.mainFrame
from gui.contextMenu import ContextMenuSingle
from service.settings import ContextMenuSettings
class FillWithItem(ContextMenuSingle):
visibilitySetting = 'moduleFill'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if not self.settings.get('moduleFill'):
return False
if srcContext not in ('marketItemGroup', 'marketItemMisc'):
return False

View File

@@ -2,19 +2,16 @@ import gui.fitCommands as cmd
import gui.mainFrame
from gui.contextMenu import ContextMenuSingle
from service.fit import Fit
from service.settings import ContextMenuSettings
class ProjectItem(ContextMenuSingle):
visibilitySetting = 'project'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if not self.settings.get('project'):
return False
if srcContext not in ("marketItemGroup", "marketItemMisc") or self.mainFrame.getActiveFit() is None:
return False

View File

@@ -7,19 +7,16 @@ from gui.contextMenu import ContextMenuCombined
from gui.fitCommands.helpers import getSimilarModPositions, getSimilarFighters
from service.fit import Fit
from service.market import Market
from service.settings import ContextMenuSettings
class ChangeItemToVariation(ContextMenuCombined):
visibilitySetting = 'metaSwap'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
def display(self, callingWindow, srcContext, mainItem, selection):
if not self.settings.get('metaSwap'):
return False
if self.mainFrame.getActiveFit() is None or srcContext not in (
'fittingModule',
'droneItem',
@@ -59,8 +56,11 @@ class ChangeItemToVariation(ContextMenuCombined):
return x.metaLevel or 0
def get_metagroup(x):
# We want deadspace before officer mods
remap = {5: 6, 6: 5}
remap = {
# We want deadspace before officer mods
5: 6, 6: 5,
# For structures we want t1-t2-faction
54: 52, 52: 54}
metaGroup = sMkt.getMetaGroupByItem(x)
return remap.get(metaGroup.ID, metaGroup.ID) if metaGroup is not None else 0

View File

@@ -3,33 +3,28 @@ import wx
import gui.fitCommands as cmd
import gui.mainFrame
from eos.const import FittingHardpoint
from eos.saveddata.module import Module
from gui.bitmap_loader import BitmapLoader
from gui.contextMenu import ContextMenuCombined
from gui.fitCommands.helpers import getSimilarModPositions
from service.ammo import Ammo
from service.fit import Fit
from service.market import Market
class ChangeModuleAmmo(ContextMenuCombined):
DAMAGE_TYPES = ("em", "explosive", "kinetic", "thermal")
MISSILE_ORDER = ("em", "thermal", "kinetic", "explosive", "mixed")
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
# Format: {type ID: set(loadable, charges)}
self.loadableCharges = {}
self.loadableChargesCache = {}
def display(self, callingWindow, srcContext, mainItem, selection):
if srcContext not in ("fittingModule", "projectedModule"):
if srcContext not in ('fittingModule', 'projectedModule'):
return False
if self.mainFrame.getActiveFit() is None:
return False
self.mainCharges = self.getChargesForMod(mainItem)
self.mainCharges = self._getAmmo(mainItem)
if not self.mainCharges:
return False
@@ -39,186 +34,81 @@ class ChangeModuleAmmo(ContextMenuCombined):
return True
def getText(self, callingWindow, itmContext, mainItem, selection):
return "Charge"
return 'Charge'
def getChargesForMod(self, mod):
sMkt = Market.getInstance()
if mod is None or mod.isEmpty:
def _getAmmo(self, mod):
if mod.itemID is None:
return set()
typeID = mod.item.ID
if typeID in self.loadableCharges:
return self.loadableCharges[typeID]
chargeSet = self.loadableCharges.setdefault(typeID, set())
# Do not try to grab it for modes which can also be passed as part of selection
if isinstance(mod, Module):
for charge in mod.getValidCharges():
if sMkt.getPublicityByItem(charge):
chargeSet.add(charge)
return chargeSet
if mod.itemID not in self.loadableChargesCache:
self.loadableChargesCache[mod.itemID] = Ammo.getInstance().getModuleFlatAmmo(mod)
return self.loadableChargesCache[mod.itemID]
def turretSorter(self, charge):
damage = 0
range_ = (self.module.item.getAttribute("maxRange")) * \
(charge.getAttribute("weaponRangeMultiplier") or 1)
falloff = (self.module.item.getAttribute("falloff") or 0) * \
(charge.getAttribute("fallofMultiplier") or 1)
for type_ in self.DAMAGE_TYPES:
d = charge.getAttribute("%sDamage" % type_)
if d > 0:
damage += d
# Take optimal and falloff as range factor
rangeFactor = range_ + falloff
return - rangeFactor, charge.name.rsplit()[-2:], damage, charge.name
def missileSorter(self, charge):
# Get charge damage type and total damage
chargeDamageType, totalDamage = self.damageInfo(charge)
# Find its position in sort list
position = self.MISSILE_ORDER.index(chargeDamageType)
return position, totalDamage, charge.name
def damageInfo(self, charge):
# Set up data storage for missile damage stuff
damageMap = {}
totalDamage = 0
# Fill them with the data about charge
for damageType in self.DAMAGE_TYPES:
currentDamage = charge.getAttribute("{0}Damage".format(damageType)) or 0
damageMap[damageType] = currentDamage
totalDamage += currentDamage
# Detect type of ammo
chargeDamageType = None
for damageType in damageMap:
# If all damage belongs to certain type purely, set appropriate
# ammoType
if damageMap[damageType] == totalDamage:
chargeDamageType = damageType
break
# Else consider ammo as mixed damage
if chargeDamageType is None:
chargeDamageType = "mixed"
return chargeDamageType, totalDamage
@staticmethod
def numericConverter(string):
return int(string) if string.isdigit() else string
def nameSorter(self, charge):
parts = charge.name.split(" ")
return list(map(self.numericConverter, parts))
def addCharge(self, menu, charge):
def _addCharge(self, menu, charge):
id_ = ContextMenuCombined.nextID()
name = charge.name if charge is not None else "Empty"
self.chargeIds[id_] = charge
name = charge.name if charge is not None else 'Empty'
self.chargeEventMap[id_] = charge
item = wx.MenuItem(menu, id_, name)
menu.Bind(wx.EVT_MENU, self.handleAmmoSwitch, item)
item.charge = charge
if charge is not None and charge.iconID is not None:
bitmap = BitmapLoader.getBitmap(charge.iconID, "icons")
bitmap = BitmapLoader.getBitmap(charge.iconID, 'icons')
if bitmap is not None:
item.SetBitmap(bitmap)
return item
@staticmethod
def addSeperator(m, text):
def _addSeparator(m, text):
id_ = ContextMenuCombined.nextID()
m.Append(id_, '%s' % text)
m.Enable(id_, False)
def getSubMenu(self, callingWindow, context, mainItem, selection, rootMenu, i, pitem):
msw = True if "wxMSW" in wx.PlatformInfo else False
m = wx.Menu()
self.chargeIds = {}
hardpoint = self.module.hardpoint
moduleName = self.module.item.name
# Make sure we do not consider mining turrets as combat turrets
if hardpoint == FittingHardpoint.TURRET and self.module.getModifiedItemAttr("miningAmount", None) is None:
self.addSeperator(m, "Long Range")
items = []
range_ = None
nameBase = None
sub = None
chargesSorted = sorted(self.mainCharges, key=self.turretSorter)
for charge in chargesSorted:
if "civilian" in charge.name.lower():
continue
currBase = charge.name.rsplit()[-2:]
currRange = charge.getAttribute("weaponRangeMultiplier")
if nameBase is None or range_ != currRange or nameBase != currBase:
if sub is not None:
self.addSeperator(sub, "More Damage")
sub = None
base = charge
nameBase = currBase
range_ = currRange
item = self.addCharge(rootMenu if msw else m, charge)
items.append(item)
msw = True if 'wxMSW' in wx.PlatformInfo else False
menu = wx.Menu()
self.chargeEventMap = {}
modType, chargeDict = Ammo.getInstance().getModuleStructuredAmmo(self.module, ammo=self.mainCharges)
if modType == 'ddTurret':
self._addSeparator(menu, 'Long Range')
menuItems = []
for charges in chargeDict.values():
if len(charges) == 1:
menuItems.append(self._addCharge(rootMenu if msw else menu, charges[0]))
else:
if sub is None and item and base:
sub = wx.Menu()
sub.Bind(wx.EVT_MENU, self.handleAmmoSwitch)
self.addSeperator(sub, "Less Damage")
item.SetSubMenu(sub)
sub.Append(self.addCharge(rootMenu if msw else sub, base))
sub.Append(self.addCharge(rootMenu if msw else sub, charge))
if sub is not None:
self.addSeperator(sub, "More Damage")
for item in items:
m.Append(item)
self.addSeperator(m, "Short Range")
elif hardpoint == FittingHardpoint.MISSILE and moduleName != 'Festival Launcher':
type_ = None
sub = None
defender = None
chargesSorted = sorted(self.mainCharges, key=self.missileSorter)
for charge in chargesSorted:
currType = self.damageInfo(charge)[0]
if currType != type_ or type_ is None:
if sub is not None:
self.addSeperator(sub, "More Damage")
type_ = currType
item = wx.MenuItem(m, wx.ID_ANY, type_.capitalize())
bitmap = BitmapLoader.getBitmap("%s_small" % type_, "gui")
if bitmap is not None:
item.SetBitmap(bitmap)
sub = wx.Menu()
sub.Bind(wx.EVT_MENU, self.handleAmmoSwitch)
self.addSeperator(sub, "Less Damage")
item.SetSubMenu(sub)
m.Append(item)
if charge.name not in ("Light Defender Missile I", "Heavy Defender Missile I"):
sub.Append(self.addCharge(rootMenu if msw else sub, charge))
else:
defender = charge
if defender is not None:
m.Append(self.addCharge(rootMenu if msw else m, defender))
if sub is not None:
self.addSeperator(sub, "More Damage")
else:
chargesSorted = sorted(self.mainCharges, key=self.nameSorter)
for charge in chargesSorted:
m.Append(self.addCharge(rootMenu if msw else m, charge))
m.Append(self.addCharge(rootMenu if msw else m, None))
return m
baseCharge = charges[0]
menuItem = self._addCharge(rootMenu if msw else menu, baseCharge)
menuItems.append(menuItem)
subMenu = wx.Menu()
subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch)
menuItem.SetSubMenu(subMenu)
self._addSeparator(subMenu, 'Less Damage')
for charge in charges:
subMenu.Append(self._addCharge(rootMenu if msw else subMenu, charge))
self._addSeparator(subMenu, 'More Damage')
for menuItem in menuItems:
menu.Append(menuItem)
self._addSeparator(menu, 'Short Range')
elif modType == 'ddMissile':
menuItems = []
for chargeCatName, charges in chargeDict.items():
menuItem = wx.MenuItem(menu, wx.ID_ANY, chargeCatName.capitalize())
menuItems.append(menuItem)
subMenu = wx.Menu()
subMenu.Bind(wx.EVT_MENU, self.handleAmmoSwitch)
menuItem.SetSubMenu(subMenu)
self._addSeparator(subMenu, 'Less Damage')
for charge in charges:
subMenu.Append(self._addCharge(rootMenu if msw else subMenu, charge))
self._addSeparator(subMenu, 'More Damage')
for menuItem in menuItems:
menu.Append(menuItem)
elif modType == 'general':
for charge in chargeDict['general']:
menu.Append(self._addCharge(rootMenu if msw else menu, charge))
menu.Append(self._addCharge(rootMenu if msw else menu, None))
return menu
def handleAmmoSwitch(self, event):
charge = self.chargeIds.get(event.Id, False)
charge = self.chargeEventMap.get(event.Id, False)
if charge is False:
event.Skip()
return
@@ -254,7 +144,7 @@ class ChangeModuleAmmo(ContextMenuCombined):
positions = []
for position, mod in enumerate(modContainer):
if mod in self.selection:
modCharges = self.getChargesForMod(mod)
modCharges = self._getAmmo(mod)
if modCharges.issubset(self.mainCharges):
positions.append(position)
self.mainFrame.command.Submit(command(

View File

@@ -2,20 +2,16 @@ import gui.fitCommands as cmd
import gui.mainFrame
from gui.contextMenu import ContextMenuSingle
from service.fit import Fit
from service.settings import ContextMenuSettings
class FillWithModule(ContextMenuSingle):
visibilitySetting = 'moduleFill'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if not self.settings.get('moduleFill'):
return False
if mainItem is None or getattr(mainItem, 'isEmpty', False):
return False

View File

@@ -9,20 +9,17 @@ import gui.mainFrame
from eos.utils.spoolSupport import SpoolType, SpoolOptions
from gui.contextMenu import ContextMenuSingle
from service.fit import Fit
from service.settings import ContextMenuSettings
class ChangeModuleSpool(ContextMenuSingle):
visibilitySetting = 'spoolup'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
self.resetId = None
def display(self, callingWindow, srcContext, mainItem):
if not self.settings.get('spoolup'):
return False
if srcContext not in ('fittingModule', 'projectedModule') or self.mainFrame.getActiveFit() is None:
return False

View File

@@ -8,19 +8,16 @@ from gui.bitmap_loader import BitmapLoader
from gui.contextMenu import ContextMenuSingle
from service.character import Character
from service.fit import Fit
from service.settings import ContextMenuSettings
class ChangeAffectingSkills(ContextMenuSingle):
visibilitySetting = 'changeAffectingSkills'
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
def display(self, callingWindow, srcContext, mainItem):
if not self.settings.get('changeAffectingSkills'):
return False
if srcContext not in (
"fittingModule", "fittingCharge",
"fittingShip", "droneItem",

View File

@@ -7,6 +7,7 @@ import wx
import gui.mainFrame
from eos.saveddata.targetProfile import TargetProfile
from gui.contextMenu import ContextMenuUnconditional
from gui.utils.sorter import smartSort
from service.targetProfile import TargetProfile as svc_TargetProfile
@@ -18,74 +19,68 @@ class TargetProfileAdder(ContextMenuUnconditional):
def display(self, callingWindow, srcContext):
if srcContext != 'graphTgtList':
return False
sTR = svc_TargetProfile.getInstance()
self.callingWindow = callingWindow
self.profiles = sTR.getTargetProfileList()
self.profiles.sort(key=lambda p: (p.name in ['None'], p.name))
return len(self.profiles) > 0
# We always show "Ideal Profile" anyway
return True
def getText(self, callingWindow, itmContext):
return 'Add Target Profile'
def handleProfileAdd(self, event):
profile = self.profileIds.get(event.Id, False)
profile = self.eventProfileMap.get(event.Id, False)
if profile is False:
event.Skip()
return
self.callingWindow.addProfile(profile)
def addProfile(self, rootMenu, profile):
def _addProfile(self, parentMenu, profile, name):
id = ContextMenuUnconditional.nextID()
name = getattr(profile, '_name', profile.name)
self.eventProfileMap[id] = profile
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleProfileAdd, menuItem)
return menuItem
self.profileIds[id] = profile
item = wx.MenuItem(rootMenu, id, name)
rootMenu.Bind(wx.EVT_MENU, self.handleProfileAdd, item)
return item
def _addCategory(self, parentMenu, name):
id = ContextMenuUnconditional.nextID()
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleProfileAdd, menuItem)
return menuItem
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
self.profileIds = {}
self.subMenus = OrderedDict()
self.singles = []
self.callingWindow = callingWindow
sTR = svc_TargetProfile.getInstance()
profiles = list(chain(sTR.getBuiltinTargetProfileList(), sTR.getUserTargetProfileList()))
profiles.sort(key=lambda p: smartSort(p.fullName))
sub = wx.Menu()
for profile in chain([TargetProfile.getIdeal()], self.profiles):
start, end = profile.name.find('['), profile.name.find(']')
if start is not -1 and end is not -1:
currBase = profile.name[start + 1:end]
# set helper attr
setattr(profile, '_name', profile.name[end + 1:].strip())
if currBase not in self.subMenus:
self.subMenus[currBase] = []
self.subMenus[currBase].append(profile)
else:
self.singles.append(profile)
self.eventProfileMap = {}
items = (OrderedDict(), OrderedDict())
for profile in profiles:
container = items
for categoryName in profile.hierarchy:
container = container[1].setdefault(categoryName, (OrderedDict(), OrderedDict()))
container[0][profile.shortName] = profile
# Single items, no parent
msw = 'wxMSW' in wx.PlatformInfo
for profile in self.singles:
sub.Append(self.addProfile(rootMenu if msw else sub, profile))
# Category as menu item - expands further
msw = "wxMSW" in wx.PlatformInfo
# Items that have a parent
for menuName, profiles in list(self.subMenus.items()):
# Create parent item for root menu that is simply name of parent
item = wx.MenuItem(rootMenu, ContextMenuUnconditional.nextID(), menuName)
def makeMenu(container, parentMenu, first=False):
menu = wx.Menu()
if first:
idealProfile = TargetProfile.getIdeal()
mitem = self._addProfile(rootMenu if msw else parentMenu, idealProfile, idealProfile.fullName)
menu.Append(mitem)
for name, pattern in container[0].items():
menuItem = self._addProfile(rootMenu if msw else parentMenu, pattern, name)
menu.Append(menuItem)
for name, subcontainer in container[1].items():
menuItem = self._addCategory(rootMenu if msw else parentMenu, name)
subMenu = makeMenu(subcontainer, menu)
menuItem.SetSubMenu(subMenu)
menu.Append(menuItem)
menu.Bind(wx.EVT_MENU, self.handleProfileAdd)
return menu
# Create menu for child items
grandSub = wx.Menu()
# Apply child menu to parent item
item.SetSubMenu(grandSub)
# Append child items to child menu
for profile in profiles:
grandSub.Append(self.addProfile(rootMenu if msw else grandSub, profile))
sub.Append(item) # finally, append parent item to root menu
return sub
subMenu = makeMenu(items, rootMenu, first=True)
return subMenu
TargetProfileAdder.register()

View File

@@ -1,5 +1,4 @@
import gui.mainFrame
from eos.saveddata.targetProfile import TargetProfile
from graphs.wrapper import TargetWrapper
from gui.contextMenu import ContextMenuSingle
from gui.targetProfileEditor import TargetProfileEditor
@@ -17,7 +16,7 @@ class TargetProfileEditorMenu(ContextMenuSingle):
return False
if not mainItem.isProfile:
return False
if mainItem.item is TargetProfile.getIdeal():
if mainItem.item.builtin:
return False
return True

View File

@@ -1,4 +1,5 @@
from collections import OrderedDict
from itertools import chain
# noinspection PyPackageRequirements
import wx
@@ -6,6 +7,7 @@ import wx
import gui.globalEvents as GE
import gui.mainFrame
from gui.contextMenu import ContextMenuUnconditional
from gui.utils.sorter import smartSort
from service.fit import Fit
from service.targetProfile import TargetProfile as svc_TargetProfile
@@ -16,21 +18,19 @@ class TargetProfileSwitcher(ContextMenuUnconditional):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext):
if self.mainFrame.getActiveFit() is None or srcContext != 'firepowerViewFull':
if srcContext != 'firepowerViewFull':
return False
sTR = svc_TargetProfile.getInstance()
self.profiles = sTR.getTargetProfileList()
self.profiles.sort(key=lambda p: (p.name in ['None'], p.name))
return len(self.profiles) > 0
if self.mainFrame.getActiveFit() is None:
return False
# We always show "No Profile" anyway
return True
def getText(self, callingWindow, itmContext):
# We take into consideration just target resists, so call menu item accordingly
return 'Target Resists'
def handleResistSwitch(self, event):
profile = self.profileIds.get(event.Id, False)
profile = self.profileEventMap.get(event.Id, False)
if profile is False:
event.Skip()
return
@@ -40,77 +40,62 @@ class TargetProfileSwitcher(ContextMenuUnconditional):
sFit.setTargetProfile(fitID, profile)
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,)))
def addProfile(self, rootMenu, profile):
def _addProfile(self, parentMenu, profile, name):
id = ContextMenuUnconditional.nextID()
name = getattr(profile, '_name', profile.name) if profile is not None else 'No Profile'
self.profileIds[id] = profile
item = wx.MenuItem(rootMenu, id, name, kind=wx.ITEM_CHECK)
rootMenu.Bind(wx.EVT_MENU, self.handleResistSwitch, item)
self.profileEventMap[id] = profile
menuItem = wx.MenuItem(parentMenu, id, name, kind=wx.ITEM_CHECK)
parentMenu.Bind(wx.EVT_MENU, self.handleResistSwitch, menuItem)
# determine active profile
sFit = Fit.getInstance()
fitID = self.mainFrame.getActiveFit()
f = sFit.getFit(fitID)
tr = f.targetProfile
checked = sFit.getFit(fitID).targetProfile is profile
return menuItem, checked
checked = tr == profile
return item, checked
def _addCategory(self, parentMenu, name):
id = ContextMenuUnconditional.nextID()
menuItem = wx.MenuItem(parentMenu, id, name)
parentMenu.Bind(wx.EVT_MENU, self.handleResistSwitch, menuItem)
return menuItem
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
self.profileIds = {}
self.subMenus = OrderedDict()
self.singles = []
sTR = svc_TargetProfile.getInstance()
profiles = list(chain(sTR.getBuiltinTargetProfileList(), sTR.getUserTargetProfileList()))
profiles.sort(key=lambda p: smartSort(p.fullName))
sub = wx.Menu()
for profile in self.profiles:
start, end = profile.name.find('['), profile.name.find(']')
if start is not -1 and end is not -1:
currBase = profile.name[start + 1:end]
name = profile.name[end + 1:].strip()
if not name:
self.singles.append(profile)
continue
# set helper attr
setattr(profile, '_name', name)
if currBase not in self.subMenus:
self.subMenus[currBase] = []
self.subMenus[currBase].append(profile)
else:
self.singles.append(profile)
# Add reset
msw = 'wxMSW' in wx.PlatformInfo
mitem, checked = self.addProfile(rootMenu if msw else sub, None)
sub.Append(mitem)
mitem.Check(checked)
sub.AppendSeparator()
self.profileEventMap = {}
items = (OrderedDict(), OrderedDict())
for profile in profiles:
container = items
for categoryName in profile.hierarchy:
container = container[1].setdefault(categoryName, (OrderedDict(), OrderedDict()))
container[0][profile.shortName] = profile
# Single items, no parent
for profile in self.singles:
mitem, checked = self.addProfile(rootMenu if msw else sub, profile)
sub.Append(mitem)
mitem.Check(checked)
# Category as menu item - expands further
msw = "wxMSW" in wx.PlatformInfo
# Items that have a parent
for menuName, profiles in list(self.subMenus.items()):
# Create parent item for root menu that is simply name of parent
item = wx.MenuItem(rootMenu, ContextMenuUnconditional.nextID(), menuName)
# Create menu for child items
grandSub = wx.Menu()
# Apply child menu to parent item
item.SetSubMenu(grandSub)
# Append child items to child menu
for profile in profiles:
mitem, checked = self.addProfile(rootMenu if msw else grandSub, profile)
grandSub.Append(mitem)
def makeMenu(container, parentMenu, first=False):
menu = wx.Menu()
if first:
mitem, checked = self._addProfile(rootMenu if msw else parentMenu, None, 'No Profile')
menu.Append(mitem)
mitem.Check(checked)
sub.Append(item) # finally, append parent item to root menu
if len(container[0]) > 0 or len(container[1]) > 0:
menu.AppendSeparator()
for name, pattern in container[0].items():
menuItem, checked = self._addProfile(rootMenu if msw else parentMenu, pattern, name)
menu.Append(menuItem)
menuItem.Check(checked)
for name, subcontainer in container[1].items():
menuItem = self._addCategory(rootMenu if msw else parentMenu, name)
subMenu = makeMenu(subcontainer, menu)
menuItem.SetSubMenu(subMenu)
menu.Append(menuItem)
menu.Bind(wx.EVT_MENU, self.handleResistSwitch)
return menu
return sub
subMenu = makeMenu(items, rootMenu, first=True)
return subMenu
TargetProfileSwitcher.register()

View File

@@ -10,12 +10,13 @@ _ValueChanged, EVT_VALUE_CHANGED = wx.lib.newevent.NewEvent()
class AttributeSliderChangeEvent:
def __init__(self, obj, old_value, new_value, old_percentage, new_percentage):
def __init__(self, obj, old_value, new_value, old_percentage, new_percentage, affect_modified_flag=True):
self.__obj = obj
self.__old = old_value
self.__new = new_value
self.__old_percent = old_percentage
self.__new_percent = new_percentage
self.__affect_modified_flag = affect_modified_flag
def GetObj(self):
return self.__obj
@@ -32,6 +33,10 @@ class AttributeSliderChangeEvent:
def GetPercentage(self):
return self.__new_percent
@property
def AffectsModifiedFlag(self):
return self.__affect_modified_flag
Object = property(GetObj)
OldValue = property(GetOldValue)
Value = property(GetValue)
@@ -40,9 +45,9 @@ class AttributeSliderChangeEvent:
class ValueChanged(_ValueChanged, AttributeSliderChangeEvent):
def __init__(self, obj, old_value, new_value, old_percentage, new_percentage):
def __init__(self, obj, old_value, new_value, old_percentage, new_percentage, affect_modified_flag=True):
_ValueChanged.__init__(self)
AttributeSliderChangeEvent.__init__(self, obj, old_value, new_value, old_percentage, new_percentage)
AttributeSliderChangeEvent.__init__(self, obj, old_value, new_value, old_percentage, new_percentage, affect_modified_flag=affect_modified_flag)
class AttributeSlider(wx.Panel):
@@ -118,16 +123,19 @@ class AttributeSlider(wx.Panel):
self.SetValue(self.GetValue())
evt.Skip()
def SetValue(self, value, post_event=True):
def SetValue(self, value, post_event=True, affect_modified_flag=True):
self.ctrl.SetValue(value)
invert_factor = -1 if self.inverse else 1
if value >= self.base_value:
slider_percentage = (value - self.base_value) / (self.UserMaxValue - self.base_value) * 100 * invert_factor
else:
slider_percentage = (value - self.base_value) / (self.base_value - self.UserMinValue) * 100 * invert_factor
try:
if value >= self.base_value:
slider_percentage = (value - self.base_value) / (self.UserMaxValue - self.base_value) * 100 * invert_factor
else:
slider_percentage = (value - self.base_value) / (self.base_value - self.UserMinValue) * 100 * invert_factor
except ZeroDivisionError:
slider_percentage = 0
self.slider.SetValue(slider_percentage)
if post_event:
wx.PostEvent(self, ValueChanged(self, None, value, None, slider_percentage))
wx.PostEvent(self, ValueChanged(self, None, value, None, slider_percentage, affect_modified_flag=affect_modified_flag))
def OnMouseWheel(self, evt):
if evt.GetWheelRotation() > 0 and evt.GetWheelAxis() == wx.MOUSE_WHEEL_VERTICAL:

View File

@@ -112,6 +112,8 @@ class ItemAffectedBy(wx.Panel):
else:
try:
self.affectedBy.CollapseAll()
except (KeyboardInterrupt, SystemExit):
raise
except:
pass
@@ -171,7 +173,7 @@ class ItemAffectedBy(wx.Panel):
def sortAttrDisplayName(self, attr):
info = self.stuff.item.attributes.get(attr)
if info and info.displayName != "":
if info and info.displayName:
return info.displayName
return attr
@@ -251,7 +253,7 @@ class ItemAffectedBy(wx.Panel):
for attrName in attrOrder:
attrInfo = self.stuff.item.attributes.get(attrName)
displayName = attrInfo.displayName if attrInfo and attrInfo.displayName != "" else attrName
displayName = attrInfo.displayName if attrInfo and attrInfo.displayName else attrName
if attrInfo:
if attrInfo.iconID is not None:
@@ -444,7 +446,7 @@ class ItemAffectedBy(wx.Panel):
attrModifier = "-"
attrAmount = -attrAmount
attributes.append((attrName, (displayName if displayName != "" else attrName), attrModifier,
attributes.append((attrName, (displayName if displayName else attrName), attrModifier,
attrAmount, penalized, attrIcon))
attrSorted = sorted(attributes, key=lambda attribName: attribName[0])
@@ -454,14 +456,14 @@ class ItemAffectedBy(wx.Panel):
if self.showRealNames:
display = "%s %s %.2f %s" % (attrName, attrModifier, attrAmount, penalized)
saved = "%s %s %.2f %s" % (
displayName if displayName != "" else attrName,
displayName if displayName else attrName,
attrModifier,
attrAmount,
penalized
)
else:
display = "%s %s %.2f %s" % (
displayName if displayName != "" else attrName,
displayName if displayName else attrName,
attrModifier,
attrAmount,
penalized

View File

@@ -35,7 +35,7 @@ class ItemDescription(wx.Panel):
self.Layout()
self.description.Bind(wx.EVT_CONTEXT_MENU, self.onPopupMenu)
self.description.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)
self.description.Bind(wx.EVT_KEY_UP, self.onKeyUp)
self.popupMenu = wx.Menu()
copyItem = wx.MenuItem(self.popupMenu, 1, 'Copy')
@@ -50,7 +50,7 @@ class ItemDescription(wx.Panel):
if selectedMenuItem == 1: # Copy was chosen
self.copySelectionToClipboard()
def onKeyDown(self, event):
def onKeyUp(self, event):
keyCode = event.GetKeyCode()
# Ctrl + C
if keyCode == 67 and event.ControlDown():

View File

@@ -76,6 +76,7 @@ class ItemMutatorList(wx.ScrolledWindow):
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
self.mod = mod
self.timer = None
self.isModified = False
goodColor = wx.Colour(96, 191, 0)
badColor = wx.Colour(255, 64, 0)
@@ -92,6 +93,8 @@ class ItemMutatorList(wx.ScrolledWindow):
first = True
for m in sorted(mod.mutators.values(), key=lambda x: x.attribute.displayName):
if m.baseValue == 0:
continue
if not first:
sizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.ALL | wx.EXPAND, 5)
first = False
@@ -171,6 +174,8 @@ class ItemMutatorList(wx.ScrolledWindow):
self.SetSizer(sizer)
def changeMutatedValue(self, evt):
if evt.AffectsModifiedFlag:
self.isModified = True
m = self.event_mapping[evt.Object]
value = evt.Value
value = m.attribute.unit.ComplicateValue(value)
@@ -188,29 +193,32 @@ class ItemMutatorList(wx.ScrolledWindow):
self.timer = wx.CallLater(1000, self.callLater)
def resetMutatedValues(self, evt):
self.isModified = True
sFit = Fit.getInstance()
for slider, m in self.event_mapping.items():
value = sFit.changeMutatedValuePrelim(m, m.baseValue)
value = m.attribute.unit.SimplifyValue(value)
slider.SetValue(value)
slider.SetValue(value, affect_modified_flag=False)
evt.Skip()
def randomMutatedValues(self, evt):
self.isModified = True
sFit = Fit.getInstance()
for slider, m in self.event_mapping.items():
value = random.uniform(m.minValue, m.maxValue)
value = sFit.changeMutatedValuePrelim(m, value)
value = m.attribute.unit.SimplifyValue(value)
slider.SetValue(value)
slider.SetValue(value, affect_modified_flag=False)
evt.Skip()
def revertChanges(self, evt):
self.isModified = False
sFit = Fit.getInstance()
for slider, m in self.event_mapping.items():
if m.attrID in self.initialMutations:
value = sFit.changeMutatedValuePrelim(m, self.initialMutations[m.attrID])
value = m.attribute.unit.SimplifyValue(value)
slider.SetValue(value)
slider.SetValue(value, affect_modified_flag=False)
evt.Skip()
def OnWindowClose(self):
@@ -218,15 +226,18 @@ class ItemMutatorList(wx.ScrolledWindow):
sFit = Fit.getInstance()
fit = sFit.getFit(self.carryingFitID)
if self.mod in fit.modules:
currentMutation = {}
for slider, m in self.event_mapping.items():
# Sliders may have more up-to-date info than mutator in case we changed
# value in slider and without confirming it, decided to close window
value = slider.GetValue()
value = m.attribute.unit.ComplicateValue(value)
if value != m.value:
value = sFit.changeMutatedValuePrelim(m, value)
currentMutation[m.attrID] = value
if self.isModified:
currentMutation = {}
for slider, m in self.event_mapping.items():
# Sliders may have more up-to-date info than mutator in case we changed
# value in slider and without confirming it, decided to close window
value = slider.GetValue()
value = m.attribute.unit.ComplicateValue(value)
if value != m.value:
value = sFit.changeMutatedValuePrelim(m, value)
currentMutation[m.attrID] = value
else:
currentMutation = self.initialMutations
mainFrame = gui.mainFrame.MainFrame.getInstance()
mainFrame.getCommandForFit(self.carryingFitID).Submit(cmd.GuiChangeLocalModuleMutationCommand(
fitID=self.carryingFitID,

View File

@@ -86,6 +86,8 @@ class ItemProperties(wx.Panel):
valueUnit = str(value)
self.paramList.SetItem(index, 1, valueUnit)
except (KeyboardInterrupt, SystemExit):
raise
except:
# TODO: Add logging to this.
# We couldn't get a property for some reason. Skip it for now.

View File

@@ -14,7 +14,7 @@ class ItemTraits(wx.Panel):
self.traits.SetPage(item.traits.traitText)
self.traits.Bind(wx.EVT_CONTEXT_MENU, self.onPopupMenu)
self.traits.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)
self.traits.Bind(wx.EVT_KEY_UP, self.onKeyUp)
mainSizer.Add(self.traits, 1, wx.ALL | wx.EXPAND, 0)
self.Layout()
@@ -32,7 +32,7 @@ class ItemTraits(wx.Panel):
if selectedMenuItem == 1: # Copy was chosen
self.copySelectionToClipboard()
def onKeyDown(self, event):
def onKeyUp(self, event):
keyCode = event.GetKeyCode()
# Ctrl + C
if keyCode == 67 and event.ControlDown():

View File

@@ -1,15 +1,16 @@
import wx
from logbook import Logger
from eos.saveddata.module import Module
import gui.builtinMarketBrowser.pfSearchBox as SBox
from config import slotColourMap
from eos.saveddata.module import Module
from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES
from gui.contextMenu import ContextMenu
from gui.display import Display
from gui.utils.staticHelpers import DragDropHelper
from service.attribute import Attribute
from service.fit import Fit
from config import slotColourMap
from service.market import Market
pyfalog = Logger(__name__)
@@ -49,8 +50,6 @@ class ItemView(Display):
self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.itemActivated)
self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag)
# Make reverse map, used by sorter
self.metaMap = self.makeReverseMetaMap()
self.active = []
def delaySearch(self, evt):
@@ -172,15 +171,15 @@ class ItemView(Display):
def scheduleSearch(self, event=None):
self.searchTimer.Stop() # Cancel any pending timers
search = self.marketBrowser.search.GetLineText(0)
# Make sure we do not count wildcard as search symbol
realsearch = search.replace("*", "")
# Make sure we do not count wildcards as search symbol
realsearch = search.replace('*', '').replace('?', '')
# Re-select market group if search query has zero length
if len(realsearch) == 0:
self.selectionMade('search')
return
self.marketBrowser.mode = 'search'
self.sMkt.searchItems(search, self.populateSearch)
self.sMkt.searchItems(search, self.populateSearch, 'market')
def clearSearch(self, event=None):
# Wipe item store and update everything to accomodate with it
@@ -195,30 +194,15 @@ class ItemView(Display):
self.setToggles()
self.filterItemStore()
def populateSearch(self, items):
def populateSearch(self, itemIDs):
# If we're no longer searching, dump the results
if self.marketBrowser.mode != 'search':
return
items = Market.getItems(itemIDs)
self.updateItemStore(items)
self.setToggles()
self.filterItemStore()
def itemSort(self, item):
sMkt = self.sMkt
catname = sMkt.getCategoryByItem(item).name
try:
mktgrpid = sMkt.getMarketGroupByItem(item).ID
except AttributeError:
mktgrpid = -1
pyfalog.warning("unable to find market group for {}".format(item.name))
parentname = sMkt.getParentItemByItem(item).name
# Get position of market group
metagrpid = sMkt.getMetaGroupIdByItem(item)
metatab = self.metaMap.get(metagrpid)
metalvl = item.metaLevel or 0
return catname, mktgrpid, parentname, metatab, metalvl, item.name
def contextMenu(self, event):
clickedPos = self.getRowByAbs(event.Position)
self.ensureSelection(clickedPos)
@@ -241,7 +225,7 @@ class ItemView(Display):
self.unselectAll()
# Perform sorting, using item's meta levels besides other stuff
if self.marketBrowser.mode != 'recent':
items.sort(key=self.itemSort)
items.sort(key=self.sMkt.itemSort)
# Mark current item list as active
self.active = items
# Show them
@@ -251,26 +235,12 @@ class ItemView(Display):
if len(items) > 1:
# Re-sort stuff
if self.marketBrowser.mode != 'recent':
items.sort(key=self.itemSort)
items.sort(key=self.sMkt.itemSort)
for i, item in enumerate(items[:9]):
# set shortcut info for first 9 modules
item.marketShortcut = i + 1
Display.refresh(self, items)
def makeReverseMetaMap(self):
"""
Form map which tells in which tab items of given metagroup are located
"""
revmap = {}
i = 0
for mgids in self.sMkt.META_MAP.values():
for mgid in mgids:
revmap[mgid] = i
i += 1
return revmap
def columnBackground(self, colItem, item):
if self.sFit.serviceFittingOptions["colorFitBySlot"]:
return slotColourMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour()

View File

@@ -60,9 +60,12 @@ class MarketTree(wx.TreeCtrl):
# If market should have items but it doesn't, do not show it
if sMkt.marketGroupValidityCheck(childMktGrp) is False:
continue
iconId = self.addImage(sMkt.getIconByMarketGroup(childMktGrp))
icon = sMkt.getIconByMarketGroup(childMktGrp)
iconId = -1 if icon is None else self.addImage(icon)
try:
childId = self.AppendItem(root, childMktGrp.name, iconId, data=childMktGrp.ID)
except (KeyboardInterrupt, SystemExit):
raise
except Exception as e:
pyfalog.debug("Error appending item.")
pyfalog.debug(e)

View File

@@ -86,7 +86,7 @@ class PFSearchBox(wx.Window):
def OnKeyPress(self, event):
if event.RawControlDown() and event.GetKeyCode() == wx.WXK_BACK:
HandleCtrlBackspace(self.EditBox)
HandleCtrlBackspace(self.EditBox)
else:
event.Skip()

View File

@@ -21,9 +21,12 @@ class PFContextMenuPref(PreferenceView):
self.stTitle.SetFont(wx.Font(12, 70, 90, 90, False, wx.EmptyString))
mainSizer.Add(self.stTitle, 0, wx.EXPAND | wx.ALL, 5)
self.stSubTitle = wx.StaticText(panel, wx.ID_ANY,
"Disabling context menus can improve responsiveness.",
wx.DefaultPosition, wx.DefaultSize, 0)
self.stSubTitle = wx.StaticText(
panel, wx.ID_ANY,
'Disabling context menus can improve responsiveness.\n'
'You can hold {} key + right-click to show all menu items regardless of these settings.'.format(
'Command' if 'wxMac' in wx.PlatformInfo else 'Control'),
wx.DefaultPosition, wx.DefaultSize, 0)
self.stSubTitle.Wrap(-1)
mainSizer.Add(self.stSubTitle, 0, wx.ALL, 5)

View File

@@ -1,7 +1,6 @@
import wx
import config
from eos.db.saveddata.loadDefaultDatabaseValues import DefaultDatabaseValues
from eos.db.saveddata.queries import clearPrices, clearDamagePatterns, clearTargetProfiles
from gui.bitmap_loader import BitmapLoader
from gui.preferenceView import PreferenceView
@@ -36,31 +35,31 @@ class PFGeneralPref(PreferenceView):
# Database path
self.stSetUserPath = wx.StaticText(panel, wx.ID_ANY, "pyfa User Path:", wx.DefaultPosition, wx.DefaultSize, 0)
self.stSetUserPath.Wrap(-1)
mainSizer.Add(self.stSetUserPath, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
mainSizer.Add(self.stSetUserPath, 0, wx.ALL, 5)
self.inputUserPath = wx.TextCtrl(panel, wx.ID_ANY, config.savePath, wx.DefaultPosition, wx.DefaultSize, 0)
self.inputUserPath.SetEditable(False)
self.inputUserPath.SetBackgroundColour((200, 200, 200))
mainSizer.Add(self.inputUserPath, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5)
mainSizer.Add(self.inputUserPath, 0, wx.ALL | wx.EXPAND, 5)
# Save DB
self.stFitDB = wx.StaticText(panel, wx.ID_ANY, "Fitting Database:", wx.DefaultPosition, wx.DefaultSize, 0)
self.stFitDB.Wrap(-1)
mainSizer.Add(self.stFitDB, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
mainSizer.Add(self.stFitDB, 0, wx.ALL, 5)
self.inputFitDB = wx.TextCtrl(panel, wx.ID_ANY, config.saveDB, wx.DefaultPosition, wx.DefaultSize, 0)
self.inputFitDB.SetEditable(False)
self.inputFitDB.SetBackgroundColour((200, 200, 200))
mainSizer.Add(self.inputFitDB, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5)
mainSizer.Add(self.inputFitDB, 0, wx.ALL | wx.EXPAND, 5)
# Game Data DB
self.stGameDB = wx.StaticText(panel, wx.ID_ANY, "Game Database:", wx.DefaultPosition, wx.DefaultSize, 0)
self.stGameDB.Wrap(-1)
mainSizer.Add(self.stGameDB, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
mainSizer.Add(self.stGameDB, 0, wx.ALL, 5)
self.inputGameDB = wx.TextCtrl(panel, wx.ID_ANY, config.gameDB, wx.DefaultPosition, wx.DefaultSize, 0)
self.inputGameDB.SetEditable(False)
self.inputGameDB.SetBackgroundColour((200, 200, 200))
mainSizer.Add(self.inputGameDB, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5)
mainSizer.Add(self.inputGameDB, 0, wx.ALL | wx.EXPAND, 5)
self.cbsaveInRoot.SetValue(config.saveInRoot)
self.cbsaveInRoot.Bind(wx.EVT_CHECKBOX, self.onCBsaveInRoot)
@@ -75,10 +74,6 @@ class PFGeneralPref(PreferenceView):
btnSizer = wx.BoxSizer(wx.VERTICAL)
btnSizer.AddStretchSpacer()
self.btnImportDefaults = wx.Button(panel, wx.ID_ANY, "Reimport Database Defaults", wx.DefaultPosition, wx.DefaultSize, 0)
btnSizer.Add(self.btnImportDefaults, 0, wx.ALL, 5)
self.btnImportDefaults.Bind(wx.EVT_BUTTON, self.loadDatabaseDefaults)
self.btnDeleteDamagePatterns = wx.Button(panel, wx.ID_ANY, "Delete All Damage Pattern Profiles", wx.DefaultPosition, wx.DefaultSize, 0)
btnSizer.Add(self.btnDeleteDamagePatterns, 0, wx.ALL, 5)
self.btnDeleteDamagePatterns.Bind(wx.EVT_BUTTON, self.DeleteDamagePatterns)
@@ -97,14 +92,6 @@ class PFGeneralPref(PreferenceView):
panel.SetSizer(mainSizer)
panel.Layout()
def loadDatabaseDefaults(self, event):
# Import values that must exist otherwise Pyfa breaks
DefaultDatabaseValues.importRequiredDefaults()
# Import default values for damage profiles
DefaultDatabaseValues.importDamageProfileDefaults()
# Import default values for target resist profiles
DefaultDatabaseValues.importTargetProfileDefaults()
def DeleteDamagePatterns(self, event):
question = "This is a destructive action that will delete all damage pattern profiles.\nAre you sure you want to do this?"
if wxHelpers.YesNoDialog(question, "Confirm"):

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