Compare commits

...

146 Commits

Author SHA1 Message Date
DarkPhoenix
b5d3dc6d56 Bump version 2024-11-28 17:28:21 +01:00
DarkPhoenix
3500867336 Revert wxpython version bump 2024-11-27 23:41:29 +01:00
Anton Vorobyov
ea26d57566 Merge pull request #2635 from wereii/fix-syntax-warnings
Fix invalid escape syntax warnings
2024-11-27 23:38:15 +01:00
Anton Vorobyov
45f0743c57 Merge branch 'master' into fix-syntax-warnings 2024-11-27 23:37:50 +01:00
DarkPhoenix
08d4e852d3 Fix comparison issue which broke graphs in some cases 2024-11-27 23:25:49 +01:00
Anton Vorobyov
7edf1f93cb Merge pull request #2631 from ugurilter/master
fix arbitrary content being dragged and dropped causing errors
2024-11-26 19:59:08 +01:00
DarkPhoenix
31cfa7c93a Bump version 2024-11-26 19:17:50 +01:00
DarkPhoenix
da1d42b578 Fix drone sorter for EFT export 2024-11-26 19:16:44 +01:00
DarkPhoenix
502e5a8d8e Update static data 2024-11-26 19:03:00 +01:00
DarkPhoenix
087867bdca Update static data and icons 2024-11-14 14:12:20 +01:00
DarkPhoenix
86ad17b075 Expand breacher tooltip 2024-11-13 15:37:40 +01:00
DarkPhoenix
b8ad4772b8 Bump version 2024-11-13 12:32:24 +01:00
DarkPhoenix
e0e5be5870 Change tooltip 2024-11-13 12:32:07 +01:00
DarkPhoenix
8b2f7c56db Update effects 2024-11-13 12:30:51 +01:00
DarkPhoenix
0ca1a675c2 Update static data 2024-11-13 12:30:28 +01:00
DarkPhoenix
6a0bfce262 Expand radius tooltip 2024-11-13 11:02:02 +01:00
DarkPhoenix
362aebe658 Increase size of inputs 2024-11-13 10:35:09 +01:00
DarkPhoenix
bbe3e931f7 Bump wxpython version 2024-11-13 05:58:11 +01:00
DarkPhoenix
f5743af6a4 Cache calculated values 2024-11-13 05:57:05 +01:00
DarkPhoenix
dbc2993fb9 Support inflicted damage for breachers 2024-11-13 05:35:41 +01:00
DarkPhoenix
219173c43e A few fixes for breachers on graphs 2024-11-13 05:13:37 +01:00
DarkPhoenix
6074cfe15a Rework damage stats calculation to always expose full breacher info 2024-11-13 04:37:15 +01:00
DarkPhoenix
f0a72f4307 Add full HP parameter to graphs (for now, unused) 2024-11-12 17:14:52 +01:00
DarkPhoenix
ecc3f9fa7e Add base support for breacher pods to graphs 2024-11-12 16:58:18 +01:00
DarkPhoenix
c660e4058c Fix graphs 2024-11-12 14:49:52 +01:00
DarkPhoenix
7664b00b59 Minor fixes 2024-11-12 14:45:41 +01:00
DarkPhoenix
5721beacf5 Expose breacher DPS and volley to stats 2024-11-12 14:20:20 +01:00
DarkPhoenix
13f3793515 Implement breacher logic on module level 2024-11-11 21:17:32 +01:00
DarkPhoenix
f8e0520344 Add HP value to target profile editor 2024-11-11 15:07:40 +01:00
DarkPhoenix
2c8e306dc2 Add HP field to target profile 2024-11-11 14:34:38 +01:00
DarkPhoenix
7c37d8fd74 Add a few bits to breacher volley calculation 2024-11-11 14:12:06 +01:00
DarkPhoenix
88129c0df4 Do not choke on mutated drones in drone panel, since they lost market group 2024-11-10 12:10:10 +01:00
DarkPhoenix
fa67efd9cb Revert wxpython version change, since binary wheels seem to be unavailable for it yet 2024-11-09 23:54:00 +01:00
DarkPhoenix
38a345df93 Allow release info change on linux 2024-11-09 17:29:53 +01:00
DarkPhoenix
b41ec8a5a9 Bump version 2024-11-09 16:45:26 +01:00
DarkPhoenix
4ba9bb7b99 Add info to misc column 2024-11-09 16:37:46 +01:00
DarkPhoenix
f3c8f485b9 Implement breacher pod skill effects 2024-11-09 12:01:28 +01:00
DarkPhoenix
8d204dd173 Add deathless faction icon 2024-11-09 11:37:24 +01:00
DarkPhoenix
f870d7e6d2 Add effects for new ships 2024-11-09 11:28:29 +01:00
DarkPhoenix
715997041a Update icons 2024-11-09 09:23:19 +01:00
DarkPhoenix
003191d9a4 Update effects for everything but new ships 2024-11-09 09:21:48 +01:00
DarkPhoenix
9302d79e1d Sort glorified mutaplasmids right after their base versions, and shorten them to Gl. 2024-11-09 09:04:18 +01:00
DarkPhoenix
401fc671d4 Fix a few regex warnings 2024-11-09 08:40:55 +01:00
DarkPhoenix
fbadcf0af6 Update static data 2024-11-09 08:28:44 +01:00
DarkPhoenix
c7f600f88c Bump wxpython version 2024-11-02 08:15:33 +01:00
DarkPhoenix
229dd0ddae Fix sorter for structure fighters 2024-11-02 08:15:21 +01:00
DarkPhoenix
8883b18025 Bump version 2024-10-31 00:50:26 +01:00
DarkPhoenix
ea9b67cedd Implement python effects 2024-10-31 00:50:08 +01:00
DarkPhoenix
c4cf1d1ecb Implement Cobra effects 2024-10-31 00:32:50 +01:00
DarkPhoenix
f76f7e85db Add ability to override character security status via a menu 2024-10-31 00:08:31 +01:00
DarkPhoenix
b807e2a36b Add on-fit pilot security attribute, and use it in effects 2024-10-30 20:09:26 +01:00
DarkPhoenix
2bb35a15ce Change lower limit of SS to be -10 instead of -5 2024-10-30 19:12:19 +01:00
DarkPhoenix
376bda7a94 Implement sidewinder effects 2024-10-30 19:10:25 +01:00
DarkPhoenix
a642bdc8c9 Move prize ships to separate category 2024-10-30 18:16:14 +01:00
DarkPhoenix
71f3e7b858 Update icons 2024-10-30 18:12:47 +01:00
DarkPhoenix
88f5ed2da5 Update staticdata 2024-10-30 18:04:41 +01:00
DarkPhoenix
3b9bbae0c3 Bump version 2024-10-03 13:37:54 +02:00
DarkPhoenix
6d474492ea Update effects file 2024-10-03 13:37:25 +02:00
DarkPhoenix
ebc61efef8 Update static data 2024-10-03 13:28:30 +02:00
Ugur Ilter
3892dfc78c fix arbitrary content being dragged and dropped causing errors
Fixes #2630
2024-09-26 15:52:26 +03:00
DarkPhoenix
7b5c95213f Bump version 2024-09-18 13:54:33 +02:00
DarkPhoenix
1312177e0e Add new renames 2024-09-18 13:42:53 +02:00
DarkPhoenix
5b0df0a9c3 Update static data to 2682489 2024-09-18 13:33:03 +02:00
wereii
954a164922 Fix invalid escape syntax warnings
Additionally use rawstrings with all regex calls
2024-09-11 17:25:38 +02:00
DarkPhoenix
1ea6136cb2 Update static data 2024-09-11 16:32:13 +02:00
DarkPhoenix
da2b6bbb6d Bump version 2024-09-10 00:01:40 +02:00
DarkPhoenix
87007af5f8 Pass package path to importlib call, not module path 2024-09-09 23:35:07 +02:00
DarkPhoenix
dd5ab872b1 Reapply "Make it 3.9+, but get rid of deprecation warnings"
This reverts commit 65fb46885a.
2024-09-09 23:31:19 +02:00
DarkPhoenix
65fb46885a Revert "Make it 3.9+, but get rid of deprecation warnings"
This reverts commit 01354a0a83.
2024-09-09 23:00:24 +02:00
DarkPhoenix
fad1e401b2 Bump version 2024-09-09 20:25:02 +02:00
DarkPhoenix
d59f8afd8a Update icons 2024-09-09 20:24:33 +02:00
DarkPhoenix
7c42d00219 Update static data 2024-09-09 20:20:44 +02:00
DarkPhoenix
777ba69ac4 Ensure context menus can be called on items w/o market group 2024-07-20 19:09:00 +02:00
DarkPhoenix
9a3bde872b Fix regex pattern to remove syntax warning 2024-07-09 23:23:49 +02:00
DarkPhoenix
01354a0a83 Make it 3.9+, but get rid of deprecation warnings 2024-07-09 21:49:05 +02:00
DarkPhoenix
8c3b8589d5 Adjust jargon loader to be py3.12-compatible 2024-07-09 21:46:39 +02:00
Anton Vorobyov
451b5d4312 Merge pull request #2624 from lunedis/feature/charges-for-fit
Add "Charges For Active Fit" market tree shortcut
2024-07-09 23:31:53 +04:00
Anton Vorobyov
d204e70afc Merge pull request #2614 from kgasiorowski/issue/2613
Rename Industrial to Hauler
2024-07-09 23:29:02 +04:00
Karl Werner
ce9ce17ad2 use mode to limit refresh 2024-07-07 14:41:16 +02:00
Karl Werner
6a7cdda91a add comments 2024-07-06 16:01:58 +02:00
Karl Werner
f38c61da51 update on fit change, new icon 2024-07-06 15:52:18 +02:00
Karl Werner
735827a25b proof of concept 2024-07-06 12:12:48 +02:00
DarkPhoenix
d3fcdcbe47 Marshal bonus is not stacking penalized 2024-06-29 23:50:03 +02:00
DarkPhoenix
a3dce73663 Update static data 2024-06-27 21:21:35 +02:00
Kuba Gasiorowski
594b58388f Merge branch 'master' into issue/2613 2024-06-14 18:40:49 -04:00
DarkPhoenix
fa6be2edfb Bump version 2024-06-13 16:20:49 +02:00
DarkPhoenix
1177968b02 Update renders 2024-06-13 16:20:29 +02:00
DarkPhoenix
c04b370410 Update static data 2024-06-13 16:15:45 +02:00
DarkPhoenix
038b95531d Bump version 2024-06-12 16:04:00 +02:00
DarkPhoenix
56bc5c3376 Fix icons script and add missing renders 2024-06-12 16:03:09 +02:00
DarkPhoenix
f5d8be7861 Update static data and add new effect 2024-06-12 15:11:15 +02:00
Kuba Gasiorowski
5366c23db2 Merge branch 'master' into issue/2613 2024-06-11 14:03:24 -04:00
DarkPhoenix
d147db22f1 Bump version 2024-06-11 16:33:10 +02:00
DarkPhoenix
28ebcd2739 Fix upwell ship race detection 2024-06-08 03:12:34 +02:00
DarkPhoenix
2f57eb6ea6 Bump version 2024-06-08 03:01:13 +02:00
DarkPhoenix
3a7ee4699e Update effects and related things 2024-06-08 03:00:45 +02:00
DarkPhoenix
fa7f991fae Update icons 2024-06-08 01:37:13 +02:00
DarkPhoenix
0be8b429aa Add renames 2024-06-08 01:19:22 +02:00
DarkPhoenix
7d94e2de7d Update static data to 2604967 2024-06-08 00:57:34 +02:00
Kuba Gasiorowski
6d32db5827 Rename Industrial to Hauler 2024-05-26 23:45:27 -04:00
DarkPhoenix
0fbb318d8a Bump version 2024-04-30 04:10:53 +06:00
DarkPhoenix
cd013e8287 Add new effects 2024-04-30 04:10:33 +06:00
DarkPhoenix
e667453c1e Add new icons 2024-04-30 03:59:25 +06:00
DarkPhoenix
24e2db0f88 Update static data to 2563119 2024-04-30 03:55:34 +06:00
DarkPhoenix
3c47f8c6bb Add OS version comments to windows manifest 2024-04-17 02:42:42 +06:00
DarkPhoenix
42c2b3f253 Bundle extra library with appimage 2024-04-16 22:16:35 +06:00
DarkPhoenix
71d6830ac0 Bundle extra library with appimage 2024-04-16 22:03:25 +06:00
DarkPhoenix
ac2fb01a7c Update icons 2024-04-16 21:32:17 +06:00
DarkPhoenix
fc66c212fc Bump version 2024-04-16 21:31:29 +06:00
DarkPhoenix
9625fafda7 Merge remote-tracking branch 'origin/master' 2024-04-16 21:21:46 +06:00
DarkPhoenix
6b1aca4306 Update static data to 2550121 2024-04-16 21:21:33 +06:00
Anton Vorobyov
fd5a094304 Merge pull request #2597 from josephdouce/patch-2
Searchable exportHtml.py
2024-02-28 20:10:02 +04:00
Joseph Douce
a3142ff62f Update exportHtml.py 2024-02-28 15:36:02 +00:00
Joseph Douce
58ebfd3643 Searchable exportHtml.py
added searchable fits
2024-02-28 15:33:18 +00:00
Anton Vorobyov
e272267842 Merge pull request #2596 from josephdouce/patch-1
Fix exportHtml.py
2024-02-28 19:25:20 +04:00
Joseph Douce
b0256542bc Fix exportHtml.py 2024-02-28 13:17:23 +00:00
DarkPhoenix
9b8fd24987 Update pot 2024-02-27 16:30:06 +06:00
DarkPhoenix
9c68889a6d Call SelectAll() after Fit() since that's what seems to be causing issues under windows 2024-02-27 08:15:39 +06:00
DarkPhoenix
a4ed6e8066 Move select all call earlier, to see which call breaks it on windows 2024-02-27 07:38:01 +06:00
DarkPhoenix
9fe8163a69 Select all text even later 2024-02-27 05:54:55 +06:00
DarkPhoenix
9f9b496726 Select all text even later 2024-02-27 05:30:03 +06:00
DarkPhoenix
3b8fa68a4c Bump version 2024-02-27 04:11:02 +06:00
DarkPhoenix
9f5a649b04 Change migration to include newly added constraints 2024-02-27 04:05:19 +06:00
DarkPhoenix
fb45736c14 Extend UNIQUE constraint by new server column, to avoid possible collisions 2024-02-27 03:36:43 +06:00
Anton Vorobyov
b8a58fc7a6 Merge pull request #2590 from huangzheng2016/master
Update the SSO Login for Serenity and Singularity server's player
2024-02-26 23:35:31 +04:00
DarkPhoenix
b04c521805 Make sure not to crash character editor during SSO init when char editor is opened 2024-02-27 01:18:01 +06:00
DarkPhoenix
526695fa9a Try setting size for graph input boxes as a workaround for wx bug 2024-02-27 00:30:19 +06:00
Anton Vorobyov
d6be77d107 Merge pull request #2589 from pyfa-org/dependabot/pip/cryptography-42.0.4
Bump cryptography from 42.0.2 to 42.0.4
2024-02-26 22:22:33 +04:00
DarkPhoenix
907da343b1 Rework how progress dialog is used 2024-02-27 00:04:44 +06:00
DarkPhoenix
3fadccc715 Select all after setting focus as an attempt to fix input box not selecting everything under windows 2024-02-26 06:25:07 +06:00
DarkPhoenix
82a912858f Fix items in market's item view 2024-02-26 04:45:18 +06:00
正汰
cc1fdddc0a Merge branch 'pyfa-org:master' into master 2024-02-23 02:50:41 +08:00
正汰
e314ecf887 Fix the esi problem for Serenity when setting 2024-02-23 02:48:54 +08:00
正汰
94b1c8b029 Fix the esi problem for Serenity when setting 2024-02-23 02:47:59 +08:00
正汰
6e3b7ff132 Fix the esi problem for Serenity when setting 2024-02-23 02:47:47 +08:00
正汰
ff14855808 Fix the esi problem for Serenity when setting 2024-02-23 02:47:27 +08:00
dependabot[bot]
4761857b84 Bump cryptography from 42.0.2 to 42.0.4
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.2 to 42.0.4.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.2...42.0.4)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-21 20:48:49 +00:00
DarkPhoenix
bb53e75bfe Force light rack colors on windows regardless of OS theme 2024-02-21 03:04:36 +06:00
正汰
be85080d8d Merge branch 'pyfa-org:master' into master 2024-01-12 11:00:13 +08:00
blitzmann
27101732ca Add a couple of bug fixes, and make the server option a checkbox 2022-05-08 13:48:38 -04:00
blitzmann
ed48a8b5d0 Re-work the exceptions related to logging a character in 2022-05-08 13:21:23 -04:00
blitzmann
5a6fe373f1 Merge branch 'master' into huangzheng2016-master
# Conflicts:
#	gui/builtinPreferenceViews/pyfaEsiPreferences.py
2022-05-08 12:35:46 -04:00
blitzmann
3c6eb6d054 Some updates to allow code / token paste regardless of server setting 2022-05-08 12:24:12 -04:00
blitzmann
dc997f0dc4 Various updates to support server-aware calls per character 2022-05-07 12:35:16 -04:00
blitzmann
14afc83e86 Fix client ID that is used 2022-04-30 10:17:51 -04:00
hz2016
216dd2a787 Update the SSO Login for Serenity and Singularity server's player 2022-04-28 15:58:33 +08:00
364 changed files with 182372 additions and 24597 deletions

View File

@@ -17,7 +17,7 @@ for:
# init: # init:
# - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - # - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e -
install: install:
- sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y update - sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y update --allow-releaseinfo-change
# AppImage dependencies # AppImage dependencies
- sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install libfuse2 - sh: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install libfuse2
# Preparation script dependencies # Preparation script dependencies

View File

@@ -2,7 +2,7 @@
## Requirements ## Requirements
- Python 3.7 - Python 3.11
- Git CLI installed - Git CLI installed
- Python, pip and git are all available as command-line commands (add to the path if needed) - Python, pip and git are all available as command-line commands (add to the path if needed)

View File

@@ -9,6 +9,7 @@ import hashlib
from eos.const import FittingSlot from eos.const import FittingSlot
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from collections import namedtuple
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -44,9 +45,16 @@ experimentalFeatures = None
version = None version = None
language = None language = None
API_CLIENT_ID = '095d8cd841ac40b581330919b49fe746' ApiServer = namedtuple('ApiBase', ['name', 'sso', 'esi', 'client_id', 'callback', 'supports_auto_login'])
supported_servers = {
"Tranquility": ApiServer("Tranquility", "login.eveonline.com", "esi.evetech.net", '095d8cd841ac40b581330919b49fe746', 'https://pyfa-org.github.io/Pyfa/callback', True),
# No point having SISI: https://developers.eveonline.com/blog/article/removing-datasource-singularity
# "Singularity": ApiServer("Singularity", "sisilogin.testeveonline.com", "esi.evetech.net", 'b9c3cc79448f449ab17f3aebd018842e', 'https://pyfa-org.github.io/Pyfa/callback'),
"Serenity": ApiServer("Serenity", "login.evepc.163.com", "ali-esi.evepc.163.com", 'bc90aa496a404724a93f41b4f4e97761', 'https://ali-esi.evepc.163.com/ui/oauth2-redirect.html', False)
}
SSO_LOGOFF_SERENITY='https://login.evepc.163.com/account/logoff'
ESI_CACHE = 'esi_cache' ESI_CACHE = 'esi_cache'
SSO_CALLBACK = 'https://pyfa-org.github.io/Pyfa/callback'
LOGLEVEL_MAP = { LOGLEVEL_MAP = {
"critical": CRITICAL, "critical": CRITICAL,

View File

@@ -141,14 +141,14 @@ def update_db():
(row['typeName_en-us'].startswith('Civilian') and "Shuttle" not in row['typeName_en-us']) (row['typeName_en-us'].startswith('Civilian') and "Shuttle" not in row['typeName_en-us'])
or row['typeName_en-us'] == 'Capsule' or row['typeName_en-us'] == 'Capsule'
or row['groupID'] == 4033 # destructible effect beacons or row['groupID'] == 4033 # destructible effect beacons
or re.match('AIR .+Booster.*', row['typeName_en-us']) or re.match(r'AIR .+Booster.*', row['typeName_en-us'])
): ):
row['published'] = True row['published'] = True
# Nearly useless and clutter search results too much # Nearly useless and clutter search results too much
elif ( elif (
row['typeName_en-us'].startswith('Limited Synth ') row['typeName_en-us'].startswith('Limited Synth ')
or row['typeName_en-us'].startswith('Expired ') or row['typeName_en-us'].startswith('Expired ')
or re.match('Mining Blitz .+ Booster Dose .+', row['typeName_en-us']) or re.match(r'Mining Blitz .+ Booster Dose .+', row['typeName_en-us'])
or row['typeName_en-us'].endswith(' Filament') and ( or row['typeName_en-us'].endswith(' Filament') and (
"'Needlejack'" not in row['typeName_en-us'] and "'Needlejack'" not in row['typeName_en-us'] and
"'Devana'" not in row['typeName_en-us'] and "'Devana'" not in row['typeName_en-us'] and
@@ -544,7 +544,7 @@ def update_db():
continue continue
typeName = row.get('typeName_en-us', '') typeName = row.get('typeName_en-us', '')
# Regular sets matching # 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) m = re.match(r'(?P<grade>(High|Mid|Low)-grade) (?P<set>\w+) (?P<implant>(Alpha|Beta|Gamma|Delta|Epsilon|Omega))', typeName, re.IGNORECASE)
if m: if m:
implantSets.setdefault((m.group('grade'), m.group('set')), set()).add(row['typeID']) implantSets.setdefault((m.group('grade'), m.group('set')), set()).add(row['typeID'])
# Special set matching # Special set matching

View File

@@ -33,6 +33,8 @@ AppDir:
- libwebkit2gtk-4.0-37 # Needed for wx's HTML lib - libwebkit2gtk-4.0-37 # Needed for wx's HTML lib
# Unknown # Unknown
- libpcre2-32-0 # https://github.com/pyfa-org/Pyfa/issues/2572 - libpcre2-32-0 # https://github.com/pyfa-org/Pyfa/issues/2572
- libnotify4 # https://github.com/pyfa-org/Pyfa/issues/2598
- libwayland-client0 # https://github.com/pyfa-org/Pyfa/issues/2600
exclude: exclude:
- hicolor-icon-theme - hicolor-icon-theme
- humanity-icon-theme - humanity-icon-theme

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_DIST_DIR"] = os.path.join(os.getcwd(), 'dist')
os.environ["PYFA_VERSION"] = version os.environ["PYFA_VERSION"] = version
iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" iscc = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
source = os.path.join(os.environ["PYFA_DIST_DIR"], "pyfa") source = os.path.join(os.environ["PYFA_DIST_DIR"], "pyfa")

View File

@@ -10,10 +10,15 @@
</trustInfo> </trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application> <application>
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application> </application>
</compatibility> </compatibility>

View File

@@ -21,7 +21,7 @@ for modName in iterNamespace(__name__, __path__):
# loop through python files, extracting update number and function, and # loop through python files, extracting update number and function, and
# adding it to a list # adding it to a list
modname_tail = modName.rsplit('.', 1)[-1] modname_tail = modName.rsplit('.', 1)[-1]
m = re.match("^upgrade(?P<index>\d+)$", modname_tail) m = re.match(r"^upgrade(?P<index>\d+)$", modname_tail)
if not m: if not m:
continue continue
index = int(m.group("index")) index = int(m.group("index"))

View File

@@ -0,0 +1,36 @@
"""
Migration 46
- add support for server selection for SSO characters
"""
import sqlalchemy
tmpTable = """
CREATE TABLE ssoCharacterTemp (
ID INTEGER NOT NULL,
client VARCHAR NOT NULL,
characterID INTEGER NOT NULL,
characterName VARCHAR NOT NULL,
refreshToken VARCHAR NOT NULL,
accessToken VARCHAR NOT NULL,
accessTokenExpires DATETIME NOT NULL,
created DATETIME,
modified DATETIME,
server VARCHAR,
PRIMARY KEY (ID),
CONSTRAINT "uix_client_server_characterID" UNIQUE (client, server, characterID),
CONSTRAINT "uix_client_server_characterName" UNIQUE (client, server, characterName)
)
"""
def upgrade(saveddata_engine):
try:
saveddata_engine.execute("SELECT server FROM ssoCharacter LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute(tmpTable)
saveddata_engine.execute(
"INSERT INTO ssoCharacterTemp (ID, client, characterID, characterName, refreshToken, accessToken, accessTokenExpires, created, modified, server) "
"SELECT ID, client, characterID, characterName, refreshToken, accessToken, accessTokenExpires, created, modified, 'Tranquility' "
"FROM ssoCharacter")
saveddata_engine.execute("DROP TABLE ssoCharacter")
saveddata_engine.execute("ALTER TABLE ssoCharacterTemp RENAME TO ssoCharacter")

View File

@@ -0,0 +1,15 @@
"""
Migration 48
- added pilot security column (CONCORD ships)
"""
import sqlalchemy
def upgrade(saveddata_engine):
try:
saveddata_engine.execute("SELECT pilotSecurity FROM fits LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE fits ADD COLUMN pilotSecurity FLOAT")

View File

@@ -0,0 +1,15 @@
"""
Migration 49
- added hp column to targetResists table
"""
import sqlalchemy
def upgrade(saveddata_engine):
try:
saveddata_engine.execute("SELECT hp FROM targetResists LIMIT 1;")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE targetResists ADD COLUMN hp FLOAT;")

View File

@@ -40,18 +40,18 @@ characters_table = Table("characters", saveddata_meta,
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)) Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now))
sso_table = Table("ssoCharacter", saveddata_meta, sso_table = Table("ssoCharacter", saveddata_meta,
Column("ID", Integer, primary_key=True), Column("ID", Integer, primary_key=True),
Column("client", String, nullable=False), Column("client", String, nullable=False),
Column("characterID", Integer, nullable=False), Column("characterID", Integer, nullable=False),
Column("characterName", String, nullable=False), Column("characterName", String, nullable=False),
Column("refreshToken", String, nullable=False), Column("server", String, nullable=False),
Column("accessToken", String, nullable=False), Column("refreshToken", String, nullable=False),
Column("accessTokenExpires", DateTime, nullable=False), Column("accessToken", String, nullable=False),
Column("created", DateTime, nullable=True, default=datetime.datetime.now), Column("accessTokenExpires", DateTime, nullable=False),
Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now), Column("created", DateTime, nullable=True, default=datetime.datetime.now),
UniqueConstraint('client', 'characterID', name='uix_client_characterID'), Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now),
UniqueConstraint('client', 'characterName', name='uix_client_characterName') UniqueConstraint('client', 'server', 'characterID', name='uix_client_server_characterID'),
) UniqueConstraint('client', 'server', 'characterName', name='uix_client_server_characterName'))
sso_character_map_table = Table("ssoCharacterMap", saveddata_meta, sso_character_map_table = Table("ssoCharacterMap", saveddata_meta,
Column("characterID", ForeignKey("characters.ID"), primary_key=True), Column("characterID", ForeignKey("characters.ID"), primary_key=True),

View File

@@ -63,7 +63,8 @@ fits_table = Table("fits", saveddata_meta,
Column("ignoreRestrictions", Boolean, default=0), Column("ignoreRestrictions", Boolean, default=0),
Column("created", DateTime, nullable=True, default=datetime.datetime.now), Column("created", DateTime, nullable=True, default=datetime.datetime.now),
Column("modified", DateTime, nullable=True, default=datetime.datetime.now, onupdate=datetime.datetime.now), Column("modified", DateTime, nullable=True, default=datetime.datetime.now, onupdate=datetime.datetime.now),
Column("systemSecurity", Integer, nullable=True) Column("systemSecurity", Integer, nullable=True),
Column("pilotSecurity", Float, nullable=True),
) )
projectedFits_table = Table("projectedFits", saveddata_meta, projectedFits_table = Table("projectedFits", saveddata_meta,

View File

@@ -493,9 +493,12 @@ def getSsoCharacters(clientHash, eager=None):
@cachedQuery(SsoCharacter, 1, "lookfor", "clientHash") @cachedQuery(SsoCharacter, 1, "lookfor", "clientHash")
def getSsoCharacter(lookfor, clientHash, eager=None): def getSsoCharacter(lookfor, clientHash, server=None, eager=None):
filter = SsoCharacter.client == clientHash filter = SsoCharacter.client == clientHash
if server is not None:
filter = and_(filter, SsoCharacter.server == server)
if isinstance(lookfor, int): if isinstance(lookfor, int):
filter = and_(filter, SsoCharacter.ID == lookfor) filter = and_(filter, SsoCharacter.ID == lookfor)
elif isinstance(lookfor, str): elif isinstance(lookfor, str):

View File

@@ -37,6 +37,7 @@ targetProfiles_table = Table(
Column('maxVelocity', Float, nullable=True), Column('maxVelocity', Float, nullable=True),
Column('signatureRadius', Float, nullable=True), Column('signatureRadius', Float, nullable=True),
Column('radius', Float, nullable=True), Column('radius', Float, nullable=True),
Column('hp', Float, nullable=True),
Column('ownerID', ForeignKey('users.ID'), nullable=True), Column('ownerID', ForeignKey('users.ID'), nullable=True),
Column('created', DateTime, nullable=True, default=datetime.datetime.now), Column('created', DateTime, nullable=True, default=datetime.datetime.now),
Column('modified', DateTime, nullable=True, onupdate=datetime.datetime.now)) Column('modified', DateTime, nullable=True, onupdate=datetime.datetime.now))
@@ -48,4 +49,5 @@ mapper(
'rawName': targetProfiles_table.c.name, 'rawName': targetProfiles_table.c.name,
'_maxVelocity': targetProfiles_table.c.maxVelocity, '_maxVelocity': targetProfiles_table.c.maxVelocity,
'_signatureRadius': targetProfiles_table.c.signatureRadius, '_signatureRadius': targetProfiles_table.c.signatureRadius,
'_radius': targetProfiles_table.c.radius}) '_radius': targetProfiles_table.c.radius,
'_hp': targetProfiles_table.c.hp})

File diff suppressed because it is too large Load Diff

View File

@@ -343,7 +343,9 @@ class Item(EqBase):
500018: "mordu", 500018: "mordu",
500019: "sansha", 500019: "sansha",
500020: "serpentis", 500020: "serpentis",
500026: "triglavian" 500026: "triglavian",
500027: "upwell",
500029: "deathless",
} }
@property @property
@@ -351,11 +353,7 @@ class Item(EqBase):
if self.__race is None: if self.__race is None:
try: try:
if ( if self.category.name == 'Structure':
self.category.name == 'Structure' or
# Here until CCP puts their shit together
self.name in ("Thunderchild", "Stormbringer", "Skybreaker")
):
self.__race = "upwell" self.__race = "upwell"
else: else:
self.__race = self.factionMap[self.factionID] self.__race = self.factionMap[self.factionID]
@@ -377,7 +375,8 @@ class Item(EqBase):
16 : "jove", 16 : "jove",
32 : "sansha", # Incrusion Sansha 32 : "sansha", # Incrusion Sansha
128: "ore", 128: "ore",
135: "triglavian" 135: "triglavian",
168: "upwell",
} }
# Race is None by default # Race is None by default
race = None race = None
@@ -571,13 +570,18 @@ class DynamicItem(EqBase):
@property @property
def shortName(self): def shortName(self):
name = self.item.customName name = self.item.customName
keywords = ('Decayed', 'Gravid', 'Unstable', 'Radical') keywords = (
'Decayed', 'Glorified Decayed',
'Gravid', 'Glorified Gravid',
'Unstable', 'Glorified Unstable',
'Radical', 'Glorified Radical')
for kw in keywords: for kw in keywords:
if name.startswith(f'{kw} '): if name.startswith(f'{kw} '):
name = kw name = kw
m = re.match('(?P<mutagrade>\S+) (?P<dronetype>\S+) Drone (?P<mutatype>\S+) Mutaplasmid', name) m = re.match(r'(?P<mutagrade>(Glorified )?\S+) (?P<dronetype>\S+) Drone (?P<mutatype>\S+) Mutaplasmid', name)
if m: if m:
name = '{} {}'.format(m.group('mutagrade'), m.group('mutatype')) name = '{} {}'.format(m.group('mutagrade'), m.group('mutatype'))
name = name.replace('Glorified ', 'Gl. ')
return name return name

View File

@@ -19,6 +19,7 @@
import math import math
from copy import deepcopy
from logbook import Logger from logbook import Logger
from sqlalchemy.orm import reconstructor, validates from sqlalchemy.orm import reconstructor, validates
@@ -161,7 +162,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
def getVolleyParameters(self, targetProfile=None): def getVolleyParameters(self, targetProfile=None):
if not self.dealsDamage or self.amountActive <= 0: if not self.dealsDamage or self.amountActive <= 0:
return {0: DmgTypes(0, 0, 0, 0)} return {0: DmgTypes.default()}
if self.__baseVolley is None: if self.__baseVolley is None:
dmgGetter = self.getModifiedChargeAttr if self.hasAmmo else self.getModifiedItemAttr dmgGetter = self.getModifiedChargeAttr if self.hasAmmo else self.getModifiedItemAttr
dmgMult = self.amountActive * (self.getModifiedItemAttr("damageMultiplier", 1)) dmgMult = self.amountActive * (self.getModifiedItemAttr("damageMultiplier", 1))
@@ -170,11 +171,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult, thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult, kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult) explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
volley = DmgTypes( volley = deepcopy(self.__baseVolley)
em=self.__baseVolley.em * (1 - getattr(targetProfile, "emAmount", 0)), volley.profile = targetProfile
thermal=self.__baseVolley.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=self.__baseVolley.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=self.__baseVolley.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)))
return {0: volley} return {0: volley}
def getVolley(self, targetProfile=None): def getVolley(self, targetProfile=None):
@@ -183,16 +181,12 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
def getDps(self, targetProfile=None): def getDps(self, targetProfile=None):
volley = self.getVolley(targetProfile=targetProfile) volley = self.getVolley(targetProfile=targetProfile)
if not volley: if not volley:
return DmgTypes(0, 0, 0, 0) return DmgTypes.default()
cycleParams = self.getCycleParameters() cycleParams = self.getCycleParameters()
if cycleParams is None: if cycleParams is None:
return DmgTypes(0, 0, 0, 0) return DmgTypes.default()
dpsFactor = 1 / (cycleParams.averageTime / 1000) dpsFactor = 1 / (cycleParams.averageTime / 1000)
dps = DmgTypes( dps = volley * dpsFactor
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,
kinetic=volley.kinetic * dpsFactor,
explosive=volley.explosive * dpsFactor)
return dps return dps
def isRemoteRepping(self, ignoreState=False): def isRemoteRepping(self, ignoreState=False):

View File

@@ -19,6 +19,7 @@
import math import math
from copy import deepcopy
from logbook import Logger from logbook import Logger
from sqlalchemy.orm import reconstructor, validates from sqlalchemy.orm import reconstructor, validates
@@ -198,16 +199,14 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
for ability in self.abilities: for ability in self.abilities:
# Not passing resists here as we want to calculate and store base volley # Not passing resists here as we want to calculate and store base volley
self.__baseVolley[ability.effectID] = {0: ability.getVolley()} self.__baseVolley[ability.effectID] = {0: ability.getVolley()}
adjustedVolley = {} adjustedVolleys = {}
for effectID, effectData in self.__baseVolley.items(): for effectID, effectData in self.__baseVolley.items():
adjustedVolley[effectID] = {} adjustedVolleys[effectID] = {}
for volleyTime, volleyValue in effectData.items(): for volleyTime, baseVolley in effectData.items():
adjustedVolley[effectID][volleyTime] = DmgTypes( adjustedVolley = deepcopy(baseVolley)
em=volleyValue.em * (1 - getattr(targetProfile, "emAmount", 0)), adjustedVolley.profile = targetProfile
thermal=volleyValue.thermal * (1 - getattr(targetProfile, "thermalAmount", 0)), adjustedVolleys[effectID][volleyTime] = adjustedVolley
kinetic=volleyValue.kinetic * (1 - getattr(targetProfile, "kineticAmount", 0)), return adjustedVolleys
explosive=volleyValue.explosive * (1 - getattr(targetProfile, "explosiveAmount", 0)))
return adjustedVolley
def getVolleyPerEffect(self, targetProfile=None): def getVolleyPerEffect(self, targetProfile=None):
volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile) volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile)
@@ -218,28 +217,16 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def getVolley(self, targetProfile=None): def getVolley(self, targetProfile=None):
volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile) volleyParams = self.getVolleyParametersPerEffect(targetProfile=targetProfile)
em = 0 volley = DmgTypes.default()
therm = 0
kin = 0
exp = 0
for volleyData in volleyParams.values(): for volleyData in volleyParams.values():
em += volleyData[0].em volley += volleyData[0]
therm += volleyData[0].thermal return volley
kin += volleyData[0].kinetic
exp += volleyData[0].explosive
return DmgTypes(em, therm, kin, exp)
def getDps(self, targetProfile=None): def getDps(self, targetProfile=None):
em = 0 dps = DmgTypes.default()
thermal = 0 for subdps in self.getDpsPerEffect(targetProfile=targetProfile).values():
kinetic = 0 dps += subdps
explosive = 0 return dps
for dps in self.getDpsPerEffect(targetProfile=targetProfile).values():
em += dps.em
thermal += dps.thermal
kinetic += dps.kinetic
explosive += dps.explosive
return DmgTypes(em=em, thermal=thermal, kinetic=kinetic, explosive=explosive)
def getDpsPerEffect(self, targetProfile=None): def getDpsPerEffect(self, targetProfile=None):
if not self.active or self.amount <= 0: if not self.active or self.amount <= 0:

View File

@@ -116,7 +116,7 @@ class FighterAbility:
def getVolley(self, targetProfile=None): def getVolley(self, targetProfile=None):
if not self.dealsDamage or not self.active: if not self.dealsDamage or not self.active:
return DmgTypes(0, 0, 0, 0) return DmgTypes.default()
if self.attrPrefix == "fighterAbilityLaunchBomb": if self.attrPrefix == "fighterAbilityLaunchBomb":
em = self.fighter.getModifiedChargeAttr("emDamage", 0) em = self.fighter.getModifiedChargeAttr("emDamage", 0)
therm = self.fighter.getModifiedChargeAttr("thermalDamage", 0) therm = self.fighter.getModifiedChargeAttr("thermalDamage", 0)
@@ -128,24 +128,17 @@ class FighterAbility:
kin = self.fighter.getModifiedItemAttr("{}DamageKin".format(self.attrPrefix), 0) kin = self.fighter.getModifiedItemAttr("{}DamageKin".format(self.attrPrefix), 0)
exp = self.fighter.getModifiedItemAttr("{}DamageExp".format(self.attrPrefix), 0) exp = self.fighter.getModifiedItemAttr("{}DamageExp".format(self.attrPrefix), 0)
dmgMult = self.fighter.amount * self.fighter.getModifiedItemAttr("{}DamageMultiplier".format(self.attrPrefix), 1) dmgMult = self.fighter.amount * self.fighter.getModifiedItemAttr("{}DamageMultiplier".format(self.attrPrefix), 1)
volley = DmgTypes( volley = DmgTypes(em=em * dmgMult, thermal=therm * dmgMult, kinetic=kin * dmgMult, explosive=exp * dmgMult)
em=em * dmgMult * (1 - getattr(targetProfile, "emAmount", 0)), volley.profile = targetProfile
thermal=therm * dmgMult * (1 - getattr(targetProfile, "thermalAmount", 0)),
kinetic=kin * dmgMult * (1 - getattr(targetProfile, "kineticAmount", 0)),
explosive=exp * dmgMult * (1 - getattr(targetProfile, "explosiveAmount", 0)))
return volley return volley
def getDps(self, targetProfile=None, cycleTimeOverride=None): def getDps(self, targetProfile=None, cycleTimeOverride=None):
volley = self.getVolley(targetProfile=targetProfile) volley = self.getVolley(targetProfile=targetProfile)
if not volley: if not volley:
return DmgTypes(0, 0, 0, 0) return DmgTypes.default()
cycleTime = cycleTimeOverride if cycleTimeOverride is not None else self.cycleTime cycleTime = cycleTimeOverride if cycleTimeOverride is not None else self.cycleTime
dpsFactor = 1 / (cycleTime / 1000) dpsFactor = 1 / (cycleTime / 1000)
dps = DmgTypes( dps = volley * dpsFactor
em=volley.em * dpsFactor,
thermal=volley.thermal * dpsFactor,
kinetic=volley.kinetic * dpsFactor,
explosive=volley.explosive * dpsFactor)
return dps return dps
def clear(self): def clear(self):

View File

@@ -1688,27 +1688,33 @@ class Fit:
self.__droneWaste = droneWaste self.__droneWaste = droneWaste
def calculateWeaponDmgStats(self, spoolOptions): def calculateWeaponDmgStats(self, spoolOptions):
weaponVolley = DmgTypes(0, 0, 0, 0) weaponVolley = DmgTypes.default()
weaponDps = DmgTypes(0, 0, 0, 0) weaponDps = DmgTypes.default()
for mod in self.modules: for mod in self.modules:
weaponVolley += mod.getVolley(spoolOptions=spoolOptions, targetProfile=self.targetProfile) weaponVolley += mod.getVolley(spoolOptions=spoolOptions)
weaponDps += mod.getDps(spoolOptions=spoolOptions, targetProfile=self.targetProfile) weaponDps += mod.getDps(spoolOptions=spoolOptions)
weaponVolley.profile = self.targetProfile
weaponDps.profile = self.targetProfile
self.__weaponVolleyMap[spoolOptions] = weaponVolley self.__weaponVolleyMap[spoolOptions] = weaponVolley
self.__weaponDpsMap[spoolOptions] = weaponDps self.__weaponDpsMap[spoolOptions] = weaponDps
def calculateDroneDmgStats(self): def calculateDroneDmgStats(self):
droneVolley = DmgTypes(0, 0, 0, 0) droneVolley = DmgTypes.default()
droneDps = DmgTypes(0, 0, 0, 0) droneDps = DmgTypes.default()
for drone in self.drones: for drone in self.drones:
droneVolley += drone.getVolley(targetProfile=self.targetProfile) droneVolley += drone.getVolley()
droneDps += drone.getDps(targetProfile=self.targetProfile) droneDps += drone.getDps()
for fighter in self.fighters: for fighter in self.fighters:
droneVolley += fighter.getVolley(targetProfile=self.targetProfile) droneVolley += fighter.getVolley()
droneDps += fighter.getDps(targetProfile=self.targetProfile) droneDps += fighter.getDps()
droneVolley.profile = self.targetProfile
droneDps.profile = self.targetProfile
self.__droneDps = droneDps self.__droneDps = droneDps
self.__droneVolley = droneVolley self.__droneVolley = droneVolley
@@ -1743,6 +1749,18 @@ class Fit:
secstatus = FitSystemSecurity.NULLSEC secstatus = FitSystemSecurity.NULLSEC
return secstatus return secstatus
def getPilotSecurity(self, low_limit=-10, high_limit=5):
secstatus = self.pilotSecurity
# Not defined -> use character SS, with 0.0 fallback if it fails
if secstatus is None:
try:
secstatus = self.character.secStatus
except (SystemExit, KeyboardInterrupt):
raise
except:
secstatus = 0
return max(low_limit, min(high_limit, secstatus))
def activeModulesIter(self): def activeModulesIter(self):
for mod in self.modules: for mod in self.modules:
if mod.state >= FittingModuleState.ACTIVE: if mod.state >= FittingModuleState.ACTIVE:
@@ -1824,6 +1842,7 @@ class Fit:
fitCopy.targetProfile = self.targetProfile fitCopy.targetProfile = self.targetProfile
fitCopy.implantLocation = self.implantLocation fitCopy.implantLocation = self.implantLocation
fitCopy.systemSecurity = self.systemSecurity fitCopy.systemSecurity = self.systemSecurity
fitCopy.pilotSecurity = self.pilotSecurity
fitCopy.notes = self.notes fitCopy.notes = self.notes
for i in self.modules: for i in self.modules:

View File

@@ -33,7 +33,7 @@ from eos.utils.cycles import CycleInfo, CycleSequence
from eos.utils.default import DEFAULT from eos.utils.default import DEFAULT
from eos.utils.float import floatUnerr from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import calculateSpoolup, resolveSpoolOptions from eos.utils.spoolSupport import calculateSpoolup, resolveSpoolOptions
from eos.utils.stats import DmgTypes, RRTypes from eos.utils.stats import BreacherInfo, DmgTypes, RRTypes
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -453,6 +453,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
return True return True
return False return False
@property
def isBreacher(self):
return self.charge and 'dotMissileLaunching' in self.charge.effects
def canDealDamage(self, ignoreState=False): def canDealDamage(self, ignoreState=False):
if self.isEmpty: if self.isEmpty:
return False return False
@@ -469,75 +473,77 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
def getVolleyParameters(self, spoolOptions=None, targetProfile=None, ignoreState=False): def getVolleyParameters(self, spoolOptions=None, targetProfile=None, ignoreState=False):
if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState): if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState):
return {0: DmgTypes(0, 0, 0, 0)} return {0: DmgTypes.default()}
if self.__baseVolley is None: if self.__baseVolley is None:
self.__baseVolley = {} self.__baseVolley = {}
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr if self.isBreacher:
dmgMult = self.getModifiedItemAttr("damageMultiplier", 1) dmgDelay = 1
# Some delay attributes have non-0 default value, so we have to pick according to effects subcycles = math.floor(self.getModifiedChargeAttr("dotDuration", 0) / 1000)
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(self.item.effects): breacher_info = BreacherInfo(
dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0) absolute=self.getModifiedChargeAttr("dotMaxDamagePerTick", 0),
elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT', 'debuffLance'}.intersection(self.item.effects): relative=self.getModifiedChargeAttr("dotMaxHPPercentagePerTick", 0) / 100)
dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0) for i in range(subcycles):
volley = DmgTypes.default()
volley.add_breacher(dmgDelay + i, breacher_info)
self.__baseVolley[dmgDelay + i] = volley
else: else:
dmgDelay = 0 dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0) dmgMult = self.getModifiedItemAttr("damageMultiplier", 1)
dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0) # Some delay attributes have non-0 default value, so we have to pick according to effects
# Reaper DD can damage each target only once if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar', 'lightningWeapon'}.intersection(self.item.effects):
if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects: dmgDelay = self.getModifiedItemAttr("damageDelayDuration", 0)
subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle)) elif {'doomsdayBeamDOT', 'doomsdaySlash', 'doomsdayConeDOT', 'debuffLance'}.intersection(self.item.effects):
else: dmgDelay = self.getModifiedItemAttr("doomsdayWarningDuration", 0)
subcycles = 1 else:
for i in range(subcycles): dmgDelay = 0
self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes( dmgDuration = self.getModifiedItemAttr("doomsdayDamageDuration", 0)
em=(dmgGetter("emDamage", 0)) * dmgMult, dmgSubcycle = self.getModifiedItemAttr("doomsdayDamageCycleTime", 0)
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult, # Reaper DD can damage each target only once
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult, if dmgDuration != 0 and dmgSubcycle != 0 and 'doomsdaySlash' not in self.item.effects:
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult) subcycles = math.floor(floatUnerr(dmgDuration / dmgSubcycle))
else:
subcycles = 1
for i in range(subcycles):
self.__baseVolley[dmgDelay + dmgSubcycle * i] = DmgTypes(
em=(dmgGetter("emDamage", 0)) * dmgMult,
thermal=(dmgGetter("thermalDamage", 0)) * dmgMult,
kinetic=(dmgGetter("kineticDamage", 0)) * dmgMult,
explosive=(dmgGetter("explosiveDamage", 0)) * dmgMult)
spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self) spoolType, spoolAmount = resolveSpoolOptions(spoolOptions, self)
spoolBoost = calculateSpoolup( spoolBoost = calculateSpoolup(
self.getModifiedItemAttr("damageMultiplierBonusMax", 0), self.getModifiedItemAttr("damageMultiplierBonusMax", 0),
self.getModifiedItemAttr("damageMultiplierBonusPerCycle", 0), self.getModifiedItemAttr("damageMultiplierBonusPerCycle", 0),
self.rawCycleTime / 1000, spoolType, spoolAmount)[0] self.rawCycleTime / 1000, spoolType, spoolAmount)[0]
spoolMultiplier = 1 + spoolBoost spoolMultiplier = 1 + spoolBoost
adjustedVolley = {} adjustedVolleys = {}
for volleyTime, volleyValue in self.__baseVolley.items(): for volleyTime, baseVolley in self.__baseVolley.items():
adjustedVolley[volleyTime] = DmgTypes( adjustedVolley = baseVolley * spoolMultiplier
em=volleyValue.em * spoolMultiplier * (1 - getattr(targetProfile, "emAmount", 0)), adjustedVolley.profile = targetProfile
thermal=volleyValue.thermal * spoolMultiplier * (1 - getattr(targetProfile, "thermalAmount", 0)), adjustedVolleys[volleyTime] = adjustedVolley
kinetic=volleyValue.kinetic * spoolMultiplier * (1 - getattr(targetProfile, "kineticAmount", 0)), return adjustedVolleys
explosive=volleyValue.explosive * spoolMultiplier * (1 - getattr(targetProfile, "explosiveAmount", 0)))
return adjustedVolley
def getVolley(self, spoolOptions=None, targetProfile=None, ignoreState=False): def getVolley(self, spoolOptions=None, targetProfile=None, ignoreState=False):
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState) volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
if len(volleyParams) == 0: if len(volleyParams) == 0:
return DmgTypes(0, 0, 0, 0) return DmgTypes.default()
return volleyParams[min(volleyParams)] return volleyParams[min(volleyParams)]
def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False, getSpreadDPS=False): def getDps(self, spoolOptions=None, targetProfile=None, ignoreState=False):
dmgDuringCycle = DmgTypes(0, 0, 0, 0) dps = DmgTypes.default()
cycleParams = self.getCycleParameters() cycleParams = self.getCycleParameters()
if cycleParams is None: if cycleParams is None:
return dmgDuringCycle return dps
volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState) volleyParams = self.getVolleyParameters(spoolOptions=spoolOptions, targetProfile=targetProfile, ignoreState=ignoreState)
avgCycleTime = cycleParams.averageTime avgCycleTime = cycleParams.averageTime
if len(volleyParams) == 0 or avgCycleTime == 0: if len(volleyParams) == 0 or avgCycleTime == 0:
return dmgDuringCycle
for volleyValue in volleyParams.values():
dmgDuringCycle += volleyValue
dpsFactor = 1 / (avgCycleTime / 1000)
dps = DmgTypes(
em=dmgDuringCycle.em * dpsFactor,
thermal=dmgDuringCycle.thermal * dpsFactor,
kinetic=dmgDuringCycle.kinetic * dpsFactor,
explosive=dmgDuringCycle.explosive * dpsFactor)
if not getSpreadDPS:
return dps return dps
return {'em':dmgDuringCycle.em * dpsFactor, if self.isBreacher:
'therm': dmgDuringCycle.thermal * dpsFactor, return volleyParams[min(volleyParams)]
'kin': dmgDuringCycle.kinetic * dpsFactor, for volleyValue in volleyParams.values():
'exp': dmgDuringCycle.explosive * dpsFactor} dps += volleyValue
dpsFactor = 1 / (avgCycleTime / 1000)
dps *= dpsFactor
return dps
def isRemoteRepping(self, ignoreState=False): def isRemoteRepping(self, ignoreState=False):
repParams = self.getRepAmountParameters(ignoreState=ignoreState) repParams = self.getRepAmountParameters(ignoreState=ignoreState)
@@ -949,6 +955,13 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
and ((gang and effect.isType("gang")) or not gang): and ((gang and effect.isType("gang")) or not gang):
effect.handler(fit, self, context, projectionRange, effect=effect) effect.handler(fit, self, context, projectionRange, effect=effect)
def getCycleParametersForDps(self, reloadOverride=None):
# Special hack for breachers, since those are DoT and work independently of gun cycle
if self.isBreacher:
return CycleInfo(activeTime=1000, inactiveTime=0, quantity=math.inf, isInactivityReload=False)
else:
return self.getCycleParameters(reloadOverride=reloadOverride)
def getCycleParameters(self, reloadOverride=None): def getCycleParameters(self, reloadOverride=None):
"""Copied from new eos as well""" """Copied from new eos as well"""
# Determine if we'll take into account reload time or not # Determine if we'll take into account reload time or not

View File

@@ -25,10 +25,11 @@ import time
class SsoCharacter: class SsoCharacter:
def __init__(self, charID, name, client, accessToken=None, refreshToken=None): def __init__(self, charID, name, client, server, accessToken=None, refreshToken=None):
self.characterID = charID self.characterID = charID
self.characterName = name self.characterName = name
self.client = client self.client = client
self.server = server
self.accessToken = accessToken self.accessToken = accessToken
self.refreshToken = refreshToken self.refreshToken = refreshToken
self.accessTokenExpires = None self.accessTokenExpires = None
@@ -37,6 +38,9 @@ class SsoCharacter:
def init(self): def init(self):
pass pass
@property
def characterDisplay(self):
return "{} [{}]".format(self.characterName, self.server)
def is_token_expired(self): def is_token_expired(self):
if self.accessTokenExpires is None: if self.accessTokenExpires is None:
return True return True

View File

@@ -254,7 +254,7 @@ class TargetProfile:
def init(self): def init(self):
self.builtin = False self.builtin = False
def update(self, emAmount=0, thermalAmount=0, kineticAmount=0, explosiveAmount=0, maxVelocity=None, signatureRadius=None, radius=None): def update(self, emAmount=0, thermalAmount=0, kineticAmount=0, explosiveAmount=0, maxVelocity=None, signatureRadius=None, radius=None, hp=None):
self.emAmount = emAmount self.emAmount = emAmount
self.thermalAmount = thermalAmount self.thermalAmount = thermalAmount
self.kineticAmount = kineticAmount self.kineticAmount = kineticAmount
@@ -262,6 +262,7 @@ class TargetProfile:
self._maxVelocity = maxVelocity self._maxVelocity = maxVelocity
self._signatureRadius = signatureRadius self._signatureRadius = signatureRadius
self._radius = radius self._radius = radius
self._hp = hp
@classmethod @classmethod
def getBuiltinList(cls): def getBuiltinList(cls):
@@ -331,6 +332,18 @@ class TargetProfile:
def radius(self, val): def radius(self, val):
self._radius = val self._radius = val
@property
def hp(self):
if self._hp is None or self._hp == -1:
return math.inf
return self._hp
@hp.setter
def hp(self, val):
if val is not None and math.isinf(val):
val = None
self._hp = val
@classmethod @classmethod
def importPatterns(cls, text): def importPatterns(cls, text):
lines = re.split('[\n\r]+', text) lines = re.split('[\n\r]+', text)

View File

@@ -18,6 +18,9 @@
# =============================================================================== # ===============================================================================
import math
from collections import defaultdict
from eos.utils.float import floatUnerr from eos.utils.float import floatUnerr
from utils.repr import makeReprStr from utils.repr import makeReprStr
@@ -26,15 +29,133 @@ def _t(x):
return x return x
class BreacherInfo:
def __init__(self, absolute, relative):
self.absolute = absolute
self.relative = relative
def __mul__(self, mul):
return type(self)(absolute=self.absolute * mul, relative=self.relative * mul)
def __imul__(self, mul):
if mul == 1:
return self
self.absolute *= mul
self.relative *= mul
return self
def __truediv__(self, div):
return type(self)(absolute=self.absolute / div, relative=self.relative / div)
class DmgTypes: class DmgTypes:
"""Container for damage data stats.""" """
Container for volley stats, which stores breacher pod data
in raw form, before application of it to target profile.
"""
def __init__(self, em, thermal, kinetic, explosive): def __init__(self, em, thermal, kinetic, explosive):
self.em = em self._em = em
self.thermal = thermal self._thermal = thermal
self.kinetic = kinetic self._kinetic = kinetic
self.explosive = explosive self._explosive = explosive
self._calcTotal() self._breachers = defaultdict(lambda: [])
self.__profile = None
# Cached data
self.__cached_em = None
self.__cached_thermal = None
self.__cached_kinetic = None
self.__cached_explosive = None
self.__cached_pure = None
self.__cached_total = None
@classmethod
def default(cls):
return cls(0, 0, 0, 0)
def _clear_cached(self):
self.__cached_em = None
self.__cached_thermal = None
self.__cached_kinetic = None
self.__cached_explosive = None
self.__cached_pure = None
self.__cached_total = None
def add_breacher(self, key, data):
self._breachers[key].append(data)
@property
def profile(self):
return self.__profile
@profile.setter
def profile(self, profile):
self.__profile = profile
self._clear_cached()
@property
def em(self):
if self.__cached_em is not None:
return self.__cached_em
dmg = self._em
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "emAmount", 0)
self.__cached_em = dmg
return dmg
@property
def thermal(self):
if self.__cached_thermal is not None:
return self.__cached_thermal
dmg = self._thermal
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "thermalAmount", 0)
self.__cached_thermal = dmg
return dmg
@property
def kinetic(self):
if self.__cached_kinetic is not None:
return self.__cached_kinetic
dmg = self._kinetic
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "kineticAmount", 0)
self.__cached_kinetic = dmg
return dmg
@property
def explosive(self):
if self.__cached_explosive is not None:
return self.__cached_explosive
dmg = self._explosive
if self.profile is not None:
dmg *= 1 - getattr(self.profile, "explosiveAmount", 0)
self.__cached_explosive = dmg
return dmg
@property
def pure(self):
if self.__cached_pure is not None:
return self.__cached_pure
if self.profile is None:
dmg = sum(
max((b.absolute for b in bs), default=0)
for bs in self._breachers.values())
else:
dmg = sum(
max((min(b.absolute, b.relative * getattr(self.profile, "hp", math.inf)) for b in bs), default=0)
for bs in self._breachers.values())
self.__cached_pure = dmg
return dmg
@property
def total(self):
if self.__cached_total is not None:
return self.__cached_total
dmg = self.em + self.thermal + self.kinetic + self.explosive + self.pure
self.__cached_total = dmg
return dmg
# Iterator is needed to support tuple-style unpacking # Iterator is needed to support tuple-style unpacking
def __iter__(self): def __iter__(self):
@@ -42,6 +163,7 @@ class DmgTypes:
yield self.thermal yield self.thermal
yield self.kinetic yield self.kinetic
yield self.explosive yield self.explosive
yield self.pure
yield self.total yield self.total
def __eq__(self, other): def __eq__(self, other):
@@ -50,77 +172,87 @@ class DmgTypes:
# Round for comparison's sake because often damage profiles are # Round for comparison's sake because often damage profiles are
# generated from data which includes float errors # generated from data which includes float errors
return ( return (
floatUnerr(self.em) == floatUnerr(other.em) and floatUnerr(self._em) == floatUnerr(other._em) and
floatUnerr(self.thermal) == floatUnerr(other.thermal) and floatUnerr(self._thermal) == floatUnerr(other._thermal) and
floatUnerr(self.kinetic) == floatUnerr(other.kinetic) and floatUnerr(self._kinetic) == floatUnerr(other._kinetic) and
floatUnerr(self.explosive) == floatUnerr(other.explosive) and floatUnerr(self._explosive) == floatUnerr(other._explosive) and
floatUnerr(self.total) == floatUnerr(other.total)) sorted(self._breachers) == sorted(other._breachers) and
self.profile == other.profile)
def __bool__(self):
return any((
self.em, self.thermal, self.kinetic,
self.explosive, self.total))
def _calcTotal(self):
self.total = self.em + self.thermal + self.kinetic + self.explosive
def __add__(self, other): def __add__(self, other):
return type(self)( new = type(self)(
em=self.em + other.em, em=self._em + other._em,
thermal=self.thermal + other.thermal, thermal=self._thermal + other._thermal,
kinetic=self.kinetic + other.kinetic, kinetic=self._kinetic + other._kinetic,
explosive=self.explosive + other.explosive) explosive=self._explosive + other._explosive)
new.profile = self.profile
for k, v in self._breachers.items():
new._breachers[k].extend(v)
for k, v in other._breachers.items():
new._breachers[k].extend(v)
return new
def __iadd__(self, other): def __iadd__(self, other):
self.em += other.em self._em += other._em
self.thermal += other.thermal self._thermal += other._thermal
self.kinetic += other.kinetic self._kinetic += other._kinetic
self.explosive += other.explosive self._explosive += other._explosive
self._calcTotal() for k, v in other._breachers.items():
self._breachers[k].extend(v)
self._clear_cached()
return self return self
def __mul__(self, mul): def __mul__(self, mul):
return type(self)( new = type(self)(
em=self.em * mul, em=self._em * mul,
thermal=self.thermal * mul, thermal=self._thermal * mul,
kinetic=self.kinetic * mul, kinetic=self._kinetic * mul,
explosive=self.explosive * mul) explosive=self._explosive * mul)
new.profile = self.profile
for k, v in self._breachers.items():
new._breachers[k] = [b * mul for b in v]
return new
def __imul__(self, mul): def __imul__(self, mul):
if mul == 1: if mul == 1:
return return self
self.em *= mul self._em *= mul
self.thermal *= mul self._thermal *= mul
self.kinetic *= mul self._kinetic *= mul
self.explosive *= mul self._explosive *= mul
self._calcTotal() for v in self._breachers.values():
for b in v:
b *= mul
self._clear_cached()
return self return self
def __truediv__(self, div): def __truediv__(self, div):
return type(self)( new = type(self)(
em=self.em / div, em=self._em / div,
thermal=self.thermal / div, thermal=self._thermal / div,
kinetic=self.kinetic / div, kinetic=self._kinetic / div,
explosive=self.explosive / div) explosive=self._explosive / div)
new.profile = self.profile
for k, v in self._breachers.items():
new._breachers[k] = [b / div for b in v]
return new
def __itruediv__(self, div): def __bool__(self):
if div == 1: return any((
return self._em, self._thermal, self._kinetic, self._explosive,
self.em /= div any(b.absolute or b.relative for b in self._breachers)))
self.thermal /= div
self.kinetic /= div
self.explosive /= div
self._calcTotal()
return self
def __repr__(self): def __repr__(self):
spec = DmgTypes.names() class_name = type(self).__name__
spec.append('total') return (f'<{class_name}(em={self._em}, thermal={self._thermal}, kinetic={self._kinetic}, '
return makeReprStr(self, spec) f'explosive={self._explosive}, breachers={len(self._breachers)})>')
@staticmethod @staticmethod
def names(short=None, postProcessor=None): def names(short=None, postProcessor=None, includePure=False):
value = [_t('em'), _t('th'), _t('kin'), _t('exp')] if short else [_t('em'), _t('thermal'), _t('kinetic'), _t('explosive')] value = [_t('em'), _t('th'), _t('kin'), _t('exp')] if short else [_t('em'), _t('thermal'), _t('kinetic'), _t('explosive')]
if includePure:
value += [_t('pure')]
if postProcessor: if postProcessor:
value = [postProcessor(x) for x in value] value = [postProcessor(x) for x in value]

View File

@@ -117,7 +117,7 @@ class TimeCache(FitDataCache):
pointData[timeStart] = (dps, volley) pointData[timeStart] = (dps, volley)
# Gap between items # Gap between items
elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart): elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart):
pointData[prevTimeEnd] = (DmgTypes(0, 0, 0, 0), DmgTypes(0, 0, 0, 0)) pointData[prevTimeEnd] = (DmgTypes.default(), DmgTypes.default())
pointData[timeStart] = (dps, volley) pointData[timeStart] = (dps, volley)
# Changed value # Changed value
elif dps != prevDps or volley != prevVolley: elif dps != prevDps or volley != prevVolley:
@@ -157,7 +157,7 @@ class TimeCache(FitDataCache):
def addDpsVolley(ddKey, addedTimeStart, addedTimeFinish, addedVolleys): def addDpsVolley(ddKey, addedTimeStart, addedTimeFinish, addedVolleys):
if not addedVolleys: if not addedVolleys:
return return
volleySum = sum(addedVolleys, DmgTypes(0, 0, 0, 0)) volleySum = sum(addedVolleys, DmgTypes.default())
if volleySum.total > 0: if volleySum.total > 0:
addedDps = volleySum / (addedTimeFinish - addedTimeStart) addedDps = volleySum / (addedTimeFinish - addedTimeStart)
# We can take "just best" volley, no matter target resistances, because all # We can take "just best" volley, no matter target resistances, because all
@@ -170,24 +170,38 @@ class TimeCache(FitDataCache):
def addDmg(ddKey, addedTime, addedDmg): def addDmg(ddKey, addedTime, addedDmg):
if addedDmg.total == 0: if addedDmg.total == 0:
return return
addedDmg._breachers = {addedTime + k: v for k, v in addedDmg._breachers.items()}
addedDmg._clear_cached()
intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg
# Modules # Modules
for mod in src.item.activeModulesIter(): for mod in src.item.activeModulesIter():
if not mod.isDealingDamage(): if not mod.isDealingDamage():
continue continue
cycleParams = mod.getCycleParameters(reloadOverride=True) cycleParams = mod.getCycleParametersForDps(reloadOverride=True)
if cycleParams is None: if cycleParams is None:
continue continue
currentTime = 0 currentTime = 0
nonstopCycles = 0 nonstopCycles = 0
isBreacher = mod.isBreacher
for cycleTimeMs, inactiveTimeMs, isInactivityReload in cycleParams.iterCycles(): for cycleTimeMs, inactiveTimeMs, isInactivityReload in cycleParams.iterCycles():
cycleVolleys = [] cycleVolleys = []
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True)) volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
for volleyTimeMs, volley in volleyParams.items(): for volleyTimeMs, volley in volleyParams.items():
cycleVolleys.append(volley) cycleVolleys.append(volley)
addDmg(mod, currentTime + volleyTimeMs / 1000, volley) time = currentTime + volleyTimeMs / 1000
addDpsVolley(mod, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys) if isBreacher:
time += 1
addDmg(mod, time, volley)
if isBreacher:
break
timeStart = currentTime
timeFinish = currentTime + cycleTimeMs / 1000
if isBreacher:
timeStart += 1
timeFinish += 1
addDpsVolley(mod, timeStart, timeFinish, cycleVolleys)
if inactiveTimeMs > 0: if inactiveTimeMs > 0:
nonstopCycles = 0 nonstopCycles = 0
else: else:

View File

@@ -98,6 +98,8 @@ def getApplicationPerKey(src, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAn
tgt=tgt, tgt=tgt,
distance=distance, distance=distance,
tgtSigRadius=tgtSigRadius) tgtSigRadius=tgtSigRadius)
elif mod.isBreacher:
applicationMap[mod] = getBreacherMult(mod=mod, distance=distance) if inLockRange else 0
for drone in src.item.activeDronesIter(): for drone in src.item.activeDronesIter():
if not drone.isDealingDamage(): if not drone.isDealingDamage():
continue continue
@@ -192,6 +194,21 @@ def getLauncherMult(mod, distance, tgtSpeed, tgtSigRadius):
return distanceFactor * applicationFactor return distanceFactor * applicationFactor
def getBreacherMult(mod, distance):
missileMaxRangeData = mod.missileMaxRangeData
if missileMaxRangeData is None:
return 0
# 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
return distanceFactor
def getSmartbombMult(mod, distance): def getSmartbombMult(mod, distance):
modRange = mod.maxRange modRange = mod.maxRange
if modRange is None: if modRange is None:

View File

@@ -19,6 +19,7 @@
import eos.config import eos.config
from eos.saveddata.targetProfile import TargetProfile
from eos.utils.spoolSupport import SpoolOptions, SpoolType from eos.utils.spoolSupport import SpoolOptions, SpoolType
from eos.utils.stats import DmgTypes from eos.utils.stats import DmgTypes
from graphs.data.base import PointGetter, SmoothPointGetter from graphs.data.base import PointGetter, SmoothPointGetter
@@ -27,17 +28,16 @@ from .calc.application import getApplicationPerKey
from .calc.projected import getScramRange, getScrammables, getTackledSpeed, getSigRadiusMult from .calc.projected import getScramRange, getScrammables, getTackledSpeed, getSigRadiusMult
def applyDamage(dmgMap, applicationMap, tgtResists): def applyDamage(dmgMap, applicationMap, tgtResists, tgtFullHp):
total = DmgTypes(em=0, thermal=0, kinetic=0, explosive=0) total = DmgTypes.default()
for key, dmg in dmgMap.items(): for key, dmg in dmgMap.items():
total += dmg * applicationMap.get(key, 0) total += dmg * applicationMap.get(key, 0)
if not GraphSettings.getInstance().get('ignoreResists'): if not GraphSettings.getInstance().get('ignoreResists'):
emRes, thermRes, kinRes, exploRes = tgtResists emRes, thermRes, kinRes, exploRes = tgtResists
total = DmgTypes( else:
em=total.em * (1 - emRes), emRes = thermRes = kinRes = exploRes = 0
thermal=total.thermal * (1 - thermRes), total.profile = TargetProfile(
kinetic=total.kinetic * (1 - kinRes), emAmount=emRes, thermalAmount=thermRes, kineticAmount=kinRes, explosiveAmount=exploRes, hp=tgtFullHp)
explosive=total.explosive * (1 - exploRes))
return total return total
@@ -144,7 +144,8 @@ class XDistanceMixin(SmoothPointGetter):
'srcScramRange': getScramRange(src=src) if applyProjected else None, 'srcScramRange': getScramRange(src=src) if applyProjected else None,
'tgtScrammables': getScrammables(tgt=tgt) if applyProjected else (), 'tgtScrammables': getScrammables(tgt=tgt) if applyProjected else (),
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']), 'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()} 'tgtResists': tgt.getResists(),
'tgtFullHp': tgt.getFullHp()}
def _calculatePoint(self, x, miscParams, src, tgt, commonData): def _calculatePoint(self, x, miscParams, src, tgt, commonData):
distance = x distance = x
@@ -186,7 +187,8 @@ class XDistanceMixin(SmoothPointGetter):
y = applyDamage( y = applyDamage(
dmgMap=commonData['dmgMap'], dmgMap=commonData['dmgMap'],
applicationMap=applicationMap, applicationMap=applicationMap,
tgtResists=commonData['tgtResists']).total tgtResists=commonData['tgtResists'],
tgtFullHp=commonData['tgtFullHp']).total
return y return y
@@ -241,14 +243,17 @@ class XTimeMixin(PointGetter):
self._prepareTimeCache(src=src, maxTime=maxTime) self._prepareTimeCache(src=src, maxTime=maxTime)
timeCache = self._getTimeCacheData(src=src) timeCache = self._getTimeCacheData(src=src)
applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt) applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt)
tgtResists = tgt.getResists()
# Custom iteration for time graph to show all data points # Custom iteration for time graph to show all data points
currentDmg = None currentDmg = None
currentTime = None currentTime = None
for currentTime in sorted(timeCache): for currentTime in sorted(timeCache):
prevDmg = currentDmg prevDmg = currentDmg
currentDmgData = timeCache[currentTime] currentDmgData = timeCache[currentTime]
currentDmg = applyDamage(dmgMap=currentDmgData, applicationMap=applicationMap, tgtResists=tgtResists).total currentDmg = applyDamage(
dmgMap=currentDmgData,
applicationMap=applicationMap,
tgtResists=tgt.getResists(),
tgtFullHp=tgt.getFullHp()).total
if currentTime < minTime: if currentTime < minTime:
continue continue
# First set of data points # First set of data points
@@ -294,7 +299,11 @@ class XTimeMixin(PointGetter):
self._prepareTimeCache(src=src, maxTime=time) self._prepareTimeCache(src=src, maxTime=time)
dmgData = self._getTimeCacheDataPoint(src=src, time=time) dmgData = self._getTimeCacheDataPoint(src=src, time=time)
applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt) applicationMap = self._prepareApplicationMap(miscParams=miscParams, src=src, tgt=tgt)
y = applyDamage(dmgMap=dmgData, applicationMap=applicationMap, tgtResists=tgt.getResists()).total y = applyDamage(
dmgMap=dmgData,
applicationMap=applicationMap,
tgtResists=tgt.getResists(),
tgtFullHp=tgt.getFullHp()).total
return y return y
@@ -310,7 +319,8 @@ class XTgtSpeedMixin(SmoothPointGetter):
return { return {
'applyProjected': GraphSettings.getInstance().get('applyProjected'), 'applyProjected': GraphSettings.getInstance().get('applyProjected'),
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']), 'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()} 'tgtResists': tgt.getResists(),
'tgtFullHp': tgt.getFullHp()}
def _calculatePoint(self, x, miscParams, src, tgt, commonData): def _calculatePoint(self, x, miscParams, src, tgt, commonData):
tgtSpeed = x tgtSpeed = x
@@ -353,7 +363,8 @@ class XTgtSpeedMixin(SmoothPointGetter):
y = applyDamage( y = applyDamage(
dmgMap=commonData['dmgMap'], dmgMap=commonData['dmgMap'],
applicationMap=applicationMap, applicationMap=applicationMap,
tgtResists=commonData['tgtResists']).total tgtResists=commonData['tgtResists'],
tgtFullHp=commonData['tgtFullHp']).total
return y return y
@@ -398,7 +409,8 @@ class XTgtSigRadiusMixin(SmoothPointGetter):
'tgtSpeed': tgtSpeed, 'tgtSpeed': tgtSpeed,
'tgtSigMult': tgtSigMult, 'tgtSigMult': tgtSigMult,
'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']), 'dmgMap': self._getDamagePerKey(src=src, time=miscParams['time']),
'tgtResists': tgt.getResists()} 'tgtResists': tgt.getResists(),
'tgtFullHp': tgt.getFullHp()}
def _calculatePoint(self, x, miscParams, src, tgt, commonData): def _calculatePoint(self, x, miscParams, src, tgt, commonData):
tgtSigRadius = x tgtSigRadius = x
@@ -414,7 +426,8 @@ class XTgtSigRadiusMixin(SmoothPointGetter):
y = applyDamage( y = applyDamage(
dmgMap=commonData['dmgMap'], dmgMap=commonData['dmgMap'],
applicationMap=applicationMap, applicationMap=applicationMap,
tgtResists=commonData['tgtResists']).total tgtResists=commonData['tgtResists'],
tgtFullHp=commonData['tgtFullHp']).total
return y return y

View File

@@ -89,7 +89,7 @@ class FitDamageStatsGraph(FitGraph):
cols = [] cols = []
if not GraphSettings.getInstance().get('ignoreResists'): if not GraphSettings.getInstance().get('ignoreResists'):
cols.append('Target Resists') cols.append('Target Resists')
cols.extend(('Speed', 'SigRadius', 'Radius')) cols.extend(('Speed', 'SigRadius', 'Radius', 'FullHP'))
return cols return cols
# Calculation stuff # Calculation stuff

View File

@@ -273,7 +273,7 @@ class GraphCanvasPanel(wx.Panel):
legendLines = [] legendLines = []
for i, iData in enumerate(legendData): for i, iData in enumerate(legendData):
color, lineStyle, label = iData color, lineStyle, label = iData
legendLines.append(Line2D([0], [0], color=color, linestyle=lineStyle, label=label.replace('$', '\$'))) legendLines.append(Line2D([0], [0], color=color, linestyle=lineStyle, label=label.replace('$', r'\$')))
if len(legendLines) > 0 and self.graphFrame.ctrlPanel.showLegend: if len(legendLines) > 0 and self.graphFrame.ctrlPanel.showLegend:
legend = self.subplot.legend(handles=legendLines) legend = self.subplot.legend(handles=legendLines)

View File

@@ -145,6 +145,11 @@ class TargetWrapper(BaseWrapper):
else: else:
return em, therm, kin, explo return em, therm, kin, explo
def getFullHp(self):
if self.isProfile:
return self.item.hp
if self.isFit:
return self.item.hp.get('shield', 0) + self.item.hp.get('armor', 0) + self.item.hp.get('hull', 0)
def _getShieldResists(ship): def _getShieldResists(ship):

View File

@@ -43,6 +43,10 @@ class BoosterViewDrop(wx.DropTarget):
if self.GetData(): if self.GetData():
dragged_data = DragDropHelper.data dragged_data = DragDropHelper.data
data = dragged_data.split(':') data = dragged_data.split(':')
if dragged_data is None:
return t
self.dropFn(x, y, data) self.dropFn(x, y, data)
return t return t

View File

@@ -41,6 +41,10 @@ class CargoViewDrop(wx.DropTarget):
def OnData(self, x, y, t): def OnData(self, x, y, t):
if self.GetData(): if self.GetData():
dragged_data = DragDropHelper.data dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':') data = dragged_data.split(':')
self.dropFn(x, y, data) self.dropFn(x, y, data)
return t return t

View File

@@ -56,6 +56,10 @@ class CommandViewDrop(wx.DropTarget):
def OnData(self, x, y, t): def OnData(self, x, y, t):
if self.GetData(): if self.GetData():
dragged_data = DragDropHelper.data dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':') data = dragged_data.split(':')
self.dropFn(x, y, data) self.dropFn(x, y, data)
return t return t

View File

@@ -52,6 +52,10 @@ class DroneViewDrop(wx.DropTarget):
def OnData(self, x, y, t): def OnData(self, x, y, t):
if self.GetData(): if self.GetData():
dragged_data = DragDropHelper.data dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':') data = dragged_data.split(':')
self.dropFn(x, y, data) self.dropFn(x, y, data)
return t return t
@@ -195,7 +199,11 @@ class DroneView(Display):
@staticmethod @staticmethod
def droneKey(drone): def droneKey(drone):
groupName = Market.getInstance().getMarketGroupByItem(drone.item).marketGroupName if drone.isMutated:
item = drone.baseItem
else:
item = drone.item
groupName = Market.getInstance().getMarketGroupByItem(item).marketGroupName
return (DRONE_ORDER.index(groupName), drone.isMutated, drone.fullName) return (DRONE_ORDER.index(groupName), drone.isMutated, drone.fullName)
def fitChanged(self, event): def fitChanged(self, event):

View File

@@ -34,7 +34,10 @@ from service.fit import Fit
from service.market import Market from service.market import Market
FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter') FIGHTER_ORDER = (
'Light Fighter', 'Structure Light Fighter',
'Heavy Fighter', 'Structure Heavy Fighter',
'Support Fighter', 'Structure Support Fighter')
_t = wx.GetTranslation _t = wx.GetTranslation
@@ -49,6 +52,10 @@ class FighterViewDrop(wx.DropTarget):
def OnData(self, x, y, t): def OnData(self, x, y, t):
if self.GetData(): if self.GetData():
dragged_data = DragDropHelper.data dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':') data = dragged_data.split(':')
self.dropFn(x, y, data) self.dropFn(x, y, data)
return t return t

View File

@@ -46,6 +46,10 @@ class ImplantViewDrop(wx.DropTarget):
def OnData(self, x, y, t): def OnData(self, x, y, t):
if self.GetData(): if self.GetData():
dragged_data = DragDropHelper.data dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':') data = dragged_data.split(':')
self.dropFn(x, y, data) self.dropFn(x, y, data)
return t return t

View File

@@ -65,6 +65,10 @@ class ProjectedViewDrop(wx.DropTarget):
def OnData(self, x, y, t): def OnData(self, x, y, t):
if self.GetData(): if self.GetData():
dragged_data = DragDropHelper.data dragged_data = DragDropHelper.data
if dragged_data is None:
return t
data = dragged_data.split(':') data = dragged_data.split(':')
self.dropFn(x, y, data) self.dropFn(x, y, data)
return t return t

View File

@@ -20,6 +20,7 @@ from gui.builtinContextMenus.targetProfile import editor
from gui.builtinContextMenus import itemStats from gui.builtinContextMenus import itemStats
from gui.builtinContextMenus import itemMarketJump from gui.builtinContextMenus import itemMarketJump
from gui.builtinContextMenus import fitSystemSecurity # Not really an item info but want to keep it here from gui.builtinContextMenus import fitSystemSecurity # Not really an item info but want to keep it here
from gui.builtinContextMenus import fitPilotSecurity # Not really an item info but want to keep it here
from gui.builtinContextMenus import shipJump from gui.builtinContextMenus import shipJump
# Generic item manipulations # Generic item manipulations
from gui.builtinContextMenus import itemRemove from gui.builtinContextMenus import itemRemove

View File

@@ -25,7 +25,7 @@ class AddToCargoAmmo(ContextMenuSingle):
return True return True
def getText(self, callingWindow, itmContext, mainItem): def getText(self, callingWindow, itmContext, mainItem):
if mainItem.marketGroup.name == "Scan Probes": if mainItem.marketGroup and mainItem.marketGroup.name == "Scan Probes":
return _t("Add {0} to Cargo (x8)").format(itmContext) return _t("Add {0} to Cargo (x8)").format(itmContext)
return _t("Add {0} to Cargo (x1000)").format(itmContext) return _t("Add {0} to Cargo (x1000)").format(itmContext)
@@ -34,7 +34,7 @@ class AddToCargoAmmo(ContextMenuSingle):
fitID = self.mainFrame.getActiveFit() fitID = self.mainFrame.getActiveFit()
typeID = int(mainItem.ID) typeID = int(mainItem.ID)
if mainItem.marketGroup.name == "Scan Probes": if mainItem.marketGroup and mainItem.marketGroup.name == "Scan Probes":
command = cmd.GuiAddCargoCommand(fitID=fitID, itemID=typeID, amount=8) command = cmd.GuiAddCargoCommand(fitID=fitID, itemID=typeID, amount=8)
else: else:
command = cmd.GuiAddCargoCommand(fitID=fitID, itemID=typeID, amount=1000) command = cmd.GuiAddCargoCommand(fitID=fitID, itemID=typeID, amount=1000)

View File

@@ -65,7 +65,6 @@ class DroneStackSplit(wx.Dialog):
self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER) self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
self.input.SetValue(str(value)) self.input.SetValue(str(value))
self.input.SelectAll()
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15) bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
@@ -75,12 +74,13 @@ class DroneStackSplit(wx.Dialog):
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND) bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10) bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
self.input.SetFocus()
self.input.Bind(wx.EVT_CHAR, self.onChar) self.input.Bind(wx.EVT_CHAR, self.onChar)
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter) self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1) self.SetSizer(bSizer1)
self.CenterOnParent()
self.Fit() self.Fit()
self.CenterOnParent()
self.input.SetFocus()
self.input.SelectAll()
def processEnter(self, evt): def processEnter(self, evt):
self.EndModal(wx.ID_OK) self.EndModal(wx.ID_OK)

View File

@@ -0,0 +1,157 @@
import re
import wx
import gui.fitCommands as cmd
import gui.mainFrame
from gui.contextMenu import ContextMenuUnconditional
from service.fit import Fit
_t = wx.GetTranslation
class FitPilotSecurityMenu(ContextMenuUnconditional):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, callingWindow, srcContext):
if srcContext != "fittingShip":
return False
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
if fit.ship.name not in ('Pacifier', 'Enforcer', 'Marshal', 'Sidewinder', 'Cobra', 'Python'):
return
return True
def getText(self, callingWindow, itmContext):
return _t("Pilot Security Status")
def addOption(self, menu, optionLabel, optionValue):
id = ContextMenuUnconditional.nextID()
self.optionIds[id] = optionValue
menuItem = wx.MenuItem(menu, id, optionLabel, kind=wx.ITEM_CHECK)
menu.Bind(wx.EVT_MENU, self.handleMode, menuItem)
return menuItem
def addOptionCustom(self, menu, optionLabel):
id = ContextMenuUnconditional.nextID()
menuItem = wx.MenuItem(menu, id, optionLabel, kind=wx.ITEM_CHECK)
menu.Bind(wx.EVT_MENU, self.handleModeCustom, menuItem)
return menuItem
def getSubMenu(self, callingWindow, context, rootMenu, i, pitem):
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
msw = True if "wxMSW" in wx.PlatformInfo else False
self.optionIds = {}
sub = wx.Menu()
presets = (-10, -8, -6, -4, -2, 0, 1, 2, 3, 4, 5)
# Inherit
char_sec_status = round(fit.character.secStatus, 2)
menuItem = self.addOption(rootMenu if msw else sub, _t('Character') + f' ({char_sec_status})', None)
sub.Append(menuItem)
menuItem.Check(fit.pilotSecurity is None)
# Custom
label = _t('Custom')
is_checked = False
if fit.pilotSecurity is not None and fit.pilotSecurity not in presets:
sec_status = round(fit.getPilotSecurity(), 2)
label += f' ({sec_status})'
is_checked = True
menuItem = self.addOptionCustom(rootMenu if msw else sub, label)
sub.Append(menuItem)
menuItem.Check(is_checked)
sub.AppendSeparator()
# Predefined options
for sec_status in presets:
menuItem = self.addOption(rootMenu if msw else sub, str(sec_status), sec_status)
sub.Append(menuItem)
menuItem.Check(fit.pilotSecurity == sec_status)
return sub
def handleMode(self, event):
optionValue = self.optionIds[event.Id]
self.mainFrame.command.Submit(cmd.GuiChangeFitPilotSecurityCommand(
fitID=self.mainFrame.getActiveFit(),
secStatus=optionValue))
def handleModeCustom(self, event):
fitID = self.mainFrame.getActiveFit()
fit = Fit.getInstance().getFit(fitID)
sec_status = fit.getPilotSecurity()
with SecStatusChanger(self.mainFrame, value=sec_status) as dlg:
if dlg.ShowModal() == wx.ID_OK:
cleanInput = re.sub(r'[^0-9.\-+]', '', dlg.input.GetLineText(0).strip())
if cleanInput:
try:
cleanInputFloat = float(cleanInput)
except ValueError:
return
else:
return
self.mainFrame.command.Submit(cmd.GuiChangeFitPilotSecurityCommand(
fitID=fitID, secStatus=max(-10.0, min(5.0, cleanInputFloat))))
FitPilotSecurityMenu.register()
class SecStatusChanger(wx.Dialog):
def __init__(self, parent, value):
super().__init__(parent, title=_t('Change Security Status'), 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, _t('Security Status (min -10.0, max 5.0):'))
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 = '0.0'
else:
if value == int(value):
value = int(value)
value = str(value)
self.input.SetValue(value)
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.Bind(wx.EVT_CHAR, self.onChar)
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1)
self.Fit()
self.CenterOnParent()
self.input.SetFocus()
self.input.SelectAll()
def processEnter(self, evt):
self.EndModal(wx.ID_OK)
# checks to make sure it's valid number
@staticmethod
def onChar(event):
key = event.GetKeyCode()
acceptable_characters = '1234567890.-+'
acceptable_keycode = [3, 22, 13, 8, 127] # modifiers like delete, copy, paste
if key in acceptable_keycode or key >= 255 or (key < 255 and chr(key) in acceptable_characters):
event.Skip()
return
else:
return False

View File

@@ -59,7 +59,6 @@ class NameDialog(wx.Dialog):
else: else:
value = str(value) value = str(value)
self.input.SetValue(value) self.input.SetValue(value)
self.input.SelectAll()
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15) bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
@@ -69,11 +68,12 @@ class NameDialog(wx.Dialog):
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND) bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10) bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
self.input.SetFocus()
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter) self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1) self.SetSizer(bSizer1)
self.CenterOnParent()
self.Fit() self.Fit()
self.CenterOnParent()
self.input.SetFocus()
self.input.SelectAll()
def processEnter(self, evt): def processEnter(self, evt):
self.EndModal(wx.ID_OK) self.EndModal(wx.ID_OK)

View File

@@ -108,7 +108,6 @@ class AmountChanger(wx.Dialog):
self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER) self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
self.input.SetValue(str(value)) self.input.SetValue(str(value))
self.input.SelectAll()
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15) bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
@@ -118,12 +117,13 @@ class AmountChanger(wx.Dialog):
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND) bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10) bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
self.input.SetFocus()
self.input.Bind(wx.EVT_CHAR, self.onChar) self.input.Bind(wx.EVT_CHAR, self.onChar)
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter) self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1) self.SetSizer(bSizer1)
self.CenterOnParent()
self.Fit() self.Fit()
self.CenterOnParent()
self.input.SetFocus()
self.input.SelectAll()
def processEnter(self, evt): def processEnter(self, evt):
self.EndModal(wx.ID_OK) self.EndModal(wx.ID_OK)

View File

@@ -13,6 +13,16 @@ from service.fit import Fit
_t = wx.GetTranslation _t = wx.GetTranslation
GLORIFIED_PREFIX = 'Gl. '
def nameSorter(mutaplasmid):
name = mutaplasmid.shortName
if name.startswith(GLORIFIED_PREFIX):
return name[len(GLORIFIED_PREFIX):], True
return name, False
class ChangeItemMutation(ContextMenuSingle): class ChangeItemMutation(ContextMenuSingle):
def __init__(self): def __init__(self):
@@ -45,7 +55,7 @@ class ChangeItemMutation(ContextMenuSingle):
menu = rootMenu if msw else sub menu = rootMenu if msw else sub
for mutaplasmid in mainItem.item.mutaplasmids: for mutaplasmid in sorted(mainItem.item.mutaplasmids, key=nameSorter):
id = ContextMenuSingle.nextID() id = ContextMenuSingle.nextID()
self.eventIDs[id] = (mutaplasmid, mainItem) self.eventIDs[id] = (mutaplasmid, mainItem)
mItem = wx.MenuItem(menu, id, mutaplasmid.shortName) mItem = wx.MenuItem(menu, id, mutaplasmid.shortName)

View File

@@ -94,7 +94,6 @@ class RangeChanger(wx.Dialog):
value = int(value) value = int(value)
value = str(value) value = str(value)
self.input.SetValue(value) self.input.SetValue(value)
self.input.SelectAll()
bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15) bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15)
@@ -104,12 +103,13 @@ class RangeChanger(wx.Dialog):
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND) bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10) bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10)
self.input.SetFocus()
self.input.Bind(wx.EVT_CHAR, self.onChar) self.input.Bind(wx.EVT_CHAR, self.onChar)
self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter) self.input.Bind(wx.EVT_TEXT_ENTER, self.processEnter)
self.SetSizer(bSizer1) self.SetSizer(bSizer1)
self.CenterOnParent()
self.Fit() self.Fit()
self.CenterOnParent()
self.input.SetFocus()
self.input.SelectAll()
def processEnter(self, evt): def processEnter(self, evt):
self.EndModal(wx.ID_OK) self.EndModal(wx.ID_OK)

View File

@@ -72,6 +72,7 @@ AttrGroupDict = {
"specialAmmoHoldCapacity", "specialAmmoHoldCapacity",
"specialCommandCenterHoldCapacity", "specialCommandCenterHoldCapacity",
"specialPlanetaryCommoditiesHoldCapacity", "specialPlanetaryCommoditiesHoldCapacity",
"specialColonyResourcesHoldCapacity",
"structureDamageLimit", "structureDamageLimit",
"specialSubsystemHoldCapacity", "specialSubsystemHoldCapacity",
"emDamageResonance", "emDamageResonance",

View File

@@ -22,9 +22,9 @@ class ItemDescription(wx.Panel):
desc = item.description.replace("\n", "<br>") desc = item.description.replace("\n", "<br>")
# Strip font tags # Strip font tags
desc = re.sub("<( *)font( *)color( *)=(.*?)>(?P<inside>.*?)<( *)/( *)font( *)>", "\g<inside>", desc) desc = re.sub("<( *)font( *)color( *)=(.*?)>(?P<inside>.*?)<( *)/( *)font( *)>", r"\g<inside>", desc)
# Strip URLs # Strip URLs
desc = re.sub("<( *)a(.*?)>(?P<inside>.*?)<( *)/( *)a( *)>", "\g<inside>", desc) desc = re.sub("<( *)a(.*?)>(?P<inside>.*?)<( *)/( *)a( *)>", r"\g<inside>", desc)
desc = "<body bgcolor='{}' text='{}'>{}</body>".format( desc = "<body bgcolor='{}' text='{}'>{}</body>".format(
bgcolor.GetAsString(wx.C2S_HTML_SYNTAX), bgcolor.GetAsString(wx.C2S_HTML_SYNTAX),
fgcolor.GetAsString(wx.C2S_HTML_SYNTAX), fgcolor.GetAsString(wx.C2S_HTML_SYNTAX),

View File

@@ -5,3 +5,5 @@ import wx.lib.newevent
ItemSelected, ITEM_SELECTED = wx.lib.newevent.NewEvent() ItemSelected, ITEM_SELECTED = wx.lib.newevent.NewEvent()
RECENTLY_USED_MODULES = -2 RECENTLY_USED_MODULES = -2
CHARGES_FOR_FIT = -3

View File

@@ -2,14 +2,17 @@ import wx
from logbook import Logger from logbook import Logger
import gui.builtinMarketBrowser.pfSearchBox as SBox import gui.builtinMarketBrowser.pfSearchBox as SBox
from config import slotColourMap import gui.globalEvents as GE
from config import slotColourMap, slotColourMapDark
from eos.saveddata.module import Module from eos.saveddata.module import Module
from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES, CHARGES_FOR_FIT
from gui.contextMenu import ContextMenu from gui.contextMenu import ContextMenu
from gui.display import Display from gui.display import Display
from gui.utils.staticHelpers import DragDropHelper from gui.utils.staticHelpers import DragDropHelper
from gui.utils.dark import isDark
from service.fit import Fit from service.fit import Fit
from service.market import Market from service.market import Market
from service.ammo import Ammo
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -31,6 +34,7 @@ class ItemView(Display):
self.filteredStore = set() self.filteredStore = set()
self.sMkt = marketBrowser.sMkt self.sMkt = marketBrowser.sMkt
self.sFit = Fit.getInstance() self.sFit = Fit.getInstance()
self.sAmmo = Ammo.getInstance()
self.marketBrowser = marketBrowser self.marketBrowser = marketBrowser
self.marketView = marketBrowser.marketView self.marketView = marketBrowser.marketView
@@ -50,6 +54,9 @@ class ItemView(Display):
self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.itemActivated) self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.itemActivated)
self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag) self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag)
# the "charges for active fitting" needs to listen to fitting changes
self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged)
self.active = [] self.active = []
def delaySearch(self, evt): def delaySearch(self, evt):
@@ -90,7 +97,11 @@ class ItemView(Display):
if sel.IsOk(): if sel.IsOk():
# Get data field of the selected item (which is a marketGroup ID if anything was selected) # Get data field of the selected item (which is a marketGroup ID if anything was selected)
seldata = self.marketView.GetItemData(sel) seldata = self.marketView.GetItemData(sel)
if seldata is not None and seldata != RECENTLY_USED_MODULES: if seldata == RECENTLY_USED_MODULES:
items = self.sMkt.getRecentlyUsed()
elif seldata == CHARGES_FOR_FIT:
items = self.getChargesForActiveFit()
elif seldata is not None:
# If market group treeview item doesn't have children (other market groups or dummies), # If market group treeview item doesn't have children (other market groups or dummies),
# then it should have items in it and we want to request them # then it should have items in it and we want to request them
if self.marketView.ItemHasChildren(sel) is False: if self.marketView.ItemHasChildren(sel) is False:
@@ -102,11 +113,7 @@ class ItemView(Display):
else: else:
items = set() items = set()
else: else:
# If method was called but selection wasn't actually made or we have a hit on recently used modules items = set()
if seldata == RECENTLY_USED_MODULES:
items = self.sMkt.getRecentlyUsed()
else:
items = set()
# Fill store # Fill store
self.updateItemStore(items) self.updateItemStore(items)
@@ -114,6 +121,9 @@ class ItemView(Display):
# Set toggle buttons / use search mode flag if recently used modules category is selected (in order to have all modules listed and not filtered) # Set toggle buttons / use search mode flag if recently used modules category is selected (in order to have all modules listed and not filtered)
if seldata == RECENTLY_USED_MODULES: if seldata == RECENTLY_USED_MODULES:
self.marketBrowser.mode = 'recent' self.marketBrowser.mode = 'recent'
if seldata == CHARGES_FOR_FIT:
self.marketBrowser.mode = 'charges'
self.setToggles() self.setToggles()
if context == 'tree' and self.marketBrowser.settings.get('marketMGMarketSelectMode') == 1: if context == 'tree' and self.marketBrowser.settings.get('marketMGMarketSelectMode') == 1:
@@ -122,6 +132,41 @@ class ItemView(Display):
btn.setUserSelection(True) btn.setUserSelection(True)
self.filterItemStore() self.filterItemStore()
def getChargesForActiveFit(self):
fitId = self.mainFrame.getActiveFit()
# no active fit => no charges
if fitId is None:
return set()
fit = self.sFit.getFit(fitId)
# use a set so we only add one entry for each charge
items = set()
for mod in fit.modules:
charges = self.sAmmo.getModuleFlatAmmo(mod)
for charge in charges:
items.add(charge)
return items
def fitChanged(self, event):
# skip the event so the other handlers also get called
event.Skip()
if self.marketBrowser.mode != 'charges':
return
activeFitID = self.mainFrame.getActiveFit()
# if it was not the active fitting that was changed, do not do anything
if activeFitID is not None and activeFitID not in event.fitIDs:
return
items = self.getChargesForActiveFit()
# update the UI
self.updateItemStore(items)
self.filterItemStore()
def updateItemStore(self, items): def updateItemStore(self, items):
self.unfilteredStore = items self.unfilteredStore = items
@@ -243,6 +288,7 @@ class ItemView(Display):
def columnBackground(self, colItem, item): def columnBackground(self, colItem, item):
if self.sFit.serviceFittingOptions["colorFitBySlot"]: if self.sFit.serviceFittingOptions["colorFitBySlot"]:
return slotColourMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour() colorMap = slotColourMapDark if isDark() else slotColourMap
return colorMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour()
else: else:
return self.GetBackgroundColour() return self.GetBackgroundColour()

View File

@@ -1,7 +1,7 @@
import wx import wx
from gui.cachingImageList import CachingImageList from gui.cachingImageList import CachingImageList
from gui.builtinMarketBrowser.events import RECENTLY_USED_MODULES from gui.builtinMarketBrowser.events import RECENTLY_USED_MODULES, CHARGES_FOR_FIT
from logbook import Logger from logbook import Logger
@@ -35,6 +35,9 @@ class MarketTree(wx.TreeCtrl):
# Add recently used modules node # Add recently used modules node
rumIconId = self.addImage("market_small", "gui") rumIconId = self.addImage("market_small", "gui")
self.AppendItem(self.root, _t("Recently Used Items"), rumIconId, data=RECENTLY_USED_MODULES) self.AppendItem(self.root, _t("Recently Used Items"), rumIconId, data=RECENTLY_USED_MODULES)
# Add charges for active fitting node
cffIconId = self.addImage("damagePattern_small", "gui")
self.AppendItem(self.root, _t("Charges For Active Fit"), cffIconId, data=CHARGES_FOR_FIT)
# Bind our lookup method to when the tree gets expanded # Bind our lookup method to when the tree gets expanded
self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.expandLookup) self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.expandLookup)

View File

@@ -1,9 +1,11 @@
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import wx import wx
import config
import gui.mainFrame import gui.mainFrame
from gui.bitmap_loader import BitmapLoader from gui.bitmap_loader import BitmapLoader
from gui.preferenceView import PreferenceView from gui.preferenceView import PreferenceView
from service.esi import Esi
from service.settings import EsiSettings from service.settings import EsiSettings
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
@@ -41,38 +43,68 @@ class PFEsiPref(PreferenceView):
"due to 'Signature has expired' error"))) "due to 'Signature has expired' error")))
mainSizer.Add(self.enforceJwtExpiration, 0, wx.ALL | wx.EXPAND, 5) mainSizer.Add(self.enforceJwtExpiration, 0, wx.ALL | wx.EXPAND, 5)
self.ssoServer = wx.CheckBox(panel, wx.ID_ANY, _t("Auto-login (starts local server)"), wx.DefaultPosition,
wx.DefaultSize,
0)
self.ssoServer.SetToolTip(wx.ToolTip(_t("This allows the EVE SSO to callback to your local pyfa instance and complete the authentication process without manual intervention.")))
mainSizer.Add(self.ssoServer, 0, wx.ALL | wx.EXPAND, 5)
rbSizer = wx.BoxSizer(wx.HORIZONTAL) rbSizer = wx.BoxSizer(wx.HORIZONTAL)
self.rbMode = wx.RadioBox(panel, -1, _t("Login Authentication Method"), wx.DefaultPosition, wx.DefaultSize,
[_t('Local Server'), _t('Manual')], 1, wx.RA_SPECIFY_COLS)
self.rbMode.SetItemToolTip(0, _t("This option starts a local webserver that EVE SSO Server will call back to"
" with information about the character login."))
self.rbMode.SetItemToolTip(1, _t("This option prompts users to copy and paste information to allow for"
" character login. Use this if having issues with the local server."))
self.rbMode.SetSelection(self.settings.get('loginMode')) self.enforceJwtExpiration.SetValue(self.settings.get("enforceJwtExpiration") or True)
self.enforceJwtExpiration.SetValue(self.settings.get("enforceJwtExpiration" or True)) self.ssoServer.SetValue(True if self.settings.get("loginMode") == 0 else False)
rbSizer.Add(self.rbMode, 1, wx.TOP | wx.RIGHT, 5) mainSizer.Add(rbSizer, 0, wx.ALL | wx.EXPAND, 0)
self.rbMode.Bind(wx.EVT_RADIOBOX, self.OnModeChange) esiSizer = wx.BoxSizer(wx.HORIZONTAL)
self.esiServer = wx.StaticText(panel, wx.ID_ANY, _t("Default SSO Server:"), wx.DefaultPosition, wx.DefaultSize, 0)
self.esiServer.Wrap(-1)
esiSizer.Add(self.esiServer, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
self.esiServer.SetToolTip(wx.ToolTip(_t('The source you choose will be used on connection.')))
self.chESIserver = wx.Choice(panel, choices=list(self.settings.keys()))
self.chESIserver.SetStringSelection(self.settings.get("server"))
esiSizer.Add(self.chESIserver, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 10)
mainSizer.Add(esiSizer, 0, wx.TOP | wx.RIGHT, 10)
self.chESIserver.Bind(wx.EVT_CHOICE, self.OnServerChange)
self.enforceJwtExpiration.Bind(wx.EVT_CHECKBOX, self.OnEnforceChange) self.enforceJwtExpiration.Bind(wx.EVT_CHECKBOX, self.OnEnforceChange)
mainSizer.Add(rbSizer, 1, wx.ALL | wx.EXPAND, 0) self.ssoServer.Bind(wx.EVT_CHECKBOX, self.OnModeChange)
panel.SetSizer(mainSizer) panel.SetSizer(mainSizer)
panel.Layout() panel.Layout()
def OnTimeoutChange(self, event): def OnTimeoutChange(self, event):
self.settings.set('timeout', event.GetEventObject().GetValue()) self.settings.set('timeout', event.GetEventObject().GetValue())
event.Skip()
def OnModeChange(self, event): def OnModeChange(self, event):
self.settings.set('loginMode', event.GetInt()) self.settings.set('loginMode', 0 if self.ssoServer.GetValue() else 1)
event.Skip()
def OnEnforceChange(self, event): def OnEnforceChange(self, event):
self.settings.set('enforceJwtExpiration', self.enforceJwtExpiration.GetValue()) self.settings.set('enforceJwtExpiration', self.enforceJwtExpiration.GetValue())
event.Skip() event.Skip()
def OnServerChange(self, event):
# pass
source = self.chESIserver.GetString(self.chESIserver.GetSelection())
esiService = Esi.getInstance()
# init servers
esiService.init(config.supported_servers[source])
self.settings.set("server", source)
event.Skip()
def getImage(self): def getImage(self):
return BitmapLoader.getBitmap("eve", "gui") return BitmapLoader.getBitmap("eve", "gui")
PFEsiPref.register() PFEsiPref.register()

View File

@@ -173,7 +173,7 @@ class FirepowerViewFull(StatsView):
if hasSpool: if hasSpool:
lines.append("") lines.append("")
lines.append(_t("Current") + ": {}".format(formatAmount(normal.total, prec, lowest, highest))) lines.append(_t("Current") + ": {}".format(formatAmount(normal.total, prec, lowest, highest)))
for dmgType in normal.names(): for dmgType in normal.names(includePure=True):
val = getattr(normal, dmgType, None) val = getattr(normal, dmgType, None)
if val: if val:
lines.append("{}{}: {}%".format( lines.append("{}{}: {}%".format(
@@ -215,13 +215,13 @@ class FirepowerViewFull(StatsView):
val = val() if fit is not None else None val = val() if fit is not None else None
preSpoolVal = preSpoolVal() if fit is not None else None preSpoolVal = preSpoolVal() if fit is not None else None
fullSpoolVal = fullSpoolVal() if fit is not None else None fullSpoolVal = fullSpoolVal() if fit is not None else None
if self._cachedValues[counter] != val: if self._cachedValues[counter] != getattr(val, 'total', None):
tooltipText = dpsToolTip(val, preSpoolVal, fullSpoolVal, prec, lowest, highest) tooltipText = dpsToolTip(val, preSpoolVal, fullSpoolVal, prec, lowest, highest)
label.SetLabel(valueFormat.format( label.SetLabel(valueFormat.format(
formatAmount(0 if val is None else val.total, prec, lowest, highest), formatAmount(0 if val is None else val.total, prec, lowest, highest),
"\u02e2" if hasSpoolUp(preSpoolVal, fullSpoolVal) else "")) "\u02e2" if hasSpoolUp(preSpoolVal, fullSpoolVal) else ""))
label.SetToolTip(wx.ToolTip(tooltipText)) label.SetToolTip(wx.ToolTip(tooltipText))
self._cachedValues[counter] = val self._cachedValues[counter] = getattr(val, 'total', None)
counter += 1 counter += 1
self.panel.Layout() self.panel.Layout()

View File

@@ -112,6 +112,7 @@ class TargetingMiscViewMinimal(StatsView):
cargoNamesOrder = OrderedDict(( cargoNamesOrder = OrderedDict((
("fleetHangarCapacity", _t("Fleet hangar")), ("fleetHangarCapacity", _t("Fleet hangar")),
("shipMaintenanceBayCapacity", _t("Maintenance bay")), ("shipMaintenanceBayCapacity", _t("Maintenance bay")),
("specialColonyResourcesHoldCapacity", _t("Infrastructure hold")),
("specialAmmoHoldCapacity", _t("Ammo hold")), ("specialAmmoHoldCapacity", _t("Ammo hold")),
("specialFuelBayCapacity", _t("Fuel bay")), ("specialFuelBayCapacity", _t("Fuel bay")),
("specialShipHoldCapacity", _t("Ship hold")), ("specialShipHoldCapacity", _t("Ship hold")),
@@ -134,6 +135,7 @@ class TargetingMiscViewMinimal(StatsView):
cargoValues = { cargoValues = {
"main": lambda: fit.ship.getModifiedItemAttr("capacity"), "main": lambda: fit.ship.getModifiedItemAttr("capacity"),
"fleetHangarCapacity": lambda: fit.ship.getModifiedItemAttr("fleetHangarCapacity"), "fleetHangarCapacity": lambda: fit.ship.getModifiedItemAttr("fleetHangarCapacity"),
"specialColonyResourcesHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialColonyResourcesHoldCapacity"),
"shipMaintenanceBayCapacity": lambda: fit.ship.getModifiedItemAttr("shipMaintenanceBayCapacity"), "shipMaintenanceBayCapacity": lambda: fit.ship.getModifiedItemAttr("shipMaintenanceBayCapacity"),
"specialAmmoHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialAmmoHoldCapacity"), "specialAmmoHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialAmmoHoldCapacity"),
"specialFuelBayCapacity": lambda: fit.ship.getModifiedItemAttr("specialFuelBayCapacity"), "specialFuelBayCapacity": lambda: fit.ship.getModifiedItemAttr("specialFuelBayCapacity"),

View File

@@ -197,6 +197,30 @@ class SignatureRadiusColumn(GraphColumn):
SignatureRadiusColumn.register() SignatureRadiusColumn.register()
class FullHpColumn(GraphColumn):
name = 'FullHP'
stickPrefixToValue = True
def __init__(self, fittingView, params):
super().__init__(fittingView, 68)
def _getValue(self, stuff):
if isinstance(stuff, Fit):
full_hp = stuff.hp.get('shield', 0) + stuff.hp.get('armor', 0) + stuff.hp.get('hull', 0)
elif isinstance(stuff, TargetProfile):
full_hp = stuff.hp
else:
full_hp = 0
return full_hp, 'hp'
def _getFitTooltip(self):
return 'Total raw HP'
FullHpColumn.register()
class ShieldAmountColumn(GraphColumn): class ShieldAmountColumn(GraphColumn):
name = 'ShieldAmount' name = 'ShieldAmount'

View File

@@ -93,8 +93,6 @@ class Miscellanea(ViewColumn):
text = "{} dmg".format(formatAmount(dmg, 3, 0, 6)) text = "{} dmg".format(formatAmount(dmg, 3, 0, 6))
tooltip = "Raw damage done" tooltip = "Raw damage done"
return text, tooltip return text, tooltip
pass
elif itemGroup in ("Energy Weapon", "Hybrid Weapon", "Projectile Weapon", "Combat Drone", "Fighter Drone"): elif itemGroup in ("Energy Weapon", "Hybrid Weapon", "Projectile Weapon", "Combat Drone", "Fighter Drone"):
trackingSpeed = stuff.getModifiedItemAttr("trackingSpeed") trackingSpeed = stuff.getModifiedItemAttr("trackingSpeed")
optimalSig = stuff.getModifiedItemAttr("optimalSigRadius") optimalSig = stuff.getModifiedItemAttr("optimalSigRadius")
@@ -590,7 +588,7 @@ class Miscellanea(ViewColumn):
text = "{0}/s".format(formatAmount(capPerSec, 3, 0, 3)) text = "{0}/s".format(formatAmount(capPerSec, 3, 0, 3))
tooltip = "Energy neutralization per second" tooltip = "Energy neutralization per second"
return text, tooltip return text, tooltip
elif itemGroup in ("Micro Jump Drive", "Micro Jump Field Generators"): elif itemGroup in ("Micro Jump Drive", "Micro Jump Field Generators", "Capital Mobility Modules"):
cycleTime = stuff.getModifiedItemAttr("duration") / 1000 cycleTime = stuff.getModifiedItemAttr("duration") / 1000
text = "{0}s".format(formatAmount(cycleTime, 3, 0, 3)) text = "{0}s".format(formatAmount(cycleTime, 3, 0, 3))
tooltip = "Spoolup time" tooltip = "Spoolup time"
@@ -810,6 +808,19 @@ class Miscellanea(ViewColumn):
text = "{}".format(formatAmount(scanStr, 4, 0, 3)) text = "{}".format(formatAmount(scanStr, 4, 0, 3))
tooltip = "Scan strength at {} AU scan range".format(formatAmount(baseRange, 3, 0, 0)) tooltip = "Scan strength at {} AU scan range".format(formatAmount(baseRange, 3, 0, 0))
return text, tooltip return text, tooltip
elif chargeGroup in ("SCARAB Breacher Pods",):
duration = stuff.getModifiedChargeAttr("dotDuration") / 1000
dmgAbs = stuff.getModifiedChargeAttr("dotMaxDamagePerTick")
dmgRel = stuff.getModifiedChargeAttr("dotMaxHPPercentagePerTick")
text = "{}/{}% over {}s".format(
formatAmount(dmgAbs * duration, 3, 0, 6),
formatAmount(dmgRel * duration, 3, 0, 6),
formatAmount(duration, 0, 0, 0))
fullDmgHp = dmgAbs / (dmgRel / 100)
tooltip = (
'Pure damage inflicted over time, minimum of absolute / relative\n'
'Full DPS from {} target HP').format(formatAmount(fullDmgHp, 3, 0, 6))
return text, tooltip
else: else:
return "", None return "", None
else: else:

View File

@@ -127,6 +127,10 @@ class FittingViewDrop(wx.DropTarget):
if self.GetData(): if self.GetData():
dragged_data = DragDropHelper.data dragged_data = DragDropHelper.data
# pyfalog.debug("fittingView: recieved drag: " + self.dropData.GetText()) # pyfalog.debug("fittingView: recieved drag: " + self.dropData.GetText())
if dragged_data is None:
return t
data = dragged_data.split(':') data = dragged_data.split(':')
self.dropFn(x, y, data) self.dropFn(x, y, data)
return t return t

View File

@@ -13,8 +13,8 @@ from service.market import Market
def stripHtml(text): def stripHtml(text):
text = re.sub('<\s*br\s*/?\s*>', '\n', text) text = re.sub(r'<\s*br\s*/?\s*>', '\n', text)
text = re.sub('</?[^/]+?(/\s*)?>', '', text) text = re.sub(r'</?[^/]+?(/\s*)?>', '', text)
return text return text

View File

@@ -115,7 +115,7 @@ class CharacterEntityEditor(EntityEditor):
sChar = Character.getInstance() sChar = Character.getInstance()
if entity.alphaCloneID: if entity.alphaCloneID:
trimmed_name = re.sub('[ \(\u03B1\)]+$', '', name) trimmed_name = re.sub('[ \\(\u03B1\\)]+$', '', name)
sChar.rename(entity, trimmed_name) sChar.rename(entity, trimmed_name)
else: else:
sChar.rename(entity, name) sChar.rename(entity, name)
@@ -807,7 +807,12 @@ class APIView(wx.Panel):
self.SetSizer(pmainSizer) self.SetSizer(pmainSizer)
self.Layout() self.Layout()
self.ssoListChanged(None) try:
self.ssoListChanged(None)
except (KeyboardInterrupt, SystemExit):
raise
except:
pass
def ssoCharChanged(self, event): def ssoCharChanged(self, event):
sChar = Character.getInstance() sChar = Character.getInstance()
@@ -859,7 +864,7 @@ class APIView(wx.Panel):
noneID = self.charChoice.Append(_t("None"), None) noneID = self.charChoice.Append(_t("None"), None)
for char in ssoChars: for char in ssoChars:
currId = self.charChoice.Append(char.characterName, char.ID) currId = self.charChoice.Append(char.characterDisplay, char.ID)
if sso is not None and char.ID == sso.ID: if sso is not None and char.ID == sso.ID:
self.charChoice.SetSelection(currId) self.charChoice.SetSelection(currId)
@@ -913,7 +918,7 @@ class SecStatusDialog(wx.Dialog):
self.m_staticText1.Wrap(-1) self.m_staticText1.Wrap(-1)
bSizer1.Add(self.m_staticText1, 1, wx.ALL | wx.EXPAND, 5) bSizer1.Add(self.m_staticText1, 1, wx.ALL | wx.EXPAND, 5)
self.floatSpin = FloatSpin(self, value=sec, min_val=-5.0, max_val=5.0, increment=0.1, digits=2, size=(-1, -1)) self.floatSpin = FloatSpin(self, value=sec, min_val=-10.0, max_val=5.0, increment=0.1, digits=2, size=(-1, -1))
bSizer1.Add(self.floatSpin, 0, wx.ALIGN_CENTER | wx.ALL, 5) bSizer1.Add(self.floatSpin, 0, wx.ALIGN_CENTER | wx.ALL, 5)
btnOk = wx.Button(self, wx.ID_OK) btnOk = wx.Button(self, wx.ID_OK)

View File

@@ -96,7 +96,7 @@ class EveFittings(AuxiliaryFrame):
self.charChoice.Clear() self.charChoice.Clear()
for char in chars: for char in chars:
self.charChoice.Append(char.characterName, char.ID) self.charChoice.Append(char.characterDisplay, char.ID)
if len(chars) > 0: if len(chars) > 0:
self.charChoice.SetSelection(0) self.charChoice.SetSelection(0)
@@ -227,21 +227,6 @@ class EveFittings(AuxiliaryFrame):
self.fitView.update([]) self.fitView.update([])
class ESIServerExceptionHandler:
def __init__(self, parentWindow, ex):
pyfalog.error(ex)
with wx.MessageDialog(
parentWindow,
_t("There was an issue starting up the localized server, try setting "
"Login Authentication Method to Manual by going to Preferences -> EVE SS0 -> "
"Login Authentication Method. If this doesn't fix the problem please file an "
"issue on Github."),
_t("Add Character Error"),
wx.OK | wx.ICON_ERROR
) as dlg:
dlg.ShowModal()
class ESIExceptionHandler: class ESIExceptionHandler:
# todo: make this a generate excetpion handler for all calls # todo: make this a generate excetpion handler for all calls
def __init__(self, ex): def __init__(self, ex):
@@ -348,7 +333,7 @@ class ExportToEve(AuxiliaryFrame):
self.charChoice.Clear() self.charChoice.Clear()
for char in chars: for char in chars:
self.charChoice.Append(char.characterName, char.ID) self.charChoice.Append(char.characterDisplay, char.ID)
if len(chars) > 0: if len(chars) > 0:
self.charChoice.SetSelection(0) self.charChoice.SetSelection(0)
@@ -434,6 +419,7 @@ class SsoCharacterMgmt(AuxiliaryFrame):
self.lcCharacters.InsertColumn(0, heading=_t('Character')) self.lcCharacters.InsertColumn(0, heading=_t('Character'))
self.lcCharacters.InsertColumn(1, heading=_t('Character ID')) self.lcCharacters.InsertColumn(1, heading=_t('Character ID'))
self.lcCharacters.InsertColumn(2, heading=_t('Server'))
self.popCharList() self.popCharList()
@@ -496,9 +482,11 @@ class SsoCharacterMgmt(AuxiliaryFrame):
self.lcCharacters.InsertItem(index, char.characterName) self.lcCharacters.InsertItem(index, char.characterName)
self.lcCharacters.SetItem(index, 1, str(char.characterID)) self.lcCharacters.SetItem(index, 1, str(char.characterID))
self.lcCharacters.SetItemData(index, char.ID) self.lcCharacters.SetItemData(index, char.ID)
self.lcCharacters.SetItem(index, 2, char.server or "<unknown>")
self.lcCharacters.SetColumnWidth(0, wx.LIST_AUTOSIZE) self.lcCharacters.SetColumnWidth(0, wx.LIST_AUTOSIZE)
self.lcCharacters.SetColumnWidth(1, wx.LIST_AUTOSIZE) self.lcCharacters.SetColumnWidth(1, wx.LIST_AUTOSIZE)
self.lcCharacters.SetColumnWidth(2, wx.LIST_AUTOSIZE)
def addChar(self, event): def addChar(self, event):
try: try:
@@ -506,8 +494,6 @@ class SsoCharacterMgmt(AuxiliaryFrame):
sEsi.login() sEsi.login()
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
raise raise
except Exception as ex:
ESIServerExceptionHandler(self, ex)
def delChar(self, event): def delChar(self, event):
item = self.lcCharacters.GetFirstSelected() item = self.lcCharacters.GetFirstSelected()

View File

@@ -112,7 +112,7 @@ class FitBrowserLiteDialog(wx.Dialog):
return True return True
matches = [] matches = []
searchTokens = [t.lower() for t in re.split('\s+', searchPattern)] searchTokens = [t.lower() for t in re.split(r'\s+', searchPattern)]
for fit in self.allFits: for fit in self.allFits:
if isMatch(fit, searchTokens): if isMatch(fit, searchTokens):
matches.append(fit) matches.append(fit)

View File

@@ -12,6 +12,7 @@ from .gui.cargo.remove import GuiRemoveCargosCommand
from .gui.commandFit.add import GuiAddCommandFitsCommand from .gui.commandFit.add import GuiAddCommandFitsCommand
from .gui.commandFit.remove import GuiRemoveCommandFitsCommand from .gui.commandFit.remove import GuiRemoveCommandFitsCommand
from .gui.commandFit.toggleStates import GuiToggleCommandFitStatesCommand from .gui.commandFit.toggleStates import GuiToggleCommandFitStatesCommand
from .gui.fitPilotSecurity import GuiChangeFitPilotSecurityCommand
from .gui.fitRename import GuiRenameFitCommand from .gui.fitRename import GuiRenameFitCommand
from .gui.fitRestrictionToggle import GuiToggleFittingRestrictionsCommand from .gui.fitRestrictionToggle import GuiToggleFittingRestrictionsCommand
from .gui.fitSystemSecurity import GuiChangeFitSystemSecurityCommand from .gui.fitSystemSecurity import GuiChangeFitSystemSecurityCommand

View File

@@ -0,0 +1,32 @@
import wx
from logbook import Logger
from service.fit import Fit
pyfalog = Logger(__name__)
class CalcChangeFitPilotSecurityCommand(wx.Command):
def __init__(self, fitID, secStatus):
wx.Command.__init__(self, True, 'Change Fit Pilot Security')
self.fitID = fitID
self.secStatus = secStatus
self.savedSecStatus = None
def Do(self):
pyfalog.debug('Doing changing pilot security status of fit {} to {}'.format(self.fitID, self.secStatus))
fit = Fit.getInstance().getFit(self.fitID, basic=True)
# Fetching status via getter and then saving 'raw' security status
# is intentional, to restore pre-change state properly
if fit.pilotSecurity == self.secStatus:
return False
self.savedSecStatus = fit.pilotSecurity
fit.pilotSecurity = self.secStatus
return True
def Undo(self):
pyfalog.debug('Undoing changing pilot security status of fit {} to {}'.format(self.fitID, self.secStatus))
cmd = CalcChangeFitPilotSecurityCommand(fitID=self.fitID, secStatus=self.savedSecStatus)
return cmd.Do()

View File

@@ -0,0 +1,36 @@
import wx
from service.fit import Fit
import eos.db
import gui.mainFrame
from gui import globalEvents as GE
from gui.fitCommands.helpers import InternalCommandHistory
from gui.fitCommands.calc.fitPilotSecurity import CalcChangeFitPilotSecurityCommand
class GuiChangeFitPilotSecurityCommand(wx.Command):
def __init__(self, fitID, secStatus):
wx.Command.__init__(self, True, 'Change Fit Pilot Security')
self.internalHistory = InternalCommandHistory()
self.fitID = fitID
self.secStatus = secStatus
def Do(self):
cmd = CalcChangeFitPilotSecurityCommand(fitID=self.fitID, secStatus=self.secStatus)
success = self.internalHistory.submit(cmd)
eos.db.flush()
sFit = Fit.getInstance()
sFit.recalc(self.fitID)
eos.db.commit()
wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,)))
return success
def Undo(self):
success = self.internalHistory.undoAll()
eos.db.flush()
sFit = Fit.getInstance()
sFit.recalc(self.fitID)
eos.db.commit()
wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,)))
return success

View File

@@ -61,10 +61,11 @@ from gui.statsPane import StatsPane
from gui.targetProfileEditor import TargetProfileEditor from gui.targetProfileEditor import TargetProfileEditor
from gui.updateDialog import UpdateDialog from gui.updateDialog import UpdateDialog
from gui.utils.clipboard import fromClipboard from gui.utils.clipboard import fromClipboard
from gui.utils.progressHelper import ProgressHelper
from service.character import Character from service.character import Character
from service.esi import Esi from service.esi import Esi
from service.fit import Fit from service.fit import Fit
from service.port import IPortUser, Port from service.port import Port
from service.price import Price from service.price import Price
from service.settings import HTMLExportSettings, SettingsProvider from service.settings import HTMLExportSettings, SettingsProvider
from service.update import Update from service.update import Update
@@ -130,7 +131,6 @@ class OpenFitsThread(threading.Thread):
self.running = False self.running = False
# todo: include IPortUser again
class MainFrame(wx.Frame): class MainFrame(wx.Frame):
__instance = None __instance = None
@@ -845,14 +845,15 @@ class MainFrame(wx.Frame):
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
) as dlg: ) as dlg:
if dlg.ShowModal() == wx.ID_OK: if dlg.ShowModal() == wx.ID_OK:
self.progressDialog = wx.ProgressDialog( # set some arbitrary spacing to create width in window
_t("Importing fits"), progress = ProgressHelper(message=" " * 100, callback=self._openAfterImport)
" " * 100, # set some arbitrary spacing to create width in window call = (Port.importFitsThreaded, [dlg.GetPaths(), progress], {})
parent=self, self.handleProgress(
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL title=_t("Importing fits"),
) style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_APP_MODAL | wx.PD_AUTO_HIDE,
Port.importFitsThreaded(dlg.GetPaths(), self) call=call,
self.progressDialog.ShowModal() progress=progress,
errMsgLbl=_t("Import Error"))
def backupToXml(self, event): def backupToXml(self, event):
""" Back up all fits to EVE XML file """ """ Back up all fits to EVE XML file """
@@ -863,32 +864,30 @@ class MainFrame(wx.Frame):
_t("Save Backup As..."), _t("Save Backup As..."),
wildcard=_t("EVE XML fitting file") + " (*.xml)|*.xml", wildcard=_t("EVE XML fitting file") + " (*.xml)|*.xml",
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
defaultFile=defaultFile, defaultFile=defaultFile) as fileDlg:
) as dlg: if fileDlg.ShowModal() == wx.ID_OK:
if dlg.ShowModal() == wx.ID_OK: filePath = fileDlg.GetPath()
filePath = dlg.GetPath()
if '.' not in os.path.basename(filePath): if '.' not in os.path.basename(filePath):
filePath += ".xml" filePath += ".xml"
sFit = Fit.getInstance() fitAmount = Fit.getInstance().countAllFits()
max_ = sFit.countAllFits() progress = ProgressHelper(
message=_t("Backing up {} fits to: {}").format(fitAmount, filePath),
self.progressDialog = wx.ProgressDialog( maximum=fitAmount + 1)
_t("Backup fits"), call = (Port.backupFits, [filePath, progress], {})
_t("Backing up {} fits to: {}").format(max_, filePath), self.handleProgress(
maximum=max_, title=_t("Backup fits"),
parent=self, style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL | wx.PD_AUTO_HIDE,
style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL call=call,
) progress=progress,
Port.backupFits(filePath, self) errMsgLbl=_t("Export Error"))
self.progressDialog.ShowModal()
def exportHtml(self, event): def exportHtml(self, event):
from gui.utils.exportHtml import exportHtml from gui.utils.exportHtml import exportHtml
sFit = Fit.getInstance() sFit = Fit.getInstance()
settings = HTMLExportSettings.getInstance() settings = HTMLExportSettings.getInstance()
max_ = sFit.countAllFits()
path = settings.getPath() path = settings.getPath()
if not os.path.isdir(os.path.dirname(path)): if not os.path.isdir(os.path.dirname(path)):
@@ -903,82 +902,44 @@ class MainFrame(wx.Frame):
) as dlg: ) as dlg:
if dlg.ShowModal() == wx.ID_OK: if dlg.ShowModal() == wx.ID_OK:
return return
progress = ProgressHelper(
message=_t("Generating HTML file at: {}").format(path),
maximum=sFit.countAllFits() + 1)
call = (exportHtml.getInstance().refreshFittingHtml, [True, progress], {})
self.handleProgress(
title=_t("Backup fits"),
style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME,
call=call,
progress=progress)
self.progressDialog = wx.ProgressDialog( def handleProgress(self, title, style, call, progress, errMsgLbl=None):
_t("Backup fits"), extraArgs = {}
_t("Generating HTML file at: {}").format(path), if progress.maximum is not None:
maximum=max_, parent=self, extraArgs['maximum'] = progress.maximum
style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME) with wx.ProgressDialog(
parent=self,
exportHtml.getInstance().refreshFittingHtml(True, self.backupCallback) title=title,
self.progressDialog.ShowModal() message=progress.message,
style=style,
def backupCallback(self, info): **extraArgs
if info == -1: ) as dlg:
self.closeProgressDialog() func, args, kwargs = call
else: func(*args, **kwargs)
self.progressDialog.Update(info) while progress.working:
wx.MilliSleep(250)
def on_port_process_start(self): wx.Yield()
# flag for progress dialog. (progress.dlgWorking, skip) = dlg.Update(progress.current, progress.message)
self.__progress_flag = True if progress.error and errMsgLbl:
def on_port_processing(self, action, data=None):
# 2017/03/29 NOTE: implementation like interface
wx.CallAfter(
self._on_port_processing, action, data
)
return self.__progress_flag
def _on_port_processing(self, action, data):
"""
While importing fits from file, the logic calls back to this function to
update progress bar to show activity. XML files can contain multiple
ships with multiple fits, whereas EFT cfg files contain many fits of
a single ship. When iterating through the files, we update the message
when we start a new file, and then Pulse the progress bar with every fit
that is processed.
action : a flag that lets us know how to deal with :data
None: Pulse the progress bar
1: Replace message with data
other: Close dialog and handle based on :action (-1 open fits, -2 display error)
"""
_message = None
if action & IPortUser.ID_ERROR:
self.closeProgressDialog()
_message = _t("Import Error") if action & IPortUser.PROCESS_IMPORT else _t("Export Error")
with wx.MessageDialog( with wx.MessageDialog(
self, self,
_t("The following error was generated") + _t("The following error was generated") +
f"\n\n{data}\n\n" + f"\n\n{progress.error}\n\n" +
_t("Be aware that already processed fits were not saved"), _t("Be aware that already processed fits were not saved"),
_message, wx.OK | wx.ICON_ERROR errMsgLbl, wx.OK | wx.ICON_ERROR
) as dlg: ) as dlg:
dlg.ShowModal() dlg.ShowModal()
return elif progress.callback:
progress.callback(*progress.cbArgs)
# data is str
if action & IPortUser.PROCESS_IMPORT:
if action & IPortUser.ID_PULSE:
_message = ()
# update message
elif action & IPortUser.ID_UPDATE: # and data != self.progressDialog.message:
_message = data
if _message is not None:
self.__progress_flag, _unuse = self.progressDialog.Pulse(_message)
else:
self.closeProgressDialog()
if action & IPortUser.ID_DONE:
self._openAfterImport(data)
# data is tuple(int, str)
elif action & IPortUser.PROCESS_EXPORT:
if action & IPortUser.ID_DONE:
self.closeProgressDialog()
else:
self.__progress_flag, _unuse = self.progressDialog.Update(data[0], data[1])
def _openAfterImport(self, fits): def _openAfterImport(self, fits):
if len(fits) > 0: if len(fits) > 0:
@@ -988,6 +949,8 @@ class MainFrame(wx.Frame):
wx.PostEvent(self.shipBrowser, Stage3Selected(shipID=fit.shipID, back=True)) wx.PostEvent(self.shipBrowser, Stage3Selected(shipID=fit.shipID, back=True))
else: else:
fits.sort(key=lambda _fit: (_fit.ship.item.name, _fit.name)) fits.sort(key=lambda _fit: (_fit.ship.item.name, _fit.name))
# Show 100 fits max
fits = fits[:100]
results = [] results = []
for fit in fits: for fit in fits:
results.append(( results.append((
@@ -999,15 +962,6 @@ class MainFrame(wx.Frame):
)) ))
wx.PostEvent(self.shipBrowser, ImportSelected(fits=results, back=True)) wx.PostEvent(self.shipBrowser, ImportSelected(fits=results, back=True))
def closeProgressDialog(self):
# Windows apparently handles ProgressDialogs differently. We can
# simply Destroy it here, but for other platforms we must Close it
if 'wxMSW' in wx.PlatformInfo:
self.progressDialog.Destroy()
else:
self.progressDialog.EndModal(wx.ID_OK)
self.progressDialog.Close()
def importCharacter(self, event): def importCharacter(self, event):
""" Imports character XML file from EVE API """ """ Imports character XML file from EVE API """
with wx.FileDialog( with wx.FileDialog(

View File

@@ -146,7 +146,7 @@ class MarketBrowser(wx.Panel):
setting = self.settings.get('marketMGSearchMode') setting = self.settings.get('marketMGSearchMode')
# We turn on all meta buttons for the duration of search/recents # We turn on all meta buttons for the duration of search/recents
if setting == 1: if setting == 1:
if newMode in ('search', 'recent'): if newMode in ('search', 'recent', 'charges'):
for btn in self.metaButtons: for btn in self.metaButtons:
btn.setUserSelection(True) btn.setUserSelection(True)
if newMode == 'normal': if newMode == 'normal':

View File

@@ -191,7 +191,7 @@ class ShipBrowser(wx.Panel):
"amarr", "caldari", "gallente", "minmatar", "amarr", "caldari", "gallente", "minmatar",
"sisters", "ore", "concord", "sisters", "ore", "concord",
"serpentis", "angel", "blood", "sansha", "guristas", "mordu", "serpentis", "angel", "blood", "sansha", "guristas", "mordu",
"jove", "triglavian", "upwell", None "deathless", "jove", "triglavian", "upwell", None
] ]
def raceNameKey(self, ship): def raceNameKey(self, ship):

View File

@@ -2,30 +2,48 @@ import wx
import gui.mainFrame import gui.mainFrame
import webbrowser import webbrowser
import gui.globalEvents as GE import gui.globalEvents as GE
import config
import time
from service.settings import EsiSettings
_t = wx.GetTranslation _t = wx.GetTranslation
class SsoLogin(wx.Dialog): class SsoLogin(wx.Dialog):
def __init__(self): def __init__(self, server: config.ApiServer, start_local_server=True):
mainFrame = gui.mainFrame.MainFrame.getInstance() self.mainFrame = gui.mainFrame.MainFrame.getInstance()
from service.esi import Esi
super().__init__( super().__init__(
mainFrame, id=wx.ID_ANY, title=_t("SSO Login"), style=wx.DEFAULT_DIALOG_STYLE, self.mainFrame, id=wx.ID_ANY, title=_t("SSO Login"), style=wx.DEFAULT_DIALOG_STYLE,
size=wx.Size(450, 240) if "wxGTK" in wx.PlatformInfo else wx.Size(400, 240)) size=wx.Size(450, 240) if "wxGTK" in wx.PlatformInfo else wx.Size(400, 240))
bSizer1 = wx.BoxSizer(wx.VERTICAL) bSizer1 = wx.BoxSizer(wx.VERTICAL)
text = wx.StaticText(self, wx.ID_ANY, _t("Copy and paste the block of text provided by pyfa.io")) if start_local_server:
bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10) text = wx.StaticText(self, wx.ID_ANY, _t("Waiting for character login through EVE Single Sign-On."))
bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10)
bSizer1.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.EXPAND, 15)
text = wx.StaticText(self, wx.ID_ANY, _t("If auto-login fails, copy and paste the token provided by pyfa.io"))
bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10)
elif server.name == "Serenity":
text = wx.StaticText(self, wx.ID_ANY, _t("Please copy and paste the url when your authorization is completed"))
bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10)
else:
text = wx.StaticText(self, wx.ID_ANY, _t("Please copy and paste the token provided by pyfa.io"))
bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10)
self.ssoInfoCtrl = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, (-1, -1), style=wx.TE_MULTILINE) self.ssoInfoCtrl = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, (-1, -1), style=wx.TE_MULTILINE)
self.ssoInfoCtrl.SetFont(wx.Font(8, wx.FONTFAMILY_TELETYPE, wx.NORMAL, wx.NORMAL)) self.ssoInfoCtrl.SetFont(wx.Font(8, wx.FONTFAMILY_TELETYPE, wx.NORMAL, wx.NORMAL))
self.ssoInfoCtrl.Layout() self.ssoInfoCtrl.Layout()
self.ssoInfoCtrl.Bind(wx.EVT_TEXT, self.OnTextEnter)
bSizer1.Add(self.ssoInfoCtrl, 1, wx.LEFT | wx.RIGHT | wx.EXPAND, 10) bSizer1.Add(self.ssoInfoCtrl, 1, wx.LEFT | wx.RIGHT | wx.EXPAND, 10)
self.Esisettings = EsiSettings.getInstance()
bSizer3 = wx.BoxSizer(wx.VERTICAL) bSizer3 = wx.BoxSizer(wx.VERTICAL)
bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 10) bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 10)
@@ -34,51 +52,43 @@ class SsoLogin(wx.Dialog):
self.SetSizer(bSizer1) self.SetSizer(bSizer1)
self.Center() self.Center()
from service.esi import Esi
self.sEsi = Esi.getInstance() self.sEsi = Esi.getInstance()
uri = self.sEsi.get_login_uri(None)
webbrowser.open(uri)
class SsoLoginServer(wx.Dialog):
def __init__(self, port):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
super().__init__(self.mainFrame, id=wx.ID_ANY, title=_t("SSO Login"), size=(-1, -1), style=wx.DEFAULT_DIALOG_STYLE)
from service.esi import Esi
self.sEsi = Esi.getInstance()
serverAddr = self.sEsi.startServer(port)
serverAddr = self.sEsi.startServer(0) if start_local_server else None
uri = self.sEsi.get_login_uri(serverAddr) uri = self.sEsi.get_login_uri(serverAddr)
bSizer1 = wx.BoxSizer(wx.VERTICAL) if server.name == "Serenity":
self.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.OnLogin) webbrowser.open(config.SSO_LOGOFF_SERENITY)
self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) time.sleep(1)
text = wx.StaticText(self, wx.ID_ANY, _t("Waiting for character login through EVE Single Sign-On.")) self.okBtn = self.FindWindow(wx.ID_OK)
bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10) self.okBtn.Enable(False)
# Ensure we clean up once they hit the "OK" button
bSizer3 = wx.BoxSizer(wx.VERTICAL) self.okBtn.Bind(wx.EVT_BUTTON, self.OnDestroy)
bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 10)
bSizer3.Add(self.CreateStdDialogButtonSizer(wx.CANCEL), 0, wx.EXPAND)
bSizer1.Add(bSizer3, 0, wx.BOTTOM | wx.RIGHT | wx.LEFT | wx.EXPAND, 10)
self.SetSizer(bSizer1)
self.Fit()
self.Center()
webbrowser.open(uri) webbrowser.open(uri)
self.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.OnLogin)
# Ensure we clean up if ESC is pressed
self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
def OnTextEnter(self, event):
t = event.String.strip()
if t == "":
self.okBtn.Enable(False)
else:
self.okBtn.Enable(True)
event.Skip()
def OnLogin(self, event): def OnLogin(self, event):
self.EndModal(wx.ID_OK) # This would normally happen if it was logged in via server auto-login. In this case, the modal is done, we effectively want to cancel out
self.EndModal(wx.ID_CANCEL)
event.Skip() event.Skip()
def OnDestroy(self, event): def OnDestroy(self, event):
# Clean up by unbinding some events and stopping the server
self.mainFrame.Unbind(GE.EVT_SSO_LOGIN, handler=self.OnLogin) self.mainFrame.Unbind(GE.EVT_SSO_LOGIN, handler=self.OnLogin)
if self:
self.Unbind(wx.EVT_WINDOW_DESTROY, handler=self.OnDestroy)
self.sEsi.stopServer() self.sEsi.stopServer()
event.Skip() event.Skip()

View File

@@ -123,13 +123,14 @@ class TargetProfileEditor(AuxiliaryFrame):
ATTRIBUTES = OrderedDict([ ATTRIBUTES = OrderedDict([
('maxVelocity', (_t('Maximum speed'), 'm/s')), ('maxVelocity', (_t('Maximum speed'), 'm/s')),
('signatureRadius', (_t('Signature radius\nLeave blank for infinitely big value'), 'm')), ('signatureRadius', (_t('Signature radius\nLeave blank for infinitely big value'), 'm')),
('radius', (_t('Radius'), 'm'))]) ('radius', (_t('Radius\nThe radius of the sphere that represents a ship/drone in space. Affects range calculations.'), 'm')),
('hp', (_t('Total HP\nAffects how much damage breacher pods can do. Leave blank for infinitely big value'), 'hp'))])
def __init__(self, parent): def __init__(self, parent):
super().__init__( super().__init__(
parent, id=wx.ID_ANY, title=_t("Target Profile Editor"), resizeable=True, parent, id=wx.ID_ANY, title=_t("Target Profile Editor"), resizeable=True,
# Dropdown list widget is scaled to its longest content line on GTK, adapt to that # Dropdown list widget is scaled to its longest content line on GTK, adapt to that
size=wx.Size(500, 240) if "wxGTK" in wx.PlatformInfo else wx.Size(350, 240)) size=wx.Size(630, 240) if "wxGTK" in wx.PlatformInfo else wx.Size(450, 240))
self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.block = False self.block = False
@@ -145,36 +146,29 @@ class TargetProfileEditor(AuxiliaryFrame):
contentSizer = wx.BoxSizer(wx.VERTICAL) contentSizer = wx.BoxSizer(wx.VERTICAL)
resistEditSizer = wx.FlexGridSizer(2, 6, 0, 2) resistEditSizer = wx.BoxSizer(wx.HORIZONTAL)
resistEditSizer.AddGrowableCol(0) resistEditSizer.AddStretchSpacer()
resistEditSizer.AddGrowableCol(5)
resistEditSizer.SetFlexibleDirection(wx.BOTH)
resistEditSizer.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED)
defSize = wx.Size(50, -1) defSize = wx.Size(70, -1)
for i, type_ in enumerate(self.DAMAGE_TYPES): for type_ in self.DAMAGE_TYPES:
if i % 2: leftPad = 25 if type_ != list(self.DAMAGE_TYPES)[0] else 0
style = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.LEFT
border = 25
else:
style = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT
border = 5
ttText = self.DAMAGE_TYPES[type_] ttText = self.DAMAGE_TYPES[type_]
bmp = wx.StaticBitmap(self, wx.ID_ANY, BitmapLoader.getBitmap("%s_big" % type_, "gui")) bmp = wx.StaticBitmap(self, wx.ID_ANY, BitmapLoader.getBitmap("%s_big" % type_, "gui"))
bmp.SetToolTip(wx.ToolTip(ttText)) bmp.SetToolTip(wx.ToolTip(ttText))
resistEditSizer.Add(bmp, 0, style, border) resistEditSizer.Add(bmp, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, leftPad)
# set text edit # set text edit
editBox = FloatBox(parent=self, id=wx.ID_ANY, value=None, pos=wx.DefaultPosition, size=defSize, validator=ResistValidator()) editBox = FloatBox(parent=self, id=wx.ID_ANY, value=None, pos=wx.DefaultPosition, size=defSize)
editBox.SetToolTip(wx.ToolTip(ttText)) editBox.SetToolTip(wx.ToolTip(ttText))
self.Bind(event=wx.EVT_TEXT, handler=self.OnFieldChanged, source=editBox) self.Bind(event=wx.EVT_TEXT, handler=self.OnFieldChanged, source=editBox)
setattr(self, '{}Edit'.format(type_), editBox) setattr(self, '{}Edit'.format(type_), editBox)
resistEditSizer.Add(editBox, 0, wx.BOTTOM | wx.TOP | wx.ALIGN_CENTER_VERTICAL, 5) resistEditSizer.Add(editBox, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
unit = wx.StaticText(self, wx.ID_ANY, "%", wx.DefaultPosition, wx.DefaultSize, 0) unit = wx.StaticText(self, wx.ID_ANY, "%", wx.DefaultPosition, wx.DefaultSize, 0)
unit.SetToolTip(wx.ToolTip(ttText)) unit.SetToolTip(wx.ToolTip(ttText))
resistEditSizer.Add(unit, 0, wx.BOTTOM | wx.TOP | wx.ALIGN_CENTER_VERTICAL, 5) resistEditSizer.Add(unit, 0, wx.BOTTOM | wx.TOP | wx.ALIGN_CENTER_VERTICAL, 5)
contentSizer.Add(resistEditSizer, 0, wx.EXPAND | wx.ALL, 5) resistEditSizer.AddStretchSpacer()
contentSizer.Add(resistEditSizer, 1, wx.EXPAND | wx.ALL, 5)
miscAttrSizer = wx.BoxSizer(wx.HORIZONTAL) miscAttrSizer = wx.BoxSizer(wx.HORIZONTAL)
miscAttrSizer.AddStretchSpacer() miscAttrSizer.AddStretchSpacer()

View File

@@ -66,8 +66,8 @@ class UpdateDialog(wx.Dialog):
self.browser.Bind(wx.html2.EVT_WEBVIEW_NEWWINDOW, self.OnNewWindow) self.browser.Bind(wx.html2.EVT_WEBVIEW_NEWWINDOW, self.OnNewWindow)
link_patterns = [ link_patterns = [
(re.compile("#(\d+)", re.I), r"https://github.com/pyfa-org/Pyfa/issues/\1"), (re.compile(r"#(\d+)", re.I), r"https://github.com/pyfa-org/Pyfa/issues/\1"),
(re.compile("@(\w+)", re.I), r"https://github.com/\1") (re.compile(r"@(\w+)", re.I), r"https://github.com/\1")
] ]
markdowner = markdown2.Markdown( markdowner = markdown2.Markdown(

View File

@@ -2,6 +2,8 @@ import wx
def isDark(): def isDark():
if 'wxMSW' in wx.PlatformInfo:
return False
try: try:
return wx.SystemSettings.GetAppearance().IsDark() return wx.SystemSettings.GetAppearance().IsDark()
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):

View File

@@ -26,20 +26,20 @@ class exportHtml:
def __init__(self): def __init__(self):
self.thread = exportHtmlThread() self.thread = exportHtmlThread()
def refreshFittingHtml(self, force=False, callback=False): def refreshFittingHtml(self, force=False, progress=None):
settings = HTMLExportSettings.getInstance() settings = HTMLExportSettings.getInstance()
if force or settings.getEnabled(): if force or settings.getEnabled():
self.thread.stop() self.thread.stop()
self.thread = exportHtmlThread(callback) self.thread = exportHtmlThread(progress)
self.thread.start() self.thread.start()
class exportHtmlThread(threading.Thread): class exportHtmlThread(threading.Thread):
def __init__(self, callback=False): def __init__(self, progress=False):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.name = "HTMLExport" self.name = "HTMLExport"
self.callback = callback self.progress = progress
self.stopRunning = False self.stopRunning = False
def stop(self): def stop(self):
@@ -72,11 +72,13 @@ class exportHtmlThread(threading.Thread):
pass pass
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
raise raise
except Exception as ex: except Exception as e:
pass if self.progress:
self.progress.error = f'{e}'
if self.callback: finally:
wx.CallAfter(self.callback, -1) if self.progress:
self.progress.current += 1
self.progress.workerWorking = False
def generateFullHTML(self, sMkt, sFit, dnaUrl): def generateFullHTML(self, sMkt, sFit, dnaUrl):
""" Generate the complete HTML with styling and javascript """ """ Generate the complete HTML with styling and javascript """
@@ -171,13 +173,13 @@ class exportHtmlThread(threading.Thread):
</head> </head>
<body> <body>
<div id="canvas" data-role="page"> <div id="canvas" data-role="page">
<div style="text-align: center;"><strong>Last updated:</strong> %s <small>(<span class="timer"></span>)</small></div>
<div data-role="header"> <div data-role="header">
<h1>Pyfa fits</h1> <h1>Pyfa fits by Group</h1>
</div> </div>
<div data-role="content"> <div data-role="content">
<div style="text-align: center;"><strong>Last updated:</strong> %s <small>(<span class="timer"></span>)</small></div>
""" % (time.time(), dnaUrl, localDate) """ % (time.time(), dnaUrl, localDate)
HTML += ' <ul data-role="listview" class="ui-listview-outer" data-inset="true" data-filter="true">\n' HTML += ' <ul data-role="listview" class="ui-listview-outer" data-inset="true" data-filter="true">\n'
categoryList = list(sMkt.getShipRoot()) categoryList = list(sMkt.getShipRoot())
categoryList.sort(key=lambda _ship: _ship.name) categoryList.sort(key=lambda _ship: _ship.name)
@@ -214,7 +216,9 @@ class exportHtmlThread(threading.Thread):
eftFit = Port.exportEft(getFit(fit[0]), options={ eftFit = Port.exportEft(getFit(fit[0]), options={
PortEftOptions.IMPLANTS: True, PortEftOptions.IMPLANTS: True,
PortEftOptions.MUTATIONS: True, PortEftOptions.MUTATIONS: True,
PortEftOptions.LOADED_CHARGES: True}) PortEftOptions.LOADED_CHARGES: True,
PortEftOptions.BOOSTERS: True,
PortEftOptions.CARGO: True})
HTMLfit = ( HTMLfit = (
' <li data-role="collapsible" data-iconpos="right" data-shadow="false" ' ' <li data-role="collapsible" data-iconpos="right" data-shadow="false" '
@@ -234,8 +238,8 @@ class exportHtmlThread(threading.Thread):
pyfalog.warning("Failed to export line") pyfalog.warning("Failed to export line")
continue continue
finally: finally:
if self.callback: if self.progress:
wx.CallAfter(self.callback, count) self.progress.current = count
count += 1 count += 1
HTMLgroup += HTMLship + (' </ul>\n' HTMLgroup += HTMLship + (' </ul>\n'
' </li>\n') ' </li>\n')
@@ -254,6 +258,68 @@ class exportHtmlThread(threading.Thread):
HTML += """ HTML += """
</ul> </ul>
</div> </div>
<div data-role="header">
<h1>Pyfa fits by Name</h1>
</div>
<div data-role="content">
"""
HTML += ' <ul data-role="listview" class="ui-listview-outer" data-inset="true" data-filter="true">\n'
categoryList = list(sMkt.getShipRoot())
categoryList.sort(key=lambda _ship: _ship.name)
count = 0
for group in categoryList:
# init market group string to give ships something to attach to
HTMLgroup = ''
ships = list(sMkt.getShipList(group.ID))
ships.sort(key=lambda _ship: _ship.name)
# Keep track of how many ships per group
groupFits = 0
for ship in ships:
fits = sFit.getFitsWithShip(ship.ID)
if len(fits) > 0:
groupFits += len(fits)
for fit in fits:
if self.stopRunning:
return
try:
eftFit = Port.exportEft(getFit(fit[0]), options={
PortEftOptions.IMPLANTS: True,
PortEftOptions.MUTATIONS: True,
PortEftOptions.LOADED_CHARGES: True,
PortEftOptions.BOOSTERS: True,
PortEftOptions.CARGO: True})
HTMLfit = (
' <li data-role="collapsible" data-iconpos="right" data-shadow="false" '
'data-corners="false">\n'
' <h2>' + ship.name + " - " + fit[1] + '</h2>\n'
' <ul data-role="listview" data-shadow="false" data-inset="true" '
'data-corners="false">\n'
)
HTMLfit += ' <li><pre>' + eftFit + '\n </pre></li>\n'
HTMLfit += ' </ul>\n </li>\n'
HTML += HTMLfit
except (KeyboardInterrupt, SystemExit):
raise
except:
pyfalog.warning("Failed to export line")
continue
finally:
if self.progress:
self.progress.current = count
count += 1
HTML += """
</ul>
</div>
</div> </div>
</body> </body>
</html>""" </html>"""
@@ -291,7 +357,7 @@ class exportHtmlThread(threading.Thread):
pyfalog.error("Failed to export line") pyfalog.error("Failed to export line")
continue continue
finally: finally:
if self.callback: if self.progress:
wx.CallAfter(self.callback, count) self.progress.current = count
count += 1 count += 1
return HTML return HTML

View File

@@ -58,6 +58,9 @@ class InputValidator(metaclass=ABCMeta):
class FloatBox(wx.TextCtrl): class FloatBox(wx.TextCtrl):
def __init__(self, parent, value, id=wx.ID_ANY, style=0, validator=None, **kwargs): def __init__(self, parent, value, id=wx.ID_ANY, style=0, validator=None, **kwargs):
# Workaround for #2591
if 'wxMac' in wx.PlatformInfo and 'size' not in kwargs:
kwargs['size'] = wx.Size(97, 26)
super().__init__(parent=parent, id=id, style=style, **kwargs) super().__init__(parent=parent, id=id, style=style, **kwargs)
self.Bind(wx.EVT_TEXT, self.OnText) self.Bind(wx.EVT_TEXT, self.OnText)
self._storedValue = '' self._storedValue = ''
@@ -93,7 +96,7 @@ class FloatBox(wx.TextCtrl):
if currentValue == self._storedValue: if currentValue == self._storedValue:
event.Skip() event.Skip()
return return
if currentValue == '' or re.match('^\d*\.?\d*$', currentValue): if currentValue == '' or re.match(r'^\d*\.?\d*$', currentValue):
self._storedValue = currentValue self._storedValue = currentValue
self.updateColor() self.updateColor()
event.Skip() event.Skip()
@@ -107,6 +110,9 @@ class FloatBox(wx.TextCtrl):
class FloatRangeBox(wx.TextCtrl): class FloatRangeBox(wx.TextCtrl):
def __init__(self, parent, value, id=wx.ID_ANY, style=0, **kwargs): def __init__(self, parent, value, id=wx.ID_ANY, style=0, **kwargs):
# Workaround for #2591
if 'wxMac' in wx.PlatformInfo and 'size' not in kwargs:
kwargs['size'] = wx.Size(97, 26)
super().__init__(parent=parent, id=id, style=style, **kwargs) super().__init__(parent=parent, id=id, style=style, **kwargs)
self.Bind(wx.EVT_TEXT, self.OnText) self.Bind(wx.EVT_TEXT, self.OnText)
self._storedValue = '' self._storedValue = ''
@@ -125,7 +131,7 @@ class FloatRangeBox(wx.TextCtrl):
if currentValue == self._storedValue: if currentValue == self._storedValue:
event.Skip() event.Skip()
return return
if currentValue == '' or re.match('^\d*\.?\d*-?\d*\.?\d*$', currentValue): if currentValue == '' or re.match(r'^\d*\.?\d*-?\d*\.?\d*$', currentValue):
self._storedValue = currentValue self._storedValue = currentValue
event.Skip() event.Skip()
else: else:

View File

@@ -0,0 +1,19 @@
class ProgressHelper:
def __init__(self, message, maximum=None, callback=None):
self.message = message
self.current = 0
self.maximum = maximum
self.workerWorking = True
self.dlgWorking = True
self.error = None
self.callback = callback
self.cbArgs = []
@property
def working(self):
return self.workerWorking and self.dlgWorking and not self.error
@property
def userCancelled(self):
return not self.dlgWorking

BIN
imgs/gui/hp_big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
imgs/icons/10155@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

BIN
imgs/icons/10155@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/10848@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

BIN
imgs/icons/10848@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 B

After

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 604 B

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 B

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 634 B

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

BIN
imgs/icons/24603@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

BIN
imgs/icons/24603@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
imgs/icons/25240@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

BIN
imgs/icons/25240@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25241@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

BIN
imgs/icons/25241@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25242@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

BIN
imgs/icons/25242@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25243@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

BIN
imgs/icons/25243@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

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