diff --git a/.appveyor.yml b/.appveyor.yml
index 13952f800..821bbe7d3 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -1,5 +1,4 @@
environment:
-
global:
# SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the
# /E:ON and /V:ON options are not enabled in the batch script intepreter
@@ -8,76 +7,11 @@ environment:
matrix:
- # Python 2.7.10 is the latest version and is not pre-installed.
-
- # - PYTHON: "C:\\Python27.10"
- # PYTHON_VERSION: "2.7.10"
- # PYTHON_ARCH: "32"
-
- #- PYTHON: "C:\\Python27.10-x64"
- # PYTHON_VERSION: "2.7.10"
- # PYTHON_ARCH: "64"
-
- # Pre-installed Python versions, which Appveyor may upgrade to
- # a later point release.
- # See: http://www.appveyor.com/docs/installed-software#python
-
- #- PYTHON: "C:\\Python27"
- # PYTHON_VERSION: "2.7.x" # currently 2.7.9
- # PYTHON_ARCH: "32"
-
- #- PYTHON: "C:\\Python27-x64"
- # PYTHON_VERSION: "2.7.x" # currently 2.7.9
- # PYTHON_ARCH: "64"
-
- #- PYTHON: "C:\\Python33"
- # PYTHON_VERSION: "3.3.x" # currently 3.3.5
- # PYTHON_ARCH: "32"
-
- #- PYTHON: "C:\\Python33-x64"
- # PYTHON_VERSION: "3.3.x" # currently 3.3.5
- # PYTHON_ARCH: "64"
-
- #- PYTHON: "C:\\Python34"
- # PYTHON_VERSION: "3.4.x" # currently 3.4.3
- # PYTHON_ARCH: "32"
-
- #- PYTHON: "C:\\Python34-x64"
- # PYTHON_VERSION: "3.4.x" # currently 3.4.3
- # PYTHON_ARCH: "64"
-
- # Python versions not pre-installed
-
- # Python 2.6.6 is the latest Python 2.6 with a Windows installer
- # See: https://github.com/ogrisel/python-appveyor-demo/issues/10
-
- #- PYTHON: "C:\\Python266"
- # PYTHON_VERSION: "2.6.6"
- # PYTHON_ARCH: "32"
-
- #- PYTHON: "C:\\Python266-x64"
- # PYTHON_VERSION: "2.6.6"
- # PYTHON_ARCH: "64"
-
- PYTHON: "C:\\Python36"
PYTHON_VERSION: "3.6.x"
PYTHON_ARCH: "32"
-
- #- PYTHON: "C:\\Python35-x64"
- # PYTHON_VERSION: "3.5.0"
- # PYTHON_ARCH: "64"
-
- # Major and minor releases (i.e x.0.0 and x.y.0) prior to 3.3.0 use
- # a different naming scheme.
-
- #- PYTHON: "C:\\Python270"
- # PYTHON_VERSION: "2.7.0"
- # PYTHON_ARCH: "32"
-
- #- PYTHON: "C:\\Python270-x64"
- # PYTHON_VERSION: "2.7.0"
- # PYTHON_ARCH: "64"
-
+init:
+ - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
install:
# If there is a newer build queued for the same PR, cancel this one.
# The AppVeyor 'rollout builds' option is supposed to serve the same
@@ -89,15 +23,6 @@ install:
Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { `
throw "There are newer queued builds for this pull request, failing early." }
-# # Install wxPython
-# - 'ECHO Downloading wxPython.'
-# - "appveyor DownloadFile https://goo.gl/yvO8PB -FileName C:\\wxpython.exe"
-# #- "appveyor DownloadFile https://goo.gl/Uj0jV3 -FileName C:\\wxpython64.exe"
-#
-# - 'ECHO Install wxPython'
-# - "C:\\wxpython.exe /SP- /VERYSILENT /NORESTART"
-# #- "C:\\wxpython64.exe /SP- /VERYSILENT /NORESTART"
-
- ECHO "Filesystem root:"
- ps: "ls \"C:/\""
@@ -110,16 +35,11 @@ install:
- ECHO "Installed SDKs:"
- ps: "ls \"C:/Program Files/Microsoft SDKs/Windows\""
- # Install Python (from the official .msi of http://python.org) and pip when
- # not already installed.
- # - ps: if (-not(Test-Path($env:PYTHON))) { & appveyor\install.ps1 }
-
# Prepend newly installed Python to the PATH of this build (this cannot be
# done from inside the powershell script as it would require to restart
# the parent CMD process).
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
- # Check that we have the expected version and architecture for Python
- "python --version"
- "python -c \"import struct; print(struct.calcsize('P') * 8)\""
@@ -131,21 +51,36 @@ install:
# compiled extensions and are not provided as pre-built wheel packages,
# pip will build them from source using the MSVC compiler matching the
# target Python version and architecture
- # C:\\projects\\eve-gnosis\\
- ECHO "Install pip requirements:"
- "pip install -r requirements.txt"
- "pip install PyInstaller"
-# - "pip install -r requirements_test.txt"
-# - "pip install -r requirements_build_windows.txt"
+
+before_build:
+ # directory that will contain the built files
+ - ps: $env:PYFA_DIST_DIR = "c:\projects\$env:APPVEYOR_PROJECT_SLUG\dist"
+ - ps: $env:PYFA_VERSION = (python ./scripts/dump_version.py)
+ - ps: echo("pyfa version ")
+ - ps: echo ($env:PYFA_VERSION)
build_script:
- # Build the compiled extension
- # - "python setup.py build"
- ECHO "Build pyfa:"
- #- copy C:\projects\pyfa\dist_assets\win\pyfa.spec C:\projects\pyfa\pyfa.spec
- - ps: cd C:\projects\$env:APPVEYOR_PROJECT_SLUG
- - "python -m PyInstaller --noupx --clean --windowed --noconsole -m ./dist_assets/win/pyfa.exe.manifest -y ./dist_assets/win/pyfa.spec"
+ ##########
+ # PyInstaller - create binaries for pyfa
+ ##########
+ # Build command for PyInstaller
+ - "python -m PyInstaller --noupx --clean --windowed --noconsole -y pyfa.spec"
+ # Copy over manifest (See pyfa-org/pyfa#1622)
+ - ps: xcopy /y dist_assets\win\pyfa.exe.manifest $env:PYFA_DIST_DIR\pyfa\
+ # Not really sure if this is needed, but why not
+ - ps: xcopy /y dist_assets\win\Microsoft.VC90.CRT.manifest $env:PYFA_DIST_DIR\pyfa\
+
+ ##########
+ # InnoScript EXE building
+ # This is in a separate script because I don't feel like copying over the logic to AppVeyor script right now...
+ ##########
+ - "python dist_assets/win/dist.py"
+ - ps: dir $env:PYFA_DIST_DIR/
#- ECHO "Build pyfa (Debug):"
#- copy C:\projects\pyfa\dist_assets\win\pyfa_debug.spec C:\projects\pyfa\pyfa_debug.spec
#- "pyinstaller.exe --clean --noconfirm --windowed --upx-dir=C:\\projects\\pyfa\\scripts\\upx.exe C:\\projects\\pyfa\\pyfa_debug.spec"
@@ -155,12 +90,11 @@ build: on
after_build:
- ps: "ls \"./\""
#- ps: "ls \"C:\\projects\\pyfa\\build\\pyfa\\\""
- - ps: "ls \"C:\\projects\\$env:APPVEYOR_PROJECT_SLUG\\dist\\\""
# - ps: "ls \"C:\\projects\\$env:APPVEYOR_PROJECT_SLUG\\build\\exe.win32-2.7\\\""
# Zip
# APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER
#- 7z a build.zip -r C:\projects\pyfa\build\pyfa\*.*
- - ps: 7z a pyfa.zip -r C:\projects\$env:APPVEYOR_PROJECT_SLUG\dist\pyfa\*.*
+ - ps: 7z a "pyfa-$env:PYFA_VERSION-win.zip" -r "$env:PYFA_DIST_DIR\pyfa\*.*"
#- 7z a pyfa_debug.zip -r C:\projects\pyfa\dist\pyfa_debug\*.*
on_success:
@@ -181,11 +115,21 @@ after_test:
artifacts:
# Archive the generated packages in the ci.appveyor.com build report.
- - path: pyfa.zip
- name: 'pyfa.zip'
+ - path: pyfa*-win.zip
+ - path: pyfa*-win.exe
#- path: pyfa_debug.zip
# name: Pyfa_debug
-
+
+deploy:
+ tag: $(pyfa_version)
+ release: pyfa $(pyfa_version)
+ description: 'Release description'
+ provider: GitHub
+ auth_token:
+ secure: BfNHO66ff5hVx2O2ORbl49X0U/5h2V2T0IuRZDwm7fd1HvsVluF0wRCbl29oRp1M
+ draft: true
+ on:
+ APPVEYOR_REPO_TAG: true # deploy on tag push only
#on_success:
# - TODO: upload the content of dist/*.whl to a public wheelhouse
#
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 889ad5544..9140c2f10 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,36 +1,29 @@
-dist: trusty
-sudo: required
+os: linux
language: python
-cache: pip
python:
- - '3.6'
-env:
- - TOXENV=pep8
-addons:
- apt:
- packages:
+ - 3.6
+matrix:
+ include:
+ - os: osx
+ osx_image: xcode7.3
+ language: generic
+ env: PYTHON=3.6.1
before_install:
- - sudo apt-get update && sudo apt-get --reinstall install -qq language-pack-en language-pack-ru language-pack-he language-pack-zh-hans
- - pip install tox
- # We're not actually installing Tox, but have to run it before we install wxPython via Conda. This is fugly but vOv
- - tox
- - pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk2/ubuntu-14.04 wxPython==4.0.0b2
-# # get Conda
-# - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then
-# wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh;
-# else
-# wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh;
-# fi
-# - bash miniconda.sh -b -p $HOME/miniconda
-# - export PATH="$HOME/miniconda/bin:$PATH"
-# - hash -r
-# - conda config --set always_yes yes --set changeps1 no
-# - conda update -q conda
-# # Useful for debugging any issues with conda
-# - conda info -a
-#install:
- # install wxPython 3.0.0.0
- # - conda install -c https://conda.anaconda.org/travis wxpython=4.0.0b2
-script:
- - tox
-
+ - bash scripts/setup-osx.sh
+install:
+ - export PYFA_VERSION="$(python3 scripts/dump_version.py)"
+ - bash scripts/package-osx.sh
+before_deploy:
+ - export RELEASE_PKG_FILE=$(ls *.deb)
+ - echo "deploying $RELEASE_PKG_FILE to GitHub releases"
+deploy:
+ provider: releases
+ api_key:
+ secure: Xfu0xApoB0zUPLXl29aYUulVC3iA4/3bXQwwADKCfAKZwxgNon4dLbO7Rie5/7Ukf2POL0KwmRaQGN3kOr+XSoIVTE4M5sXxnhiaaLGKQ+48hDizLE6JuXcZGJvkxUaghaTzIdCwHsG7VGBsPfQgfGsjJcfBp8tFNLmRyM/Jpsr8T6BR2MxtBIEUVy8zrOWFNZqnmWrY2pWMsB9fYt3JFNdpqeIgRAYqbBsBcZQ1MngLTi3ztuYS5IaF+lk06RrnBlHmUsJu/5nCvIpvPvD0i2BLZ3Uu0+Fn+8QWUgjJEL9MNseXZMXynu05xd8YRk7Ajc9CUrzQIIbAktyteYp85kE3pUJHmrMLcXhh7nqkwttR5/47Zwa3OLJLJFKBxMx6wY5jFkJjkV08850B7aWrmTFl/Eqc3Q5nZMuiEt3wFRbjxHi9h1mTN/fkxfRRHg8u3ENGPR+ZPiFC3J18qtks/B/hsKjjHvZP1i79OYlET4V/zyLyyQkCbpDaARQANuotLYJyZ7tH+KWEyRsvTi0M9Yev9mNNw6aI4vzh4HfkEhvcvnWnYwckPj1dnjQ573Qpw0Z9wsconoWfHAn+hBDt3+YLMrrFZl++mCRskHH1mZChX3aGMDi49zD0kfxBUkYPOAhguc6PwudBxHUZP+O6T/SoHylff6EizCE/k5dGeAk=
+ file_glob: true
+ file: "dist/pyfa-*.zip"
+ skip_cleanup: true
+ draft: true
+ on:
+ tags: true
+ repo: pyfa-org/Pyfa
diff --git a/config.py b/config.py
index 4ba409e33..7f347eb3c 100644
--- a/config.py
+++ b/config.py
@@ -1,9 +1,12 @@
import os
import sys
+import yaml
+import wx
from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \
StreamHandler, TimedRotatingFileHandler, WARNING
import hashlib
+from eos.const import Slot
from cryptography.fernet import Fernet
@@ -22,12 +25,6 @@ debug = False
# Defines if our saveddata will be in pyfa root or not
saveInRoot = False
-# Version data
-
-version = "2.7.0"
-tag = "Stable"
-expansionName = "December"
-expansionVersion = "1.0"
evemonMinVersion = "4081"
minItemSearchLength = 3
@@ -52,6 +49,13 @@ LOGLEVEL_MAP = {
"debug": DEBUG,
}
+slotColourMap = {
+ Slot.LOW: wx.Colour(250, 235, 204), # yellow = low slots
+ Slot.MED: wx.Colour(188, 215, 241), # blue = mid slots
+ Slot.HIGH: wx.Colour(235, 204, 209), # red = high slots
+ Slot.RIG: '',
+ Slot.SUBSYSTEM: ''
+}
def getClientSecret():
return clientHash
@@ -79,12 +83,7 @@ def getPyfaRoot():
def getVersion():
- if os.path.isfile(os.path.join(pyfaPath, '.version')):
- with open(os.path.join(pyfaPath, '.version')) as f:
- gitVersion = f.readline()
- return gitVersion
- # if no version file exists, then user is running from source or not an official build
- return version + " (git)"
+ return version
def getDefaultSave():
@@ -96,11 +95,12 @@ def defPaths(customSavePath=None):
global pyfaPath
global savePath
global saveDB
- global gameDB
+ global gameDB
global saveInRoot
global logPath
global cipher
global clientHash
+ global version
pyfalog.debug("Configuring Pyfa")
@@ -110,6 +110,12 @@ def defPaths(customSavePath=None):
if pyfaPath is None:
pyfaPath = getPyfaRoot()
+ # Version data
+
+ with open(os.path.join(pyfaPath, "version.yml"), 'r') as file:
+ data = yaml.load(file, Loader=yaml.FullLoader)
+ version = data['version']
+
# Where we store the saved fits etc, default is the current users home directory
if saveInRoot is True:
savePath = getattr(configforced, "savePath", None)
diff --git a/dist_assets/mac/pyfa.spec b/dist_assets/mac/pyfa.spec
index cf27cfe87..1e1f35711 100644
--- a/dist_assets/mac/pyfa.spec
+++ b/dist_assets/mac/pyfa.spec
@@ -24,11 +24,13 @@ added_files = [
('../../eve.db', '.'),
('../../README.md', '.'),
('../../LICENSE', '.'),
- ('../../.version', '.'),
+ ('../../version.yml', '.'),
]
-import_these = []
+import_these = [
+ 'numpy.core._dtype_ctypes' # https://github.com/pyinstaller/pyinstaller/issues/3982
+]
icon = os.path.join(os.getcwd(), "dist_assets", "mac", "pyfa.icns")
@@ -54,8 +56,10 @@ a = Analysis([r'../../pyfa.py'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
+
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
+
exe = EXE(pyz,
a.scripts,
a.binaries,
@@ -70,10 +74,16 @@ exe = EXE(pyz,
icon=icon,
)
-app = BUNDLE(exe,
- name='pyfa.app',
- icon=icon,
- bundle_identifier=None,
- info_plist={
- 'NSHighResolutionCapable': 'True'
- })
+app = BUNDLE(
+ exe,
+ name='pyfa.app',
+ icon=icon,
+ bundle_identifier=None,
+ info_plist={
+ 'NSHighResolutionCapable': 'True',
+ 'NSPrincipalClass': 'NSApplication',
+ 'CFBundleName': 'pyfa',
+ 'CFBundleDisplayName': 'pyfa',
+ 'CFBundleIdentifier': 'org.pyfaorg.pyfa',
+ }
+)
\ No newline at end of file
diff --git a/dist_assets/win/dist.py b/dist_assets/win/dist.py
index ec6da3928..e8d6f4ae5 100644
--- a/dist_assets/win/dist.py
+++ b/dist_assets/win/dist.py
@@ -3,44 +3,35 @@
import os.path
from subprocess import call
import zipfile
+from packaging.version import Version
+import yaml
-def zipdir(path, zip):
- for root, dirs, files in os.walk(path):
- for file in files:
- zip.write(os.path.join(root, file))
+with open("version.yml", 'r') as file:
+ data = yaml.load(file, Loader=yaml.FullLoader)
+ version = data['version']
-config = {}
+os.environ["PYFA_DIST_DIR"] = os.path.join(os.getcwd(), 'dist')
-exec(compile(open("config.py").read(), "config.py", 'exec'), config)
+os.environ["PYFA_VERSION"] = version
+iscc = "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" # inno script location via wine
-iscc = "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" # inno script location via wine
+source = os.path.join(os.environ["PYFA_DIST_DIR"], "pyfa")
-print("Creating archive")
-
-source = os.path.join(os.getcwd(), "dist", "pyfa")
-
-fileName = "pyfa-{}-{}-{}-win".format(
- config['version'],
- config['expansionName'].lower(),
- config['expansionVersion']
-)
-
-archive = zipfile.ZipFile(os.path.join(os.getcwd(), "dist", fileName + ".zip"), 'w', compression=zipfile.ZIP_DEFLATED)
-zipdir(source, archive)
-archive.close()
+fileName = "pyfa-{}-win".format(os.environ["PYFA_VERSION"])
print("Compiling EXE")
-expansion = "%s %s" % (config['expansionName'], config['expansionVersion']),
+v = Version(version)
+
+print(v)
call([
iscc,
os.path.join(os.getcwd(), "dist_assets", "win", "pyfa-setup.iss"),
- "/dMyAppVersion=%s" % (config['version']),
- "/dMyAppExpansion=%s" % expansion,
+ "/dMyAppVersion=%s" % v,
"/dMyAppDir=%s" % source,
- "/dMyOutputDir=%s" % os.path.join(os.getcwd(), "dist"),
+ "/dMyOutputDir=%s" % os.path.join(os.getcwd()),
"/dMyOutputFile=%s" % fileName]) # stdout=devnull, stderr=devnull
print("Done")
diff --git a/dist_assets/win/pyfa-setup.iss b/dist_assets/win/pyfa-setup.iss
index 7984da087..e016b3a37 100644
--- a/dist_assets/win/pyfa-setup.iss
+++ b/dist_assets/win/pyfa-setup.iss
@@ -7,15 +7,12 @@
#ifndef MyAppVersion
#define MyAppVersion "2.1.0"
#endif
-#ifndef MyAppExpansion
- #define MyAppExpansion "Vanguard 1.0"
-#endif
; Other config
#define MyAppName "pyfa"
#define MyAppPublisher "pyfa"
-#define MyAppURL "https://forums.eveonline.com/t/27156"
+#define MyAppURL "https://github.com/pyfa-org/Pyfa/"
#define MyAppExeName "pyfa.exe"
; What version starts with the new structure (1.x.0). This is used to determine if we run directory structure cleanup
@@ -23,7 +20,7 @@
#define MinorVersionFlag 0
#ifndef MyOutputFile
- #define MyOutputFile LowerCase(StringChange(MyAppName+'-'+MyAppVersion+'-'+MyAppExpansion+'-win-wx3', " ", "-"))
+ #define MyOutputFile LowerCase(StringChange(MyAppName+'-'+MyAppVersion+'-win', " ", "-"))
#endif
#ifndef MyAppDir
#define MyAppDir "pyfa"
@@ -39,7 +36,7 @@
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{3DA39096-C08D-49CD-90E0-1D177F32C8AA}
AppName={#MyAppName}
-AppVersion={#MyAppVersion} ({#MyAppExpansion})
+AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
@@ -51,10 +48,8 @@ LicenseFile={#MyAppDir}\LICENSE
OutputDir={#MyOutputDir}
OutputBaseFilename={#MyOutputFile}
SetupIconFile={#MyAppDir}\pyfa.ico
-Compression=lzma
SolidCompression=yes
CloseApplications=yes
-AppReadmeFile=https://github.com/pyfa-org/Pyfa/blob/v{#MyAppVersion}/readme.txt
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
diff --git a/dist_assets/win/pyfa.spec b/dist_assets/win/pyfa.spec
index d181a5eb9..1bdf820f0 100644
--- a/dist_assets/win/pyfa.spec
+++ b/dist_assets/win/pyfa.spec
@@ -5,8 +5,7 @@ from itertools import chain
import subprocess
import requests.certs
-label = subprocess.check_output([
- "git", "describe", "--tags"]).strip()
+label = subprocess.check_output(["git", "describe", "--tags"]).strip()
with open('.version', 'w+') as f:
f.write(label.decode())
@@ -18,7 +17,7 @@ added_files = [
('../../imgs/gui/*.gif', 'imgs/gui'),
('../../imgs/icons/*.png', 'imgs/icons'),
('../../imgs/renders/*.png', 'imgs/renders'),
- ('../../service/jargon/*.yaml', 'service/jargon'),
+ ('../../service/jargon/*.yaml', 'service/jargon'),
('../../dist_assets/win/pyfa.ico', '.'),
('../../dist_assets/win/pyfa.exe.manifest', '.'),
('../../dist_assets/win/Microsoft.VC90.CRT.manifest', '.'),
@@ -26,10 +25,12 @@ added_files = [
('../../eve.db', '.'),
('../../README.md', '.'),
('../../LICENSE', '.'),
- ('../../.version', '.'),
+ ('../../version.yml', '.'),
]
-import_these = []
+import_these = [
+ 'numpy.core._dtype_ctypes' # https://github.com/pyinstaller/pyinstaller/issues/3982
+]
# Walk directories that do dynamic importing
paths = ('eos/effects', 'eos/db/migrations', 'service/conversions')
diff --git a/eos/const.py b/eos/const.py
new file mode 100644
index 000000000..95dd8a888
--- /dev/null
+++ b/eos/const.py
@@ -0,0 +1,26 @@
+from eos.enum import Enum
+
+
+
+class Slot(Enum):
+ # These are self-explanatory
+ LOW = 1
+ MED = 2
+ HIGH = 3
+ RIG = 4
+ SUBSYSTEM = 5
+ # not a real slot, need for pyfa display rack separation
+ MODE = 6
+ # system effects. They are projected "modules" and pyfa assumes all modules
+ # have a slot. In this case, make one up.
+ SYSTEM = 7
+ # used for citadel services
+ SERVICE = 8
+ # fighter 'slots'. Just easier to put them here...
+ F_LIGHT = 10
+ F_SUPPORT = 11
+ F_HEAVY = 12
+ # fighter 'slots' (for structures)
+ FS_LIGHT = 13
+ FS_SUPPORT = 14
+ FS_HEAVY = 15
diff --git a/eos/db/gamedata/attribute.py b/eos/db/gamedata/attribute.py
index 727037421..4294f4ef7 100644
--- a/eos/db/gamedata/attribute.py
+++ b/eos/db/gamedata/attribute.py
@@ -39,6 +39,8 @@ attributes_table = Table("dgmattribs", gamedata_meta,
Column("displayName", String),
Column("highIsGood", Boolean),
Column("iconID", Integer),
+ Column("attributeCategory", Integer),
+ Column("tooltipDescription", Integer),
Column("unitID", Integer, ForeignKey("dgmunits.unitID")))
mapper(Attribute, typeattributes_table,
diff --git a/eos/db/gamedata/item.py b/eos/db/gamedata/item.py
index df7508e43..fd7be477d 100644
--- a/eos/db/gamedata/item.py
+++ b/eos/db/gamedata/item.py
@@ -40,7 +40,9 @@ items_table = Table("invtypes", gamedata_meta,
Column("marketGroupID", Integer, ForeignKey("invmarketgroups.marketGroupID")),
Column("iconID", Integer),
Column("graphicID", Integer),
- Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True))
+ Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True),
+ Column("replaceSame", String),
+ Column("replaceBetter", String))
from .metaGroup import metatypes_table # noqa
from .traits import traits_table # noqa
diff --git a/eos/db/migrations/upgrade30.py b/eos/db/migrations/upgrade30.py
new file mode 100644
index 000000000..7954f2d37
--- /dev/null
+++ b/eos/db/migrations/upgrade30.py
@@ -0,0 +1,17 @@
+"""
+Migration 30
+
+- changes to prices table
+"""
+
+
+import sqlalchemy
+
+
+def upgrade(saveddata_engine):
+ try:
+ saveddata_engine.execute("SELECT status FROM prices LIMIT 1")
+ except sqlalchemy.exc.DatabaseError:
+ # Just drop table, table will be re-created by sqlalchemy and
+ # data will be re-fetched
+ saveddata_engine.execute("DROP TABLE prices;")
diff --git a/eos/db/saveddata/price.py b/eos/db/saveddata/price.py
index 8be7f6519..8abd07132 100644
--- a/eos/db/saveddata/price.py
+++ b/eos/db/saveddata/price.py
@@ -17,17 +17,20 @@
# along with eos. If not, see .
# ===============================================================================
+
from sqlalchemy import Table, Column, Float, Integer
from sqlalchemy.orm import mapper
from eos.db import saveddata_meta
from eos.saveddata.price import Price
+
prices_table = Table("prices", saveddata_meta,
Column("typeID", Integer, primary_key=True),
Column("price", Float, default=0.0),
Column("time", Integer, nullable=False),
- Column("failed", Integer))
+ Column("status", Integer, nullable=False))
+
mapper(Price, prices_table, properties={
"_Price__price": prices_table.c.price,
diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py
index e582eef87..448584420 100644
--- a/eos/db/saveddata/queries.py
+++ b/eos/db/saveddata/queries.py
@@ -542,8 +542,17 @@ def commit():
with sd_lock:
try:
saveddata_session.commit()
- saveddata_session.flush()
- except Exception as ex:
+ except Exception:
+ saveddata_session.rollback()
+ exc_info = sys.exc_info()
+ raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
+
+
+def flush():
+ with sd_lock:
+ try:
+ saveddata_session.flush()
+ except Exception:
saveddata_session.rollback()
exc_info = sys.exc_info()
raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
diff --git a/eos/effects/ammoinfluencecapneed.py b/eos/effects/ammoinfluencecapneed.py
index 587e55cd5..3bbb1103c 100644
--- a/eos/effects/ammoinfluencecapneed.py
+++ b/eos/effects/ammoinfluencecapneed.py
@@ -1,7 +1,7 @@
# ammoInfluenceCapNeed
#
# Used by:
-# Items from category: Charge (493 of 947)
+# Items from category: Charge (493 of 949)
type = "passive"
diff --git a/eos/effects/ammoinfluencerange.py b/eos/effects/ammoinfluencerange.py
index 4358a6ff9..58c331911 100644
--- a/eos/effects/ammoinfluencerange.py
+++ b/eos/effects/ammoinfluencerange.py
@@ -1,7 +1,7 @@
# ammoInfluenceRange
#
# Used by:
-# Items from category: Charge (587 of 947)
+# Items from category: Charge (587 of 949)
type = "passive"
diff --git a/eos/effects/ammospeedmultiplier.py b/eos/effects/ammospeedmultiplier.py
index 2a6c2e4d3..093953c11 100644
--- a/eos/effects/ammospeedmultiplier.py
+++ b/eos/effects/ammospeedmultiplier.py
@@ -1,10 +1,9 @@
# ammoSpeedMultiplier
#
# Used by:
-# Charges from group: Festival Charges (23 of 23)
+# Charges from group: Festival Charges (26 of 26)
# Charges from group: Interdiction Probe (2 of 2)
-# Charges from group: Structure Festival Charges (3 of 3)
-# Special Edition Assetss from group: Festival Charges Expired (2 of 2)
+# Items from market group: Special Edition Assets > Special Edition Festival Assets (30 of 33)
type = "passive"
diff --git a/eos/effects/ammotrackingmultiplier.py b/eos/effects/ammotrackingmultiplier.py
index 6153af15c..95033d743 100644
--- a/eos/effects/ammotrackingmultiplier.py
+++ b/eos/effects/ammotrackingmultiplier.py
@@ -1,7 +1,7 @@
# ammoTrackingMultiplier
#
# Used by:
-# Items from category: Charge (182 of 947)
+# Items from category: Charge (182 of 949)
# Charges from group: Projectile Ammo (128 of 128)
type = "passive"
diff --git a/eos/effects/boostershieldcapacitypenalty.py b/eos/effects/boostershieldcapacitypenalty.py
index 41108747f..f93f04af9 100644
--- a/eos/effects/boostershieldcapacitypenalty.py
+++ b/eos/effects/boostershieldcapacitypenalty.py
@@ -1,7 +1,7 @@
# boosterShieldCapacityPenalty
#
# Used by:
-# Implants from group: Booster (12 of 71)
+# Implants from group: Booster (12 of 70)
type = "boosterSideEffect"
# User-friendly name for the side effect
diff --git a/eos/effects/cynosuraldurationbonus.py b/eos/effects/cynosuraldurationbonus.py
index 4f78ea09e..defbd9ca3 100644
--- a/eos/effects/cynosuraldurationbonus.py
+++ b/eos/effects/cynosuraldurationbonus.py
@@ -6,5 +6,5 @@ type = "passive"
def handler(fit, ship, context):
- fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Cynosural Field",
+ fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Cynosural Field Generator",
"duration", ship.getModifiedItemAttr("durationBonus"))
diff --git a/eos/effects/cynosuralgeneration.py b/eos/effects/cynosuralgeneration.py
index b99325c0f..36e29c52d 100644
--- a/eos/effects/cynosuralgeneration.py
+++ b/eos/effects/cynosuralgeneration.py
@@ -1,7 +1,7 @@
# cynosuralGeneration
#
# Used by:
-# Modules from group: Cynosural Field (2 of 2)
+# Modules from group: Cynosural Field Generator (2 of 2)
type = "active"
diff --git a/eos/effects/cynosuraltheoryconsumptionbonus.py b/eos/effects/cynosuraltheoryconsumptionbonus.py
index d67c969f8..d6bda31b7 100644
--- a/eos/effects/cynosuraltheoryconsumptionbonus.py
+++ b/eos/effects/cynosuraltheoryconsumptionbonus.py
@@ -8,6 +8,6 @@ type = "passive"
def handler(fit, container, context):
level = container.level if "skill" in context else 1
- fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Cynosural Field",
+ fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Cynosural Field Generator",
"consumptionQuantity",
container.getModifiedItemAttr("consumptionQuantityBonusPercentage") * level)
diff --git a/eos/effects/damagecontrol.py b/eos/effects/damagecontrol.py
index 05589e15a..f563fac03 100644
--- a/eos/effects/damagecontrol.py
+++ b/eos/effects/damagecontrol.py
@@ -11,6 +11,5 @@ def handler(fit, module, context):
bonus = "%s%sDamageResonance" % (attrPrefix, damageType)
bonus = "%s%s" % (bonus[0].lower(), bonus[1:])
booster = "%s%sDamageResonance" % (layer, damageType)
- penalize = False if layer == 'hull' else True
fit.ship.multiplyItemAttr(bonus, module.getModifiedItemAttr(booster),
- stackingPenalties=penalize, penaltyGroup="preMul")
+ stackingPenalties=True, penaltyGroup="preMul")
diff --git a/eos/effects/emergencyhullenergizer.py b/eos/effects/emergencyhullenergizer.py
index 8a281f74c..c2de4f4f5 100644
--- a/eos/effects/emergencyhullenergizer.py
+++ b/eos/effects/emergencyhullenergizer.py
@@ -8,4 +8,6 @@ runtime = "late"
def handler(fit, src, context):
for dmgType in ('em', 'thermal', 'kinetic', 'explosive'):
- fit.ship.forceItemAttr('{}DamageResonance'.format(dmgType), src.getModifiedItemAttr("hull{}DamageResonance".format(dmgType.title())))
+ fit.ship.multiplyItemAttr('{}DamageResonance'.format(dmgType),
+ src.getModifiedItemAttr("hull{}DamageResonance".format(dmgType.title())),
+ stackingPenalties=True, penaltyGroup="postMul")
diff --git a/eos/effects/implantwarpscramblerangebonus.py b/eos/effects/implantwarpscramblerangebonus.py
new file mode 100644
index 000000000..8eea16959
--- /dev/null
+++ b/eos/effects/implantwarpscramblerangebonus.py
@@ -0,0 +1,10 @@
+# implantWarpScrambleRangeBonus
+#
+# Used by:
+# Implants named like: Inquest 'Hedone' Entanglement Optimizer WS (3 of 3)
+type = "passive"
+
+
+def handler(fit, src, context):
+ fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Warp Scrambler", "maxRange",
+ src.getModifiedItemAttr("warpScrambleRangeBonus"), stackingPenalties=False)
diff --git a/eos/effects/miningdurationmultiplieronline.py b/eos/effects/miningdurationmultiplieronline.py
new file mode 100644
index 000000000..3d4cad5a5
--- /dev/null
+++ b/eos/effects/miningdurationmultiplieronline.py
@@ -0,0 +1,10 @@
+# miningDurationMultiplierOnline
+#
+# Used by:
+# Module: Frostline 'Omnivore' Harvester Upgrade
+type = "passive"
+
+
+def handler(fit, module, context):
+ fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Mining Laser",
+ "duration", module.getModifiedItemAttr("miningDurationMultiplier"))
diff --git a/eos/effects/overloadrofbonus.py b/eos/effects/overloadrofbonus.py
index b362b9cc5..c0fedef1a 100644
--- a/eos/effects/overloadrofbonus.py
+++ b/eos/effects/overloadrofbonus.py
@@ -2,7 +2,7 @@
#
# Used by:
# Modules from group: Missile Launcher Torpedo (22 of 22)
-# Items from market group: Ship Equipment > Turrets & Bays (429 of 882)
+# Items from market group: Ship Equipment > Turrets & Bays (429 of 883)
# Module: Interdiction Sphere Launcher I
type = "overheat"
diff --git a/eos/effects/remotewebifiermaxrangebonus.py b/eos/effects/remotewebifiermaxrangebonus.py
index 46e5887dd..531f18ded 100644
--- a/eos/effects/remotewebifiermaxrangebonus.py
+++ b/eos/effects/remotewebifiermaxrangebonus.py
@@ -2,6 +2,7 @@
#
# Used by:
# Implants named like: Inquest 'Eros' Stasis Webifier MR (3 of 3)
+# Implants named like: Inquest 'Hedone' Entanglement Optimizer WS (3 of 3)
type = "passive"
diff --git a/eos/effects/shipbonusnosneutcapneedrolebonus2.py b/eos/effects/shipbonusnosneutcapneedrolebonus2.py
index 314736818..ec427ec11 100644
--- a/eos/effects/shipbonusnosneutcapneedrolebonus2.py
+++ b/eos/effects/shipbonusnosneutcapneedrolebonus2.py
@@ -1,8 +1,7 @@
# shipBonusNosNeutCapNeedRoleBonus2
#
# Used by:
-# Ship: Rodiva
-# Ship: Zarmazd
+# Variations of ship: Rodiva (2 of 2)
type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Capacitor Emission Systems"), "capacitorNeed", src.getModifiedItemAttr("shipBonusRole2"))
diff --git a/eos/effects/shipbonusremotecapacitortransferrangerole1.py b/eos/effects/shipbonusremotecapacitortransferrangerole1.py
index 22e7efe3b..bfd670862 100644
--- a/eos/effects/shipbonusremotecapacitortransferrangerole1.py
+++ b/eos/effects/shipbonusremotecapacitortransferrangerole1.py
@@ -1,8 +1,7 @@
# shipBonusRemoteCapacitorTransferRangeRole1
#
# Used by:
-# Ship: Rodiva
-# Ship: Zarmazd
+# Variations of ship: Rodiva (2 of 2)
type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Remote Capacitor Transmitter", "maxRange", src.getModifiedItemAttr("shipBonusRole1"))
diff --git a/eos/effects/shipbonussmartbombcapneedrolebonus2.py b/eos/effects/shipbonussmartbombcapneedrolebonus2.py
index 71df39ec4..69e4017a8 100644
--- a/eos/effects/shipbonussmartbombcapneedrolebonus2.py
+++ b/eos/effects/shipbonussmartbombcapneedrolebonus2.py
@@ -1,15 +1,14 @@
# shipBonusSmartbombCapNeedRoleBonus2
#
# Used by:
+# Variations of ship: Rodiva (2 of 2)
# Ship: Damavik
# Ship: Drekavac
# Ship: Hydra
# Ship: Kikimora
# Ship: Leshak
-# Ship: Rodiva
# Ship: Tiamat
# Ship: Vedmak
-# Ship: Zarmazd
type = "passive"
diff --git a/eos/effects/skillbombdeploymentmodulereactivationdelaybonus.py b/eos/effects/skillbombdeploymentmodulereactivationdelaybonus.py
index e56b2fb94..4db900c9b 100644
--- a/eos/effects/skillbombdeploymentmodulereactivationdelaybonus.py
+++ b/eos/effects/skillbombdeploymentmodulereactivationdelaybonus.py
@@ -7,4 +7,4 @@ type = "passive"
def handler(fit, skill, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Missile Launcher Bomb",
- "moduleReactivationDelay", skill.getModifiedItemAttr("rofBonus") * skill.level)
+ "moduleReactivationDelay", skill.getModifiedItemAttr("reactivationDelayBonus") * skill.level)
diff --git a/eos/effects/skillbonusdronedurability.py b/eos/effects/skillbonusdronedurability.py
index e68cb4254..f36e82a3f 100644
--- a/eos/effects/skillbonusdronedurability.py
+++ b/eos/effects/skillbonusdronedurability.py
@@ -1,7 +1,6 @@
# skillBonusDroneDurability
#
# Used by:
-# Implants from group: Cyber Drones (4 of 4)
# Skill: Drone Durability
type = "passive"
diff --git a/eos/effects/skillbonusdronedurabilitynotfighters.py b/eos/effects/skillbonusdronedurabilitynotfighters.py
new file mode 100644
index 000000000..d2b8a0967
--- /dev/null
+++ b/eos/effects/skillbonusdronedurabilitynotfighters.py
@@ -0,0 +1,14 @@
+# skillBonusDroneDurabilityNotFighters
+#
+# Used by:
+# Implants from group: Cyber Drones (4 of 4)
+type = "passive"
+
+
+def handler(fit, src, context):
+ fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "hp",
+ src.getModifiedItemAttr("hullHpBonus"))
+ fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "armorHP",
+ src.getModifiedItemAttr("armorHpBonus"))
+ fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "shieldCapacity",
+ src.getModifiedItemAttr("shieldCapacityBonus"))
diff --git a/eos/effects/skillbonusdroneinterfacing.py b/eos/effects/skillbonusdroneinterfacing.py
index 93093efc3..d832d432b 100644
--- a/eos/effects/skillbonusdroneinterfacing.py
+++ b/eos/effects/skillbonusdroneinterfacing.py
@@ -1,8 +1,6 @@
# skillBonusDroneInterfacing
#
# Used by:
-# Implant: CreoDron 'Bumblebee' Drone Tuner T10-5D
-# Implant: CreoDron 'Yellowjacket' Drone Tuner D5-10T
# Skill: Drone Interfacing
type = "passive"
diff --git a/eos/effects/skillbonusdroneinterfacingnotfighters.py b/eos/effects/skillbonusdroneinterfacingnotfighters.py
new file mode 100644
index 000000000..01ab85de7
--- /dev/null
+++ b/eos/effects/skillbonusdroneinterfacingnotfighters.py
@@ -0,0 +1,11 @@
+# skillBonusDroneInterfacingNotFighters
+#
+# Used by:
+# Implant: CreoDron 'Bumblebee' Drone Tuner T10-5D
+# Implant: CreoDron 'Yellowjacket' Drone Tuner D5-10T
+type = "passive"
+
+
+def handler(fit, src, context):
+ fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "damageMultiplier",
+ src.getModifiedItemAttr("damageMultiplierBonus"))
diff --git a/eos/effects/stripminerdurationmultiplier.py b/eos/effects/stripminerdurationmultiplier.py
new file mode 100644
index 000000000..c9d0179a6
--- /dev/null
+++ b/eos/effects/stripminerdurationmultiplier.py
@@ -0,0 +1,10 @@
+# stripMinerDurationMultiplier
+#
+# Used by:
+# Module: Frostline 'Omnivore' Harvester Upgrade
+type = "passive"
+
+
+def handler(fit, module, context):
+ fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Strip Miner",
+ "duration", module.getModifiedItemAttr("miningDurationMultiplier"))
diff --git a/eos/gamedata.py b/eos/gamedata.py
index f8345ac87..1abb3f69b 100644
--- a/eos/gamedata.py
+++ b/eos/gamedata.py
@@ -239,7 +239,7 @@ class Item(EqBase):
self.__offensive = None
self.__assistive = None
self.__overrides = None
- self.__price = None
+ self.__priceObj = None
@property
def attributes(self):
@@ -446,34 +446,33 @@ class Item(EqBase):
@property
def price(self):
-
# todo: use `from sqlalchemy import inspect` instead (mac-deprecated doesn't have inspect(), was imp[lemented in 0.8)
- if self.__price is not None and getattr(self.__price, '_sa_instance_state', None) and self.__price._sa_instance_state.deleted:
+ if self.__priceObj is not None and getattr(self.__priceObj, '_sa_instance_state', None) and self.__priceObj._sa_instance_state.deleted:
pyfalog.debug("Price data for {} was deleted (probably from a cache reset), resetting object".format(self.ID))
- self.__price = None
+ self.__priceObj = None
- if self.__price is None:
+ if self.__priceObj is None:
db_price = eos.db.getPrice(self.ID)
# do not yet have a price in the database for this item, create one
if db_price is None:
pyfalog.debug("Creating a price for {}".format(self.ID))
- self.__price = types_Price(self.ID)
- eos.db.add(self.__price)
- eos.db.commit()
+ self.__priceObj = types_Price(self.ID)
+ eos.db.add(self.__priceObj)
+ eos.db.flush()
else:
- self.__price = db_price
+ self.__priceObj = db_price
- return self.__price
+ return self.__priceObj
@property
def isAbyssal(self):
if Item.ABYSSAL_TYPES is None:
- Item.getAbyssalYypes()
+ Item.getAbyssalTypes()
return self.ID in Item.ABYSSAL_TYPES
@classmethod
- def getAbyssalYypes(cls):
+ def getAbyssalTypes(cls):
cls.ABYSSAL_TYPES = eos.db.getAbyssalTypes()
@property
@@ -562,6 +561,15 @@ class Unit(EqBase):
self.name = None
self.displayName = None
+ @property
+ def rigSizes(self):
+ return {
+ 1: "Small",
+ 2: "Medium",
+ 3: "Large",
+ 4: "X-Large"
+ }
+
@property
def translations(self):
""" This is a mapping of various tweaks that we have to do between the internal representation of an attribute
@@ -594,10 +602,10 @@ class Unit(EqBase):
lambda u: "m³",
lambda d: d),
"Sizeclass": (
- lambda v: v,
- lambda v: v,
- lambda u: "",
- lambda d: d),
+ lambda v: self.rigSizes[v],
+ lambda v: self.rigSizes[v],
+ lambda d: next(i for i in self.rigSizes.keys() if self.rigSizes[i] == 'Medium'),
+ lambda u: ""),
"Absolute Percent": (
lambda v: v * 100,
lambda v: v * 100,
diff --git a/eos/modifiedAttributeDict.py b/eos/modifiedAttributeDict.py
index d660dd38b..756a9f806 100644
--- a/eos/modifiedAttributeDict.py
+++ b/eos/modifiedAttributeDict.py
@@ -215,6 +215,8 @@ class ModifiedAttributeDict(collections.MutableMapping):
if force is not None:
if cappingValue is not None:
force = min(force, cappingValue)
+ if key in (50, 30, 48, 11):
+ force = round(force, 2)
return force
# Grab our values if they're there, otherwise we'll take default values
preIncrease = self.__preIncreases.get(key, 0)
@@ -268,7 +270,8 @@ class ModifiedAttributeDict(collections.MutableMapping):
# Cap value if we have cap defined
if cappingValue is not None:
val = min(val, cappingValue)
-
+ if key in (50, 30, 48, 11):
+ val = round(val, 2)
return val
def __handleSkill(self, skillName):
diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py
index 4373bcdd0..f33a78651 100644
--- a/eos/saveddata/fit.py
+++ b/eos/saveddata/fit.py
@@ -1016,11 +1016,11 @@ class Fit(object):
@property
def pgUsed(self):
- return self.getItemAttrOnlineSum(self.modules, "power")
+ return round(self.getItemAttrOnlineSum(self.modules, "power"), 2)
@property
def cpuUsed(self):
- return self.getItemAttrOnlineSum(self.modules, "cpu")
+ return round(self.getItemAttrOnlineSum(self.modules, "cpu"), 2)
@property
def droneBandwidthUsed(self):
diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py
index c4c2f0b00..25b15e147 100644
--- a/eos/saveddata/module.py
+++ b/eos/saveddata/module.py
@@ -23,6 +23,7 @@ from logbook import Logger
from sqlalchemy.orm import reconstructor, validates
import eos.db
+from eos.const import Slot
from eos.effectHandlerHelpers import HandledCharge, HandledItem
from eos.enum import Enum
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
@@ -42,30 +43,6 @@ class State(Enum):
OVERHEATED = 2
-class Slot(Enum):
- # These are self-explanatory
- LOW = 1
- MED = 2
- HIGH = 3
- RIG = 4
- SUBSYSTEM = 5
- # not a real slot, need for pyfa display rack separation
- MODE = 6
- # system effects. They are projected "modules" and pyfa assumes all modules
- # have a slot. In this case, make one up.
- SYSTEM = 7
- # used for citadel services
- SERVICE = 8
- # fighter 'slots'. Just easier to put them here...
- F_LIGHT = 10
- F_SUPPORT = 11
- F_HEAVY = 12
- # fighter 'slots' (for structures)
- FS_LIGHT = 13
- FS_SUPPORT = 14
- FS_HEAVY = 15
-
-
ProjectedMap = {
State.OVERHEATED: State.ACTIVE,
State.ACTIVE: State.OFFLINE,
@@ -185,7 +162,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__itemModifiedAttributes.original = self.__item.attributes
self.__itemModifiedAttributes.overrides = self.__item.overrides
self.__hardpoint = self.__calculateHardpoint(self.__item)
- self.__slot = self.__calculateSlot(self.__item)
+ self.__slot = self.calculateSlot(self.__item)
# Instantiate / remove mutators if this is a mutated module
if self.__baseItem:
@@ -755,7 +732,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return Hardpoint.NONE
@staticmethod
- def __calculateSlot(item):
+ def calculateSlot(item):
effectSlotMap = {
"rigSlot" : Slot.RIG,
"loPower" : Slot.LOW,
@@ -772,7 +749,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if item.group.name in Module.SYSTEM_GROUPS:
return Slot.SYSTEM
- raise ValueError("Passed item does not fit in any known slot")
+ return None
@validates("ID", "itemID", "ammoID")
def validator(self, key, val):
diff --git a/eos/saveddata/price.py b/eos/saveddata/price.py
index 6618119d0..a2a630c30 100644
--- a/eos/saveddata/price.py
+++ b/eos/saveddata/price.py
@@ -18,24 +18,30 @@
# along with eos. If not, see .
# ===============================================================================
-import time
-from sqlalchemy.orm import reconstructor
+import time
+from enum import IntEnum, unique
+
from logbook import Logger
+
pyfalog = Logger(__name__)
+@unique
+class PriceStatus(IntEnum):
+ notFetched = 0
+ success = 1
+ fail = 2
+ notSupported = 3
+
+
class Price(object):
def __init__(self, typeID):
self.typeID = typeID
self.time = 0
self.__price = 0
- self.failed = None
-
- @reconstructor
- def init(self):
- self.__item = None
+ self.status = PriceStatus.notFetched
@property
def isValid(self):
@@ -43,7 +49,10 @@ class Price(object):
@property
def price(self):
- return self.__price or 0.0
+ if self.status != PriceStatus.success:
+ return 0
+ else:
+ return self.__price or 0
@price.setter
def price(self, price):
diff --git a/eve.db b/eve.db
index fe0f660a1..6556d4f06 100644
Binary files a/eve.db and b/eve.db differ
diff --git a/gui/aboutData.py b/gui/aboutData.py
index 6e8ce07bc..2d658832f 100644
--- a/gui/aboutData.py
+++ b/gui/aboutData.py
@@ -19,7 +19,7 @@
import config
-versionString = "{0} {1} - {2} {3}".format(config.version, config.tag, config.expansionName, config.expansionVersion)
+versionString = "{0}".format(config.version)
licenses = (
"pyfa is released under GNU GPLv3 - see included LICENSE file",
"All EVE-Online related materials are property of CCP hf.",
diff --git a/gui/bitmap_loader.py b/gui/bitmap_loader.py
index 7a8d41a46..042a6674b 100644
--- a/gui/bitmap_loader.py
+++ b/gui/bitmap_loader.py
@@ -82,9 +82,7 @@ class BitmapLoader(object):
@classmethod
def loadBitmap(cls, name, location):
if cls.scaling_factor is None:
- import gui.mainFrame
- cls.scaling_factor = int(gui.mainFrame.MainFrame.getInstance().GetContentScaleFactor())
-
+ cls.scaling_factor = int(wx.GetApp().GetTopWindow().GetContentScaleFactor())
scale = cls.scaling_factor
filename, img = cls.loadScaledBitmap(name, location, scale)
diff --git a/gui/builtinAdditionPanes/notesView.py b/gui/builtinAdditionPanes/notesView.py
index 35a75b5a8..a4a807509 100644
--- a/gui/builtinAdditionPanes/notesView.py
+++ b/gui/builtinAdditionPanes/notesView.py
@@ -4,6 +4,7 @@ import wx
from service.fit import Fit
import gui.globalEvents as GE
import gui.mainFrame
+from gui.utils.helpers_wxPython import HandleCtrlBackspace
class NotesView(wx.Panel):
@@ -17,9 +18,16 @@ class NotesView(wx.Panel):
self.SetSizer(mainSizer)
self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged)
self.Bind(wx.EVT_TEXT, self.onText)
+ self.editNotes.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
self.saveTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.delayedSave, self.saveTimer)
+ def OnKeyDown(self, event):
+ if event.RawControlDown() and event.GetKeyCode() == wx.WXK_BACK:
+ HandleCtrlBackspace(self.editNotes)
+ else:
+ event.Skip()
+
def fitChanged(self, event):
sFit = Fit.getInstance()
fit = sFit.getFit(event.fitID)
diff --git a/gui/builtinContextMenus/fillWithModule.py b/gui/builtinContextMenus/fillWithModule.py
new file mode 100644
index 000000000..0dd8d0d91
--- /dev/null
+++ b/gui/builtinContextMenus/fillWithModule.py
@@ -0,0 +1,34 @@
+from gui.contextMenu import ContextMenu
+import gui.mainFrame
+# noinspection PyPackageRequirements
+import wx
+import gui.globalEvents as GE
+from service.settings import ContextMenuSettings
+import gui.fitCommands as cmd
+
+
+class FillWithModule(ContextMenu):
+ def __init__(self):
+ self.mainFrame = gui.mainFrame.MainFrame.getInstance()
+ self.settings = ContextMenuSettings.getInstance()
+
+ def display(self, srcContext, selection):
+ if not self.settings.get('moduleFill'):
+ return False
+ return srcContext in ("fittingModule")
+
+ def getText(self, itmContext, selection):
+ return u"Fill With {0}".format(itmContext if itmContext is not None else "Module")
+
+ def activate(self, fullContext, selection, i):
+
+ srcContext = fullContext[0]
+ fitID = self.mainFrame.getActiveFit()
+
+ if srcContext == "fittingModule":
+ self.mainFrame.command.Submit(cmd.GuiFillWithModuleCommand(fitID, selection[0].itemID))
+ return # the command takes care of the PostEvent
+ wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID))
+
+
+FillWithModule.register()
diff --git a/gui/builtinContextMenus/marketJump.py b/gui/builtinContextMenus/marketJump.py
index c630312c1..a5d5e8eeb 100644
--- a/gui/builtinContextMenus/marketJump.py
+++ b/gui/builtinContextMenus/marketJump.py
@@ -26,7 +26,10 @@ class MarketJump(ContextMenu):
sMkt = Market.getInstance()
item = getattr(selection[0], "item", selection[0])
+ isMutated = getattr(selection[0], "isMutated", False)
mktGrp = sMkt.getMarketGroupByItem(item)
+ if mktGrp is None and isMutated:
+ mktGrp = sMkt.getMarketGroupByItem(selection[0].baseItem)
# 1663 is Special Edition Festival Assets, we don't have root group for it
if mktGrp is None or mktGrp.ID == 1663:
@@ -43,7 +46,10 @@ class MarketJump(ContextMenu):
if srcContext in ("fittingCharge", "projectedCharge"):
item = selection[0].charge
elif hasattr(selection[0], "item"):
- item = selection[0].item
+ if getattr(selection[0], "isMutated", False):
+ item = selection[0].baseItem
+ else:
+ item = selection[0].item
else:
item = selection[0]
diff --git a/gui/builtinItemStatsViews/attributeGrouping.py b/gui/builtinItemStatsViews/attributeGrouping.py
new file mode 100644
index 000000000..de466cf73
--- /dev/null
+++ b/gui/builtinItemStatsViews/attributeGrouping.py
@@ -0,0 +1,254 @@
+from enum import Enum, auto
+
+
+# Define the various groups of attributes
+class AttrGroup(Enum):
+ FITTING = auto()
+ STRUCTURE = auto()
+ SHIELD = auto()
+ ARMOR = auto()
+ TARGETING = auto()
+ EWAR_RESISTS = auto()
+ CAPACITOR = auto()
+ SHARED_FACILITIES = auto()
+ FIGHTER_FACILITIES = auto()
+ ON_DEATH = auto()
+ JUMP_SYSTEMS = auto()
+ PROPULSIONS = auto()
+ FIGHTERS = auto()
+
+
+RequiredSkillAttrs = sum((["requiredSkill{}".format(x), "requiredSkill{}Level".format(x)] for x in range(1, 7)), [])
+
+#todo: maybe moved some of these basic definitions into eos proper? Can really be useful with effect writing as a lot of these are used over and over
+damage_types = ["em", "thermal", "kinetic", "explosive"]
+scan_types = ["radar", "magnetometric", "gravimetric", "ladar"]
+
+DamageAttrs = ["{}Damage".format(x) for x in damage_types]
+HullResistsAttrs = ["{}DamageResonance".format(x) for x in damage_types]
+ArmorResistsAttrs = ["armor{}DamageResonance".format(x.capitalize()) for x in damage_types]
+ShieldResistsAttrs = ["shield{}DamageResonance".format(x.capitalize()) for x in damage_types]
+ScanStrAttrs = ["scan{}Strength".format(x.capitalize()) for x in scan_types]
+
+# todo: convert to named tuples?
+AttrGroups = [
+ (DamageAttrs, "Damage"),
+ (HullResistsAttrs, "Resistances"),
+ (ArmorResistsAttrs, "Resistances"),
+ (ShieldResistsAttrs, "Resistances"),
+ (ScanStrAttrs, "Sensor Strengths")
+]
+
+GroupedAttributes = []
+for x in AttrGroups:
+ GroupedAttributes += x[0]
+
+# Start defining all the known attribute groups
+AttrGroupDict = {
+ AttrGroup.FITTING : {
+ "label" : "Fitting",
+ "attributes": [
+ # parent-level attributes
+ "cpuOutput",
+ "powerOutput",
+ "upgradeCapacity",
+ "hiSlots",
+ "medSlots",
+ "lowSlots",
+ "serviceSlots",
+ "turretSlotsLeft",
+ "launcherSlotsLeft",
+ "upgradeSlotsLeft",
+ # child-level attributes
+ "cpu",
+ "power",
+ "rigSize",
+ "upgradeCost",
+ # "mass",
+ ]
+ },
+ AttrGroup.STRUCTURE : {
+ "label" : "Structure",
+ "attributes": [
+ "hp",
+ "capacity",
+ "mass",
+ "volume",
+ "agility",
+ "droneCapacity",
+ "droneBandwidth",
+ "specialOreHoldCapacity",
+ "specialGasHoldCapacity",
+ "specialMineralHoldCapacity",
+ "specialSalvageHoldCapacity",
+ "specialShipHoldCapacity",
+ "specialSmallShipHoldCapacity",
+ "specialMediumShipHoldCapacity",
+ "specialLargeShipHoldCapacity",
+ "specialIndustrialShipHoldCapacity",
+ "specialAmmoHoldCapacity",
+ "specialCommandCenterHoldCapacity",
+ "specialPlanetaryCommoditiesHoldCapacity",
+ "structureDamageLimit",
+ "specialSubsystemHoldCapacity",
+ "emDamageResonance",
+ "thermalDamageResonance",
+ "kineticDamageResonance",
+ "explosiveDamageResonance"
+ ]
+ },
+ AttrGroup.ARMOR : {
+ "label": "Armor",
+ "attributes":[
+ "armorHP",
+ "armorDamageLimit",
+ "armorEmDamageResonance",
+ "armorThermalDamageResonance",
+ "armorKineticDamageResonance",
+ "armorExplosiveDamageResonance",
+ ]
+
+ },
+ AttrGroup.SHIELD : {
+ "label": "Shield",
+ "attributes": [
+ "shieldCapacity",
+ "shieldRechargeRate",
+ "shieldDamageLimit",
+ "shieldEmDamageResonance",
+ "shieldExplosiveDamageResonance",
+ "shieldKineticDamageResonance",
+ "shieldThermalDamageResonance",
+ ]
+
+ },
+ AttrGroup.EWAR_RESISTS : {
+ "label": "Electronic Warfare",
+ "attributes": [
+ "ECMResistance",
+ "remoteAssistanceImpedance",
+ "remoteRepairImpedance",
+ "energyWarfareResistance",
+ "sensorDampenerResistance",
+ "stasisWebifierResistance",
+ "targetPainterResistance",
+ "weaponDisruptionResistance",
+ ]
+ },
+ AttrGroup.CAPACITOR : {
+ "label": "Capacitor",
+ "attributes": [
+ "capacitorCapacity",
+ "rechargeRate",
+ ]
+ },
+ AttrGroup.TARGETING : {
+ "label": "Targeting",
+ "attributes": [
+ "maxTargetRange",
+ "maxRange",
+ "maxLockedTargets",
+ "signatureRadius",
+ "optimalSigRadius",
+ "scanResolution",
+ "proximityRange",
+ "falloff",
+ "trackingSpeed",
+ "scanRadarStrength",
+ "scanMagnetometricStrength",
+ "scanGravimetricStrength",
+ "scanLadarStrength",
+ ]
+ },
+ AttrGroup.SHARED_FACILITIES : {
+ "label" : "Shared Facilities",
+ "attributes": [
+ "fleetHangarCapacity",
+ "shipMaintenanceBayCapacity",
+ "maxJumpClones",
+ ]
+ },
+ AttrGroup.FIGHTER_FACILITIES: {
+ "label": "Fighter Squadron Facilities",
+ "attributes": [
+ "fighterCapacity",
+ "fighterTubes",
+ "fighterLightSlots",
+ "fighterSupportSlots",
+ "fighterHeavySlots",
+ "fighterStandupLightSlots",
+ "fighterStandupSupportSlots",
+ "fighterStandupHeavySlots",
+ ]
+ },
+ AttrGroup.ON_DEATH : {
+ "label": "On Death",
+ "attributes": [
+ "onDeathDamageEM",
+ "onDeathDamageTherm",
+ "onDeathDamageKin",
+ "onDeathDamageExp",
+ "onDeathAOERadius",
+ "onDeathSignatureRadius",
+ ]
+ },
+ AttrGroup.JUMP_SYSTEMS : {
+ "label": "Jump Drive Systems",
+ "attributes": [
+ "jumpDriveCapacitorNeed",
+ "jumpDriveRange",
+ "jumpDriveConsumptionType",
+ "jumpDriveConsumptionAmount",
+ "jumpPortalCapacitorNeed",
+ "jumpDriveDuration",
+ "specialFuelBayCapacity",
+ "jumpPortalConsumptionMassFactor",
+ "jumpPortalDuration",
+ ]
+ },
+ AttrGroup.PROPULSIONS : {
+ "label": "Propulsion",
+ "attributes": [
+ "maxVelocity"
+ ]
+ },
+ AttrGroup.FIGHTERS : {
+ "label": "Fighter",
+ "attributes": [
+ "mass",
+ "maxVelocity",
+ "agility",
+ "volume",
+ "signatureRadius",
+ "fighterSquadronMaxSize",
+ "fighterRefuelingTime",
+ "fighterSquadronOrbitRange",
+ ]
+ },
+}
+
+Group1 = [
+ AttrGroup.FITTING,
+ AttrGroup.STRUCTURE,
+ AttrGroup.ARMOR,
+ AttrGroup.SHIELD,
+ AttrGroup.EWAR_RESISTS,
+ AttrGroup.CAPACITOR,
+ AttrGroup.TARGETING,
+ AttrGroup.SHARED_FACILITIES,
+ AttrGroup.FIGHTER_FACILITIES,
+ AttrGroup.ON_DEATH,
+ AttrGroup.JUMP_SYSTEMS,
+ AttrGroup.PROPULSIONS,
+]
+
+CategoryGroups = {
+ "Fighter" : [
+ AttrGroup.FIGHTERS,
+ AttrGroup.SHIELD,
+ AttrGroup.TARGETING,
+ ],
+ "Ship" : Group1,
+ "Drone" : Group1,
+ "Structure": Group1
+}
diff --git a/gui/builtinItemStatsViews/attributeSlider.py b/gui/builtinItemStatsViews/attributeSlider.py
index e45fa8f49..9e3585ea9 100644
--- a/gui/builtinItemStatsViews/attributeSlider.py
+++ b/gui/builtinItemStatsViews/attributeSlider.py
@@ -58,7 +58,6 @@ class AttributeSlider(wx.Panel):
self.UserMinValue = minValue
self.UserMaxValue = maxValue
- print(self.UserMinValue, self.UserMaxValue)
self.inverse = inverse
diff --git a/gui/builtinItemStatsViews/itemAttributes.py b/gui/builtinItemStatsViews/itemAttributes.py
index 45eb6ec04..747f25066 100644
--- a/gui/builtinItemStatsViews/itemAttributes.py
+++ b/gui/builtinItemStatsViews/itemAttributes.py
@@ -3,11 +3,18 @@ import config
# noinspection PyPackageRequirements
import wx
-
-from .helpers import AutoListCtrl
+import wx.lib.agw.hypertreelist
+from gui.builtinItemStatsViews.helpers import AutoListCtrl
from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount, roundDec
+from enum import IntEnum
+from gui.builtinItemStatsViews.attributeGrouping import *
+
+
+class AttributeView(IntEnum):
+ NORMAL = 1
+ RAW = -1
class ItemParams(wx.Panel):
@@ -15,8 +22,9 @@ class ItemParams(wx.Panel):
wx.Panel.__init__(self, parent)
mainSizer = wx.BoxSizer(wx.VERTICAL)
- self.paramList = AutoListCtrl(self, wx.ID_ANY,
- style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES | wx.NO_BORDER)
+ self.paramList = wx.lib.agw.hypertreelist.HyperTreeList(self, wx.ID_ANY, agwStyle=wx.TR_HIDE_ROOT | wx.TR_NO_LINES | wx.TR_FULL_ROW_HIGHLIGHT | wx.TR_HAS_BUTTONS)
+ self.paramList.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
+
mainSizer.Add(self.paramList, 1, wx.ALL | wx.EXPAND, 0)
self.SetSizer(mainSizer)
@@ -27,14 +35,19 @@ class ItemParams(wx.Panel):
self.attrValues = {}
self._fetchValues()
+ self.paramList.AddColumn("Attribute")
+ self.paramList.AddColumn("Current Value")
+ if self.stuff is not None:
+ self.paramList.AddColumn("Base Value")
+
+ self.paramList.SetMainColumn(0) # the one with the tree in it...
+ self.paramList.SetColumnWidth(0, 300)
+
self.m_staticline = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL)
mainSizer.Add(self.m_staticline, 0, wx.EXPAND)
bSizer = wx.BoxSizer(wx.HORIZONTAL)
- self.totalAttrsLabel = wx.StaticText(self, wx.ID_ANY, " ", wx.DefaultPosition, wx.DefaultSize, 0)
- bSizer.Add(self.totalAttrsLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT)
-
- self.toggleViewBtn = wx.ToggleButton(self, wx.ID_ANY, "Toggle view mode", wx.DefaultPosition, wx.DefaultSize,
+ self.toggleViewBtn = wx.ToggleButton(self, wx.ID_ANY, "Veiw Raw Data", wx.DefaultPosition, wx.DefaultSize,
0)
bSizer.Add(self.toggleViewBtn, 0, wx.ALIGN_CENTER_VERTICAL)
@@ -76,10 +89,10 @@ class ItemParams(wx.Panel):
def UpdateList(self):
self.Freeze()
- self.paramList.ClearAll()
+ self.paramList.DeleteRoot()
self.PopulateList()
self.Thaw()
- self.paramList.resizeLastColumn(100)
+ # self.paramList.resizeLastColumn(100)
def RefreshValues(self, event):
self._fetchValues()
@@ -151,89 +164,154 @@ class ItemParams(wx.Panel):
]
)
+ def AddAttribute(self, parent, attr):
+ if attr in self.attrValues and attr not in self.processed_attribs:
+
+ data = self.GetData(attr)
+ if data is None:
+ return
+
+ attrIcon, attrName, currentVal, baseVal = data
+ attr_item = self.paramList.AppendItem(parent, attrName)
+
+ self.paramList.SetItemText(attr_item, currentVal, 1)
+ if self.stuff is not None:
+ self.paramList.SetItemText(attr_item, baseVal, 2)
+ self.paramList.SetItemImage(attr_item, attrIcon, which=wx.TreeItemIcon_Normal)
+ self.processed_attribs.add(attr)
+
+ def ExpandOrDelete(self, item):
+ if self.paramList.GetChildrenCount(item) == 0:
+ self.paramList.Delete(item)
+ else:
+ self.paramList.Expand(item)
+
def PopulateList(self):
- self.paramList.InsertColumn(0, "Attribute")
- self.paramList.InsertColumn(1, "Current Value")
- if self.stuff is not None:
- self.paramList.InsertColumn(2, "Base Value")
- self.paramList.SetColumnWidth(0, 110)
- self.paramList.SetColumnWidth(1, 90)
- if self.stuff is not None:
- self.paramList.SetColumnWidth(2, 90)
- self.paramList.setResizeColumn(0)
+ # self.paramList.setResizeColumn(0)
self.imageList = wx.ImageList(16, 16)
- self.paramList.SetImageList(self.imageList, wx.IMAGE_LIST_SMALL)
+
+ self.processed_attribs = set()
+ root = self.paramList.AddRoot("The Root Item")
+ misc_parent = root
+
+ # We must first deet4ermine if it's categorey already has defined groupings set for it. Otherwise, we default to just using the fitting group
+ order = CategoryGroups.get(self.item.category.categoryName, [AttrGroup.FITTING])
+ # start building out the tree
+ for data in [AttrGroupDict[o] for o in order]:
+ heading = data.get("label")
+
+ header_item = self.paramList.AppendItem(root, heading)
+ for attr in data.get("attributes", []):
+ # Attribute is a "grouped" attr (eg: damage, sensor strengths, etc). Automatically group these into a child item
+ if attr in GroupedAttributes:
+ # find which group it's in
+ for grouping in AttrGroups:
+ if attr in grouping[0]:
+ break
+
+ # create a child item with the groups label
+ item = self.paramList.AppendItem(header_item, grouping[1])
+ for attr2 in grouping[0]:
+ # add each attribute in the group
+ self.AddAttribute(item, attr2)
+
+ self.ExpandOrDelete(item)
+ continue
+
+ self.AddAttribute(header_item, attr)
+
+ self.ExpandOrDelete(header_item)
names = list(self.attrValues.keys())
names.sort()
- idNameMap = {}
- idCount = 0
+ # this will take care of any attributes that weren't collected withe the defined grouping (or all attributes if the item ddidn't have anything defined)
for name in names:
- info = self.attrInfo.get(name)
- att = self.attrValues[name]
+ if name in GroupedAttributes:
+ # find which group it's in
+ for grouping in AttrGroups:
+ if name in grouping[0]:
+ break
- # If we're working with a stuff object, we should get the original value from our getBaseAttrValue function,
- # which will return the value with respect to the effective base (with mutators / overrides in place)
- valDefault = getattr(info, "value", None) # Get default value from attribute
- if self.stuff is not None:
- # if it's a stuff, overwrite default (with fallback to current value)
- valDefault = self.stuff.getBaseAttrValue(name, valDefault)
- valueDefault = valDefault if valDefault is not None else att
+ # get all attributes in group
+ item = self.paramList.AppendItem(root, grouping[1])
+ for attr2 in grouping[0]:
+ self.AddAttribute(item, attr2)
- val = getattr(att, "value", None)
- value = val if val is not None else att
+ self.ExpandOrDelete(item)
+ continue
- if info and info.displayName and self.toggleView == 1:
- attrName = info.displayName
- else:
- attrName = name
+ self.AddAttribute(root, name)
- if info and config.debug:
- attrName += " ({})".format(info.ID)
+ self.paramList.AssignImageList(self.imageList)
+ self.Layout()
- if info:
- if info.iconID is not None:
- iconFile = info.iconID
- icon = BitmapLoader.getBitmap(iconFile, "icons")
+ def GetData(self, attr):
+ info = self.attrInfo.get(attr)
+ att = self.attrValues[attr]
- if icon is None:
- icon = BitmapLoader.getBitmap("transparent16x16", "gui")
+ # If we're working with a stuff object, we should get the original value from our getBaseAttrValue function,
+ # which will return the value with respect to the effective base (with mutators / overrides in place)
+ valDefault = getattr(info, "value", None) # Get default value from attribute
+ if self.stuff is not None:
+ # if it's a stuff, overwrite default (with fallback to current value)
+ valDefault = self.stuff.getBaseAttrValue(attr, valDefault)
+ valueDefault = valDefault if valDefault is not None else att
- attrIcon = self.imageList.Add(icon)
- else:
- attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons"))
+ val = getattr(att, "value", None)
+ value = val if val is not None else att
+
+ if self.toggleView == AttributeView.NORMAL and ((attr not in GroupedAttributes and not value) or info is None or not info.published or attr in RequiredSkillAttrs):
+ return None
+
+ if info and info.displayName and self.toggleView == 1:
+ attrName = info.displayName
+ else:
+ attrName = attr
+
+ if info and config.debug:
+ attrName += " ({})".format(info.ID)
+
+ if info:
+ if info.iconID is not None:
+ iconFile = info.iconID
+ icon = BitmapLoader.getBitmap(iconFile, "icons")
+
+ if icon is None:
+ icon = BitmapLoader.getBitmap("transparent16x16", "gui")
+
+ attrIcon = self.imageList.Add(icon)
else:
attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons"))
+ else:
+ attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons"))
- index = self.paramList.InsertItem(self.paramList.GetItemCount(), attrName, attrIcon)
- idNameMap[idCount] = attrName
- self.paramList.SetItemData(index, idCount)
- idCount += 1
+ # index = self.paramList.AppendItem(root, attrName)
+ # idNameMap[idCount] = attrName
+ # self.paramList.SetPyData(index, idCount)
+ # idCount += 1
- if self.toggleView != 1:
- valueUnit = str(value)
- elif info and info.unit:
- valueUnit = self.FormatValue(*info.unit.PreformatValue(value))
- else:
- valueUnit = formatAmount(value, 3, 0, 0)
+ if self.toggleView != 1:
+ valueUnit = str(value)
+ elif info and info.unit:
+ valueUnit = self.FormatValue(*info.unit.PreformatValue(value))
+ else:
+ valueUnit = formatAmount(value, 3, 0, 0)
- if self.toggleView != 1:
- valueUnitDefault = str(valueDefault)
- elif info and info.unit:
- valueUnitDefault = self.FormatValue(*info.unit.PreformatValue(valueDefault))
- else:
- valueUnitDefault = formatAmount(valueDefault, 3, 0, 0)
+ if self.toggleView != 1:
+ valueUnitDefault = str(valueDefault)
+ elif info and info.unit:
+ valueUnitDefault = self.FormatValue(*info.unit.PreformatValue(valueDefault))
+ else:
+ valueUnitDefault = formatAmount(valueDefault, 3, 0, 0)
- self.paramList.SetItem(index, 1, valueUnit)
- if self.stuff is not None:
- self.paramList.SetItem(index, 2, valueUnitDefault)
- # @todo: pheonix, this lamda used cmp() which no longer exists in py3. Probably a better way to do this in the
- # long run, take a look
- self.paramList.SortItems(lambda id1, id2: (idNameMap[id1] > idNameMap[id2]) - (idNameMap[id1] < idNameMap[id2]))
- self.paramList.RefreshRows()
- self.totalAttrsLabel.SetLabel("%d attributes. " % idCount)
- self.Layout()
+ # todo: attribute that point to another item should load that item's icon.
+ return (attrIcon, attrName, valueUnit, valueUnitDefault)
+
+ # self.paramList.SetItemText(index, valueUnit, 1)
+ # if self.stuff is not None:
+ # self.paramList.SetItemText(index, valueUnitDefault, 2)
+ # self.paramList.SetItemImage(index, attrIcon, which=wx.TreeItemIcon_Normal)
@staticmethod
def FormatValue(value, unit, rounding='prec', digits=3):
@@ -246,3 +324,43 @@ class ItemParams(wx.Panel):
else:
fvalue = value
return "%s %s" % (fvalue, unit)
+
+
+if __name__ == "__main__":
+
+ import eos.db
+ # need to set up some paths, since bitmap loader requires config to have things
+ # Should probably change that so that it's not dependant on config
+ import os
+ os.chdir('..')
+ import config
+ config.defPaths(None)
+ config.debug = True
+ class Frame(wx.Frame):
+ def __init__(self, ):
+ # item = eos.db.getItem(23773) # Ragnarok
+ item = eos.db.getItem(23061) # Einherji I
+ #item = eos.db.getItem(24483) # Nidhoggur
+ #item = eos.db.getItem(587) # Rifter
+ #item = eos.db.getItem(2486) # Warrior I
+ #item = eos.db.getItem(526) # Stasis Webifier I
+ item = eos.db.getItem(486) # 200mm AutoCannon I
+ #item = eos.db.getItem(200) # Phased Plasma L
+ super().__init__(None, title="Test Attribute Window | {} - {}".format(item.ID, item.name), size=(1000, 500))
+
+ if 'wxMSW' in wx.PlatformInfo:
+ color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)
+ self.SetBackgroundColour(color)
+
+ main_sizer = wx.BoxSizer(wx.HORIZONTAL)
+
+ panel = ItemParams(self, None, item)
+
+ main_sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 2)
+
+ self.SetSizer(main_sizer)
+
+ app = wx.App(redirect=False) # Error messages go to popup window
+ top = Frame()
+ top.Show()
+ app.MainLoop()
diff --git a/gui/builtinItemStatsViews/itemCompare.py b/gui/builtinItemStatsViews/itemCompare.py
index 1561ed58b..97a4b953d 100644
--- a/gui/builtinItemStatsViews/itemCompare.py
+++ b/gui/builtinItemStatsViews/itemCompare.py
@@ -8,6 +8,10 @@ from service.attribute import Attribute
from gui.utils.numberFormatter import formatAmount
+def defaultSort(item):
+ return (item.attributes['metaLevel'].value if 'metaLevel' in item.attributes else 0, item.name)
+
+
class ItemCompare(wx.Panel):
def __init__(self, parent, stuff, item, items, context=None):
# Start dealing with Price stuff to get that thread going
@@ -27,8 +31,7 @@ class ItemCompare(wx.Panel):
self.currentSort = None
self.sortReverse = False
self.item = item
- self.items = sorted(items,
- key=lambda x: x.attributes['metaLevel'].value if 'metaLevel' in x.attributes else 0)
+ self.items = sorted(items, key=defaultSort)
self.attrs = {}
# get a dict of attrName: attrInfo of all unique attributes across all items
@@ -126,10 +129,15 @@ class ItemCompare(wx.Panel):
# starts at 0 while the list has the item name as column 0.
attr = str(list(self.attrs.keys())[sort - 1])
func = lambda _val: _val.attributes[attr].value if attr in _val.attributes else 0.0
+ # Clicked on a column that's not part of our array (price most likely)
except IndexError:
- # Clicked on a column that's not part of our array (price most likely)
- self.sortReverse = False
- func = lambda _val: _val.attributes['metaLevel'].value if 'metaLevel' in _val.attributes else 0.0
+ # Price
+ if sort == len(self.attrs) + 1:
+ func = lambda i: i.price.price if i.price.price != 0 else float("Inf")
+ # Something else
+ else:
+ self.sortReverse = False
+ func = defaultSort
self.items = sorted(self.items, key=func, reverse=self.sortReverse)
@@ -160,7 +168,7 @@ class ItemCompare(wx.Panel):
self.paramList.SetItem(i, x + 1, valueUnit)
# Add prices
- self.paramList.SetItem(i, len(self.attrs) + 1, formatAmount(item.price.price, 3, 3, 9, currency=True))
+ self.paramList.SetItem(i, len(self.attrs) + 1, formatAmount(item.price.price, 3, 3, 9, currency=True) if item.price.price else "")
self.paramList.RefreshRows()
self.Layout()
diff --git a/gui/builtinItemStatsViews/itemDescription.py b/gui/builtinItemStatsViews/itemDescription.py
index 475b88788..9c62dcc78 100644
--- a/gui/builtinItemStatsViews/itemDescription.py
+++ b/gui/builtinItemStatsViews/itemDescription.py
@@ -15,7 +15,6 @@ class ItemDescription(wx.Panel):
fgcolor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)
self.description = wx.html.HtmlWindow(self)
-
if not item.description:
return
diff --git a/gui/builtinItemStatsViews/itemMutator.py b/gui/builtinItemStatsViews/itemMutator.py
index cf7568350..dc91453fa 100644
--- a/gui/builtinItemStatsViews/itemMutator.py
+++ b/gui/builtinItemStatsViews/itemMutator.py
@@ -22,8 +22,23 @@ class ItemMutator(wx.Panel):
self.item = item
self.timer = None
self.activeFit = gui.mainFrame.MainFrame.getInstance().getActiveFit()
+
+ font = parent.GetFont()
+ font.SetWeight(wx.BOLD)
+
mainSizer = wx.BoxSizer(wx.VERTICAL)
+ sourceItemsSizer = wx.BoxSizer(wx.HORIZONTAL)
+ sourceItemsSizer.Add(BitmapLoader.getStaticBitmap(stuff.item.iconID, self, "icons"), 0, wx.LEFT, 5)
+ sourceItemsSizer.Add(BitmapLoader.getStaticBitmap(stuff.mutaplasmid.item.iconID, self, "icons"), 0, wx.LEFT, 0)
+ sourceItemShort = "{} {}".format(stuff.mutaplasmid.item.name.split(" ")[0], stuff.baseItem.name)
+ sourceItemText = wx.StaticText(self, wx.ID_ANY, sourceItemShort)
+ sourceItemText.SetFont(font)
+ sourceItemsSizer.Add(sourceItemText, 0, wx.LEFT, 10)
+ mainSizer.Add(sourceItemsSizer, 0, wx.TOP | wx.EXPAND, 10)
+
+ mainSizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.ALL | wx.EXPAND, 5)
+
self.goodColor = wx.Colour(96, 191, 0)
self.badColor = wx.Colour(255, 64, 0)
@@ -31,28 +46,43 @@ class ItemMutator(wx.Panel):
for m in sorted(stuff.mutators.values(), key=lambda x: x.attribute.displayName):
# Format: [raw value, modifier applied to base raw value, display value]
- range1 = (m.minValue, m.minMod, m.attribute.unit.SimplifyValue(m.minValue))
- range2 = (m.maxValue, m.maxMod, m.attribute.unit.SimplifyValue(m.maxValue))
+ range1 = (m.minValue, m.attribute.unit.SimplifyValue(m.minValue))
+ range2 = (m.maxValue, m.attribute.unit.SimplifyValue(m.maxValue))
- if (m.highIsGood and range1[0] >= range2[0]) or (not m.highIsGood and range1[0] <= range2[0]):
- betterRange = range1
- worseRange = range2
+ # minValue/maxValue do not always correspond to min/max, because these are
+ # just base value multiplied by minMod/maxMod, and in case base is negative
+ # minValue is actually bigger than maxValue
+ if range1[0] <= range2[0]:
+ minRange = range1
+ maxRange = range2
else:
- betterRange = range2
- worseRange = range1
+ minRange = range2
+ maxRange = range1
- if range1[2] >= range2[2]:
- displayMaxRange = range1
- displayMinRange = range2
+ if (m.highIsGood and minRange[0] >= maxRange[0]) or (not m.highIsGood and minRange[0] <= maxRange[0]):
+ betterRange = minRange
+ worseRange = maxRange
else:
- displayMaxRange = range2
- displayMinRange = range1
+ betterRange = maxRange
+ worseRange = minRange
+
+ if minRange[1] >= maxRange[1]:
+ displayMaxRange = minRange
+ displayMinRange = maxRange
+ else:
+ displayMaxRange = maxRange
+ displayMinRange = minRange
+
+ # If base value is outside of mutation range, make sure that center of slider
+ # corresponds to the value which is closest available to actual base value. It's
+ # how EVE handles it
+ if minRange[0] <= m.baseValue <= maxRange[0]:
+ sliderBaseValue = m.baseValue
+ else:
+ sliderBaseValue = max(minRange[0], min(maxRange[0], m.baseValue))
headingSizer = wx.BoxSizer(wx.HORIZONTAL)
- font = parent.GetFont()
- font.SetWeight(wx.BOLD)
-
headingSizer.Add(BitmapLoader.getStaticBitmap(m.attribute.iconID, self, "icons"), 0, wx.RIGHT, 10)
displayName = wx.StaticText(self, wx.ID_ANY, m.attribute.displayName)
@@ -75,9 +105,9 @@ class ItemMutator(wx.Panel):
mainSizer.Add(headingSizer, 0, wx.ALL | wx.EXPAND, 5)
slider = AttributeSlider(parent=self,
- baseValue=m.attribute.unit.SimplifyValue(m.baseValue),
- minValue=displayMinRange[2],
- maxValue=displayMaxRange[2],
+ baseValue=m.attribute.unit.SimplifyValue(sliderBaseValue),
+ minValue=displayMinRange[1],
+ maxValue=displayMaxRange[1],
inverse=displayMaxRange is worseRange)
slider.SetValue(m.attribute.unit.SimplifyValue(m.value), False)
slider.Bind(EVT_VALUE_CHANGED, self.changeMutatedValue)
diff --git a/gui/builtinMarketBrowser/itemView.py b/gui/builtinMarketBrowser/itemView.py
index 82454502a..258c8800f 100644
--- a/gui/builtinMarketBrowser/itemView.py
+++ b/gui/builtinMarketBrowser/itemView.py
@@ -1,6 +1,7 @@
import wx
from logbook import Logger
+from eos.saveddata.module import Module
import gui.builtinMarketBrowser.pfSearchBox as SBox
from gui.builtinMarketBrowser.events import ItemSelected, MAX_RECENTLY_USED_MODULES, RECENTLY_USED_MODULES
from gui.contextMenu import ContextMenu
@@ -8,6 +9,7 @@ from gui.display import Display
from gui.utils.staticHelpers import DragDropHelper
from service.attribute import Attribute
from service.fit import Fit
+from config import slotColourMap
pyfalog = Logger(__name__)
@@ -28,6 +30,7 @@ class ItemView(Display):
self.recentlyUsedModules = set()
self.sMkt = marketBrowser.sMkt
self.searchMode = marketBrowser.searchMode
+ self.sFit = Fit.getInstance()
self.marketBrowser = marketBrowser
self.marketView = marketBrowser.marketView
@@ -266,3 +269,9 @@ class ItemView(Display):
revmap[mgid] = i
i += 1
return revmap
+
+ def columnBackground(self, colItem, item):
+ if self.sFit.serviceFittingOptions["colorFitBySlot"]:
+ return slotColourMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour()
+ else:
+ return self.GetBackgroundColour()
diff --git a/gui/builtinMarketBrowser/pfSearchBox.py b/gui/builtinMarketBrowser/pfSearchBox.py
index 8d0871f6a..38706a34d 100644
--- a/gui/builtinMarketBrowser/pfSearchBox.py
+++ b/gui/builtinMarketBrowser/pfSearchBox.py
@@ -2,6 +2,7 @@
import wx
import gui.utils.color as colorUtils
import gui.utils.draw as drawUtils
+from gui.utils.helpers_wxPython import HandleCtrlBackspace
SearchButton, EVT_SEARCH_BTN = wx.lib.newevent.NewEvent()
CancelButton, EVT_CANCEL_BTN = wx.lib.newevent.NewEvent()
@@ -55,7 +56,7 @@ class PFSearchBox(wx.Window):
self.EditBox.Bind(wx.EVT_SET_FOCUS, self.OnEditSetFocus)
self.EditBox.Bind(wx.EVT_KILL_FOCUS, self.OnEditKillFocus)
-
+ self.EditBox.Bind(wx.EVT_KEY_DOWN, self.OnKeyPress)
self.EditBox.Bind(wx.EVT_TEXT, self.OnText)
self.EditBox.Bind(wx.EVT_TEXT_ENTER, self.OnTextEnter)
@@ -83,6 +84,12 @@ class PFSearchBox(wx.Window):
self.Clear()
event.Skip()
+ def OnKeyPress(self, event):
+ if event.RawControlDown() and event.GetKeyCode() == wx.WXK_BACK:
+ HandleCtrlBackspace(self.EditBox)
+ else:
+ event.Skip()
+
def Clear(self):
self.EditBox.Clear()
# self.EditBox.ChangeValue(self.descriptiveText)
diff --git a/gui/builtinPreferenceViews/pyfaContextMenuPreferences.py b/gui/builtinPreferenceViews/pyfaContextMenuPreferences.py
index 458424dce..7c767c5fb 100644
--- a/gui/builtinPreferenceViews/pyfaContextMenuPreferences.py
+++ b/gui/builtinPreferenceViews/pyfaContextMenuPreferences.py
@@ -81,6 +81,11 @@ class PFContextMenuPref(PreferenceView):
rbSizerRow3.Add(self.rbBox7, 1, wx.TOP | wx.RIGHT, 5)
self.rbBox7.Bind(wx.EVT_RADIOBOX, self.OnSetting7Change)
+ self.rbBox8 = wx.RadioBox(panel, -1, "Fill with module", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS)
+ self.rbBox8.SetSelection(self.settings.get('moduleFill'))
+ rbSizerRow3.Add(self.rbBox8, 1, wx.TOP | wx.RIGHT, 5)
+ self.rbBox8.Bind(wx.EVT_RADIOBOX, self.OnSetting8Change)
+
mainSizer.Add(rbSizerRow3, 1, wx.ALL | wx.EXPAND, 0)
panel.SetSizer(mainSizer)
@@ -107,6 +112,9 @@ class PFContextMenuPref(PreferenceView):
def OnSetting7Change(self, event):
self.settings.set('project', event.GetInt())
+ def OnSetting8Change(self, event):
+ self.settings.set('moduleFill', event.GetInt())
+
def getImage(self):
return BitmapLoader.getBitmap("settings_menu", "gui")
diff --git a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py
index a31e56f8a..8b1c0f27a 100644
--- a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py
+++ b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py
@@ -76,10 +76,6 @@ class PFGeneralPref(PreferenceView):
self.cbGaugeAnimation = wx.CheckBox(panel, wx.ID_ANY, "Animate gauges", wx.DefaultPosition, wx.DefaultSize, 0)
mainSizer.Add(self.cbGaugeAnimation, 0, wx.ALL | wx.EXPAND, 5)
- self.cbExportCharges = wx.CheckBox(panel, wx.ID_ANY, "Export loaded charges", wx.DefaultPosition,
- wx.DefaultSize, 0)
- mainSizer.Add(self.cbExportCharges, 0, wx.ALL | wx.EXPAND, 5)
-
self.cbOpenFitInNew = wx.CheckBox(panel, wx.ID_ANY, "Open fittings in a new page by default",
wx.DefaultPosition, wx.DefaultSize, 0)
mainSizer.Add(self.cbOpenFitInNew, 0, wx.ALL | wx.EXPAND, 5)
@@ -133,7 +129,6 @@ class PFGeneralPref(PreferenceView):
self.cbShowTooltip.SetValue(self.sFit.serviceFittingOptions["showTooltip"] or False)
self.cbMarketShortcuts.SetValue(self.sFit.serviceFittingOptions["showMarketShortcuts"] or False)
self.cbGaugeAnimation.SetValue(self.sFit.serviceFittingOptions["enableGaugeAnimation"])
- self.cbExportCharges.SetValue(self.sFit.serviceFittingOptions["exportCharges"])
self.cbOpenFitInNew.SetValue(self.sFit.serviceFittingOptions["openFitInNew"])
self.chPriceSource.SetStringSelection(self.sFit.serviceFittingOptions["priceSource"])
self.chPriceSystem.SetStringSelection(self.sFit.serviceFittingOptions["priceSystem"])
@@ -151,7 +146,6 @@ class PFGeneralPref(PreferenceView):
self.cbShowTooltip.Bind(wx.EVT_CHECKBOX, self.onCBShowTooltip)
self.cbMarketShortcuts.Bind(wx.EVT_CHECKBOX, self.onCBShowShortcuts)
self.cbGaugeAnimation.Bind(wx.EVT_CHECKBOX, self.onCBGaugeAnimation)
- self.cbExportCharges.Bind(wx.EVT_CHECKBOX, self.onCBExportCharges)
self.cbOpenFitInNew.Bind(wx.EVT_CHECKBOX, self.onCBOpenFitInNew)
self.chPriceSource.Bind(wx.EVT_CHOICE, self.onPricesSourceSelection)
self.chPriceSystem.Bind(wx.EVT_CHOICE, self.onPriceSelection)
@@ -168,9 +162,16 @@ class PFGeneralPref(PreferenceView):
event.Skip()
def onCBGlobalColorBySlot(self, event):
+ # todo: maybe create a SettingChanged event that we can fire, and have other things hook into, instead of having the preference panel itself handle the
+ # updating of things related to settings.
self.sFit.serviceFittingOptions["colorFitBySlot"] = self.cbFitColorSlots.GetValue()
fitID = self.mainFrame.getActiveFit()
self.sFit.refreshFit(fitID)
+
+ iView = self.mainFrame.marketBrowser.itemView
+ if iView.active:
+ iView.update(iView.active)
+
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID))
event.Skip()
@@ -220,9 +221,6 @@ class PFGeneralPref(PreferenceView):
def onCBGaugeAnimation(self, event):
self.sFit.serviceFittingOptions["enableGaugeAnimation"] = self.cbGaugeAnimation.GetValue()
- def onCBExportCharges(self, event):
- self.sFit.serviceFittingOptions["exportCharges"] = self.cbExportCharges.GetValue()
-
def onCBOpenFitInNew(self, event):
self.sFit.serviceFittingOptions["openFitInNew"] = self.cbOpenFitInNew.GetValue()
diff --git a/gui/builtinShipBrowser/navigationPanel.py b/gui/builtinShipBrowser/navigationPanel.py
index 204802c45..e7f339260 100644
--- a/gui/builtinShipBrowser/navigationPanel.py
+++ b/gui/builtinShipBrowser/navigationPanel.py
@@ -11,6 +11,7 @@ import gui.utils.fonts as fonts
from .events import FitSelected, SearchSelected, ImportSelected, Stage1Selected, Stage2Selected, Stage3Selected
from gui.bitmap_loader import BitmapLoader
from service.fit import Fit
+from gui.utils.helpers_wxPython import HandleCtrlBackspace
pyfalog = Logger(__name__)
@@ -72,7 +73,7 @@ class NavigationPanel(SFItem.SFBrowserItem):
# self.BrowserSearchBox.Bind(wx.EVT_TEXT_ENTER, self.OnBrowserSearchBoxEnter)
# self.BrowserSearchBox.Bind(wx.EVT_KILL_FOCUS, self.OnBrowserSearchBoxLostFocus)
- self.BrowserSearchBox.Bind(wx.EVT_KEY_DOWN, self.OnBrowserSearchBoxEsc)
+ self.BrowserSearchBox.Bind(wx.EVT_KEY_DOWN, self.OnBrowserSearchBoxKeyPress)
self.BrowserSearchBox.Bind(wx.EVT_TEXT, self.OnScheduleSearch)
self.SetMinSize(size)
@@ -103,9 +104,11 @@ class NavigationPanel(SFItem.SFBrowserItem):
def OnBrowserSearchBoxLostFocus(self, event):
self.BrowserSearchBox.Show(False)
- def OnBrowserSearchBoxEsc(self, event):
+ def OnBrowserSearchBoxKeyPress(self, event):
if event.GetKeyCode() == wx.WXK_ESCAPE:
self.BrowserSearchBox.Show(False)
+ elif event.RawControlDown() and event.GetKeyCode() == wx.WXK_BACK:
+ HandleCtrlBackspace(self.BrowserSearchBox)
else:
event.Skip()
diff --git a/gui/builtinShipBrowser/pfListPane.py b/gui/builtinShipBrowser/pfListPane.py
index cbdebff05..60a303b3a 100644
--- a/gui/builtinShipBrowser/pfListPane.py
+++ b/gui/builtinShipBrowser/pfListPane.py
@@ -158,4 +158,5 @@ class PFListPane(wx.ScrolledWindow):
for widget in self._wList:
widget.Destroy()
+ self.Scroll(0, 0)
self._wList = []
diff --git a/gui/builtinViewColumns/misc.py b/gui/builtinViewColumns/misc.py
index 85a0d545b..e9bff3d56 100644
--- a/gui/builtinViewColumns/misc.py
+++ b/gui/builtinViewColumns/misc.py
@@ -504,7 +504,7 @@ class Miscellanea(ViewColumn):
text = "{0}s".format(cycleTime)
tooltip = "Spoolup time"
return text, tooltip
- elif itemGroup in ("Siege Module", "Cynosural Field"):
+ elif itemGroup in ("Siege Module", "Cynosural Field Generator"):
amt = stuff.getModifiedItemAttr("consumptionQuantity")
if amt:
typeID = stuff.getModifiedItemAttr("consumptionType")
diff --git a/gui/builtinViewColumns/price.py b/gui/builtinViewColumns/price.py
index a68614b83..56901c172 100644
--- a/gui/builtinViewColumns/price.py
+++ b/gui/builtinViewColumns/price.py
@@ -22,6 +22,7 @@ import wx
from eos.saveddata.cargo import Cargo
from eos.saveddata.drone import Drone
+from eos.saveddata.price import PriceStatus
from service.price import Price as ServicePrice
from gui.viewColumn import ViewColumn
from gui.bitmap_loader import BitmapLoader
@@ -45,13 +46,16 @@ class Price(ViewColumn):
if stuff.isEmpty:
return ""
- price = stuff.item.price
+ priceObj = stuff.item.price
- if not price or not price.isValid:
+ if not priceObj.isValid:
return False
# Fetch actual price as float to not modify its value on Price object
- price = price.price
+ price = priceObj.price
+
+ if price == 0:
+ return ""
if isinstance(stuff, Drone) or isinstance(stuff, Cargo):
price *= stuff.amount
@@ -63,10 +67,12 @@ class Price(ViewColumn):
def callback(item):
price = item[0]
- text = formatAmount(price.price, 3, 3, 9, currency=True) if price.price else ""
- if price.failed:
- text += " (!)"
- colItem.SetText(text)
+ textItems = []
+ if price.price:
+ textItems.append(formatAmount(price.price, 3, 3, 9, currency=True))
+ if price.status == PriceStatus.fail:
+ textItems.append("(!)")
+ colItem.SetText(" ".join(textItems))
display.SetItem(colItem)
diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py
index a0aea6043..372ea6a2e 100644
--- a/gui/builtinViews/fittingView.py
+++ b/gui/builtinViews/fittingView.py
@@ -40,6 +40,7 @@ from gui.contextMenu import ContextMenu
from gui.utils.staticHelpers import DragDropHelper
from service.fit import Fit
from service.market import Market
+from config import slotColourMap
pyfalog = Logger(__name__)
@@ -629,14 +630,8 @@ class FittingView(d.Display):
else:
event.Skip()
- slotColourMap = {1: wx.Colour(250, 235, 204), # yellow = low slots
- 2: wx.Colour(188, 215, 241), # blue = mid slots
- 3: wx.Colour(235, 204, 209), # red = high slots
- 4: '',
- 5: ''}
-
def slotColour(self, slot):
- return self.slotColourMap.get(slot) or self.GetBackgroundColour()
+ return slotColourMap.get(slot) or self.GetBackgroundColour()
def refresh(self, stuff):
"""
diff --git a/gui/characterEditor.py b/gui/characterEditor.py
index 6fe7c809a..2b4d59341 100644
--- a/gui/characterEditor.py
+++ b/gui/characterEditor.py
@@ -129,7 +129,12 @@ class CharacterEntityEditor(EntityEditor):
def DoRename(self, entity, name):
sChar = Character.getInstance()
- sChar.rename(entity, name)
+
+ if entity.alphaCloneID:
+ trimmed_name = re.sub('[ \(\u03B1\)]+$', '', name)
+ sChar.rename(entity, trimmed_name)
+ else:
+ sChar.rename(entity, name)
def DoCopy(self, entity, name):
sChar = Character.getInstance()
diff --git a/gui/contextMenu.py b/gui/contextMenu.py
index 6a97481a2..43d6ef58d 100644
--- a/gui/contextMenu.py
+++ b/gui/contextMenu.py
@@ -189,6 +189,7 @@ from gui.builtinContextMenus import ( # noqa: E402,F401
marketJump,
# droneSplit,
itemRemove,
+ fillWithModule,
droneRemoveStack,
ammoPattern,
project,
diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py
index 10eff2647..91725e048 100644
--- a/gui/copySelectDialog.py
+++ b/gui/copySelectDialog.py
@@ -18,9 +18,13 @@
# =============================================================================
+from collections import OrderedDict
+
# noinspection PyPackageRequirements
import wx
+
from service.port.eft import EFT_OPTIONS
+from service.port.multibuy import MULTIBUY_OPTIONS
from service.settings import SettingsProvider
@@ -37,39 +41,53 @@ class CopySelectDialog(wx.Dialog):
style=wx.DEFAULT_DIALOG_STYLE)
mainSizer = wx.BoxSizer(wx.VERTICAL)
- self.settings = SettingsProvider.getInstance().getSettings("pyfaExport", {"format": 0, "options": 0})
+ self.copyFormats = OrderedDict((
+ ("EFT", (CopySelectDialog.copyFormatEft, EFT_OPTIONS)),
+ ("MultiBuy", (CopySelectDialog.copyFormatMultiBuy, MULTIBUY_OPTIONS)),
+ ("ESI", (CopySelectDialog.copyFormatEsi, None)),
+ ("EFS", (CopySelectDialog.copyFormatEfs, None)),
+ # ("XML", (CopySelectDialog.copyFormatXml, None)),
+ # ("DNA", (CopySelectDialog.copyFormatDna, None)),
+ ))
- self.copyFormats = {
- "EFT": CopySelectDialog.copyFormatEft,
- "XML": CopySelectDialog.copyFormatXml,
- "DNA": CopySelectDialog.copyFormatDna,
- "ESI": CopySelectDialog.copyFormatEsi,
- "MultiBuy": CopySelectDialog.copyFormatMultiBuy,
- "EFS": CopySelectDialog.copyFormatEfs
- }
+ defaultFormatOptions = {}
+ for formatId, formatOptions in self.copyFormats.values():
+ if formatOptions is None:
+ continue
+ defaultFormatOptions[formatId] = {opt[0]: opt[3] for opt in formatOptions}
+
+ self.settings = SettingsProvider.getInstance().getSettings("pyfaExport", {"format": 0, "options": defaultFormatOptions})
+ # Options used to be stored as int (EFT export options only),
+ # overwrite them with new format when needed
+ if isinstance(self.settings["options"], int):
+ self.settings["options"] = defaultFormatOptions
self.options = {}
- for i, format in enumerate(self.copyFormats.keys()):
- if i == 0:
- rdo = wx.RadioButton(self, wx.ID_ANY, format, style=wx.RB_GROUP)
+ initialized = False
+ for formatName, formatData in self.copyFormats.items():
+ formatId, formatOptions = formatData
+ if not initialized:
+ rdo = wx.RadioButton(self, wx.ID_ANY, formatName, style=wx.RB_GROUP)
+ initialized = True
else:
- rdo = wx.RadioButton(self, wx.ID_ANY, format)
+ rdo = wx.RadioButton(self, wx.ID_ANY, formatName)
rdo.Bind(wx.EVT_RADIOBUTTON, self.Selected)
- if self.settings['format'] == self.copyFormats[format]:
+ if self.settings['format'] == formatId:
rdo.SetValue(True)
- self.copyFormat = self.copyFormats[format]
+ self.copyFormat = formatId
mainSizer.Add(rdo, 0, wx.EXPAND | wx.ALL, 5)
- if format == "EFT":
+ if formatOptions:
bsizer = wx.BoxSizer(wx.VERTICAL)
+ self.options[formatId] = {}
- for x, v in EFT_OPTIONS.items():
- ch = wx.CheckBox(self, -1, v['name'])
- self.options[x] = ch
- if self.settings['options'] & x:
- ch.SetValue(True)
- bsizer.Add(ch, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 3)
+ for optId, optName, optDesc, _ in formatOptions:
+ checkbox = wx.CheckBox(self, -1, optName)
+ self.options[formatId][optId] = checkbox
+ if self.settings['options'].get(formatId, {}).get(optId, defaultFormatOptions.get(formatId, {}).get(optId)):
+ checkbox.SetValue(True)
+ bsizer.Add(checkbox, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 3)
mainSizer.Add(bsizer, 1, wx.EXPAND | wx.LEFT, 20)
buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
@@ -83,21 +101,21 @@ class CopySelectDialog(wx.Dialog):
def Selected(self, event):
obj = event.GetEventObject()
- format = obj.GetLabel()
- self.copyFormat = self.copyFormats[format]
+ formatName = obj.GetLabel()
+ self.copyFormat = self.copyFormats[formatName][0]
self.toggleOptions()
self.Fit()
def toggleOptions(self):
- for ch in self.options.values():
- ch.Enable(self.GetSelected() == CopySelectDialog.copyFormatEft)
+ for formatId in self.options:
+ for checkbox in self.options[formatId].values():
+ checkbox.Enable(self.GetSelected() == formatId)
def GetSelected(self):
return self.copyFormat
def GetOptions(self):
- i = 0
- for x, v in self.options.items():
- if v.IsChecked():
- i = i ^ x
- return i
+ options = {}
+ for formatId in self.options:
+ options[formatId] = {optId: ch.IsChecked() for optId, ch in self.options[formatId].items()}
+ return options
diff --git a/gui/display.py b/gui/display.py
index 8ee42692e..8a4e36a0b 100644
--- a/gui/display.py
+++ b/gui/display.py
@@ -206,15 +206,18 @@ class Display(wx.ListCtrl):
colItem = self.GetItem(item, i)
oldText = colItem.GetText()
oldImageId = colItem.GetImage()
+ oldColour = colItem.GetBackgroundColour();
newText = col.getText(st)
if newText is False:
col.delayedText(st, self, colItem)
newText = "\u21bb"
+ newColour = self.columnBackground(colItem, st);
newImageId = col.getImageId(st)
colItem.SetText(newText)
colItem.SetImage(newImageId)
+ colItem.SetBackgroundColour(newColour)
mask = 0
@@ -228,6 +231,9 @@ class Display(wx.ListCtrl):
if mask:
colItem.SetMask(mask)
self.SetItem(colItem)
+ else:
+ if newColour != oldColour:
+ self.SetItem(colItem)
self.SetItemData(item, id_)
@@ -257,3 +263,6 @@ class Display(wx.ListCtrl):
def getColumn(self, point):
row, _, col = self.HitTestSubItem(point)
return col
+
+ def columnBackground(self, colItem, item):
+ return colItem.GetBackgroundColour()
diff --git a/gui/errorDialog.py b/gui/errorDialog.py
index d9112d4b8..240ab8da4 100644
--- a/gui/errorDialog.py
+++ b/gui/errorDialog.py
@@ -67,7 +67,7 @@ class ErrorFrame(wx.Frame):
from eos.config import gamedata_version, gamedata_date
time = datetime.datetime.fromtimestamp(int(gamedata_date)).strftime('%Y-%m-%d %H:%M:%S')
- version = "pyfa v" + config.getVersion() + '\nEVE Data Version: {} ({})\n\n'.format(gamedata_version, time) # gui.aboutData.versionString
+ version = "pyfa " + config.getVersion() + '\nEVE Data Version: {} ({})\n\n'.format(gamedata_version, time) # gui.aboutData.versionString
desc = "pyfa has experienced an unexpected issue. Below is a message that contains crucial\n" \
"information about how this was triggered. Please contact the developers with the\n" \
diff --git a/gui/fitCommands/__init__.py b/gui/fitCommands/__init__.py
index 2598f8bfb..793431ab7 100644
--- a/gui/fitCommands/__init__.py
+++ b/gui/fitCommands/__init__.py
@@ -2,6 +2,7 @@ from .guiToggleModuleState import GuiModuleStateChangeCommand
from .guiAddModule import GuiModuleAddCommand
from .guiRemoveModule import GuiModuleRemoveCommand
from .guiAddCharge import GuiModuleAddChargeCommand
+from .guiFillWithModule import GuiFillWithModuleCommand
from .guiSwapCloneModule import GuiModuleSwapOrCloneCommand
from .guiRemoveCargo import GuiRemoveCargoCommand
from .guiAddCargo import GuiAddCargoCommand
diff --git a/gui/fitCommands/guiFillWithModule.py b/gui/fitCommands/guiFillWithModule.py
new file mode 100644
index 000000000..0fa962bef
--- /dev/null
+++ b/gui/fitCommands/guiFillWithModule.py
@@ -0,0 +1,50 @@
+import wx
+import gui.mainFrame
+from gui import globalEvents as GE
+from .calc.fitAddModule import FitAddModuleCommand
+from service.fit import Fit
+from logbook import Logger
+pyfalog = Logger(__name__)
+
+
+class GuiFillWithModuleCommand(wx.Command):
+ def __init__(self, fitID, itemID, position=None):
+ """
+ Handles adding an item, usually a module, to the Fitting Window.
+
+ :param fitID: The fit ID that we are modifying
+ :param itemID: The item that is to be added to the Fitting View. If this turns out to be a charge, we attempt to
+ set the charge on the underlying module (requires position)
+ :param position: Optional. The position in fit.modules that we are attempting to set the item to
+ """
+ wx.Command.__init__(self, True, "Module Add: {}".format(itemID))
+ self.mainFrame = gui.mainFrame.MainFrame.getInstance()
+ self.sFit = Fit.getInstance()
+ self.fitID = fitID
+ self.itemID = itemID
+ self.internal_history = wx.CommandProcessor()
+ self.position = position
+ self.old_mod = None
+
+ def Do(self):
+ pyfalog.debug("{} Do()".format(self))
+ pyfalog.debug("Trying to append a module")
+ added_modules = 0
+ success = self.internal_history.Submit(FitAddModuleCommand(self.fitID, self.itemID))
+ while (success):
+ added_modules += 1
+ success = self.internal_history.Submit(FitAddModuleCommand(self.fitID, self.itemID))
+
+ if added_modules > 0:
+ self.sFit.recalc(self.fitID)
+ wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID, action="modadd", typeID=self.itemID))
+ return True
+ return False
+
+ def Undo(self):
+ pyfalog.debug("{} Undo()".format(self))
+ for _ in self.internal_history.Commands:
+ self.internal_history.Undo()
+ self.sFit.recalc(self.fitID)
+ wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID, action="moddel", typeID=self.itemID))
+ return True
diff --git a/gui/itemStats.py b/gui/itemStats.py
index 3768780e2..8ff858402 100644
--- a/gui/itemStats.py
+++ b/gui/itemStats.py
@@ -91,7 +91,7 @@ class ItemStatsDialog(wx.Dialog):
self.SetMinSize((300, 200))
if "wxGTK" in wx.PlatformInfo: # GTK has huge tab widgets, give it a bit more room
- self.SetSize((580, 500))
+ self.SetSize((640, 620))
else:
self.SetSize((550, 500))
# self.SetMaxSize((500, -1))
@@ -168,8 +168,9 @@ class ItemStatsContainer(wx.Panel):
self.mutator = ItemMutator(self.nbContainer, stuff, item)
self.nbContainer.AddPage(self.mutator, "Mutations")
- self.desc = ItemDescription(self.nbContainer, stuff, item)
- self.nbContainer.AddPage(self.desc, "Description")
+ if item.description:
+ self.desc = ItemDescription(self.nbContainer, stuff, item)
+ self.nbContainer.AddPage(self.desc, "Description")
self.params = ItemParams(self.nbContainer, stuff, item, context)
self.nbContainer.AddPage(self.params, "Attributes")
diff --git a/gui/mainFrame.py b/gui/mainFrame.py
index 5f3aa6441..f7039b73e 100644
--- a/gui/mainFrame.py
+++ b/gui/mainFrame.py
@@ -62,13 +62,11 @@ from gui.preferenceDialog import PreferenceDialog
from gui.resistsEditor import ResistsEditorDlg
from gui.setEditor import ImplantSetEditorDlg
from gui.shipBrowser import ShipBrowser
-from gui.ssoLogin import SsoLogin
from gui.statsPane import StatsPane
from gui.updateDialog import UpdateDialog
from gui.utils.clipboard import fromClipboard, toClipboard
from service.character import Character
-from service.esi import Esi, LoginMethod
-from service.esiAccess import SsoMode
+from service.esi import Esi
from service.fit import Fit
from service.port import EfsPort, IPortUser, Port
from service.settings import HTMLExportSettings, SettingsProvider
@@ -230,20 +228,11 @@ class MainFrame(wx.Frame):
self.sUpdate.CheckUpdate(self.ShowUpdateBox)
self.Bind(GE.EVT_SSO_LOGIN, self.onSSOLogin)
- self.Bind(GE.EVT_SSO_LOGGING_IN, self.ShowSsoLogin)
@property
def command(self) -> wx.CommandProcessor:
return Fit.getCommandProcessor(self.getActiveFit())
- def ShowSsoLogin(self, event):
- if getattr(event, "login_mode", LoginMethod.SERVER) == LoginMethod.MANUAL and getattr(event, "sso_mode", SsoMode.AUTO) == SsoMode.AUTO:
- dlg = SsoLogin(self)
- if dlg.ShowModal() == wx.ID_OK:
- sEsi = Esi.getInstance()
- # todo: verify that this is a correct SSO Info block
- sEsi.handleLogin({'SSOInfo': [dlg.ssoInfoCtrl.Value.strip()]})
-
def ShowUpdateBox(self, release, version):
dlg = UpdateDialog(self, release, version)
dlg.ShowModal()
@@ -703,10 +692,6 @@ class MainFrame(wx.Frame):
fit = db_getFit(self.getActiveFit())
toClipboard(Port.exportEft(fit, options))
- def clipboardEftImps(self, options):
- fit = db_getFit(self.getActiveFit())
- toClipboard(Port.exportEftImps(fit))
-
def clipboardDna(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(Port.exportDna(fit))
@@ -721,7 +706,7 @@ class MainFrame(wx.Frame):
def clipboardMultiBuy(self, options):
fit = db_getFit(self.getActiveFit())
- toClipboard(Port.exportMultiBuy(fit))
+ toClipboard(Port.exportMultiBuy(fit, options))
def clipboardEfs(self, options):
fit = db_getFit(self.getActiveFit())
@@ -744,22 +729,22 @@ class MainFrame(wx.Frame):
def exportToClipboard(self, event):
CopySelectDict = {CopySelectDialog.copyFormatEft: self.clipboardEft,
- # CopySelectDialog.copyFormatEftImps: self.clipboardEftImps,
CopySelectDialog.copyFormatXml: self.clipboardXml,
CopySelectDialog.copyFormatDna: self.clipboardDna,
CopySelectDialog.copyFormatEsi: self.clipboardEsi,
CopySelectDialog.copyFormatMultiBuy: self.clipboardMultiBuy,
CopySelectDialog.copyFormatEfs: self.clipboardEfs}
dlg = CopySelectDialog(self)
- dlg.ShowModal()
- selected = dlg.GetSelected()
- options = dlg.GetOptions()
+ btnPressed = dlg.ShowModal()
- settings = SettingsProvider.getInstance().getSettings("pyfaExport")
- settings["format"] = selected
- settings["options"] = options
+ if btnPressed == wx.ID_OK:
+ selected = dlg.GetSelected()
+ options = dlg.GetOptions()
- CopySelectDict[selected](options)
+ settings = SettingsProvider.getInstance().getSettings("pyfaExport")
+ settings["format"] = selected
+ settings["options"] = options
+ CopySelectDict[selected](options.get(selected))
try:
dlg.Destroy()
diff --git a/gui/ssoLogin.py b/gui/ssoLogin.py
index 4a8f1197b..d11d84a53 100644
--- a/gui/ssoLogin.py
+++ b/gui/ssoLogin.py
@@ -1,9 +1,14 @@
import wx
+import gui.mainFrame
+import webbrowser
+import gui.globalEvents as GE
class SsoLogin(wx.Dialog):
- def __init__(self, parent):
- wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="SSO Login", size=wx.Size(400, 240))
+ def __init__(self):
+ mainFrame = gui.mainFrame.MainFrame.getInstance()
+
+ wx.Dialog.__init__(self, mainFrame, id=wx.ID_ANY, title="SSO Login", size=wx.Size(400, 240))
bSizer1 = wx.BoxSizer(wx.VERTICAL)
@@ -24,3 +29,55 @@ class SsoLogin(wx.Dialog):
self.SetSizer(bSizer1)
self.Center()
+
+ mainFrame.Bind(GE.EVT_SSO_LOGIN, self.OnLogin)
+
+ from service.esi import Esi
+
+ self.sEsi = Esi.getInstance()
+ uri = self.sEsi.getLoginURI(None)
+ webbrowser.open(uri)
+
+ def OnLogin(self, event):
+ self.Close()
+ event.Skip()
+
+
+class SsoLoginServer(wx.Dialog):
+ def __init__(self, port):
+ mainFrame = gui.mainFrame.MainFrame.getInstance()
+ wx.Dialog.__init__(self, mainFrame, id=wx.ID_ANY, title="SSO Login", size=(-1, -1))
+
+ from service.esi import Esi
+
+ self.sEsi = Esi.getInstance()
+ serverAddr = self.sEsi.startServer(port)
+
+ uri = self.sEsi.getLoginURI(serverAddr)
+
+ bSizer1 = wx.BoxSizer(wx.VERTICAL)
+ mainFrame.Bind(GE.EVT_SSO_LOGIN, self.OnLogin)
+ self.Bind(wx.EVT_CLOSE, self.OnClose)
+
+ text = wx.StaticText(self, wx.ID_ANY, "Waiting for character login through EVE Single Sign-On.")
+ bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10)
+
+ bSizer3 = wx.BoxSizer(wx.VERTICAL)
+ 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)
+
+ def OnLogin(self, event):
+ self.Close()
+ event.Skip()
+
+ def OnClose(self, event):
+ self.sEsi.stopServer()
+ event.Skip()
diff --git a/gui/utils/helpers_wxPython.py b/gui/utils/helpers_wxPython.py
index e0f9d0ae0..26f98c35d 100644
--- a/gui/utils/helpers_wxPython.py
+++ b/gui/utils/helpers_wxPython.py
@@ -6,3 +6,23 @@ def YesNoDialog(question='Are you sure you want to do this?', caption='Yes or no
result = dlg.ShowModal() == wx.ID_YES
dlg.Destroy()
return result
+
+
+def HandleCtrlBackspace(textControl):
+ """
+ Handles the behavior of Windows ctrl+space
+ deletes everything from the cursor to the left,
+ up to the next whitespace.
+ """
+ curPos = textControl.GetInsertionPoint()
+ searchText = textControl.GetValue()
+ foundChar = False
+ for startIndex in range(curPos, -1, -1):
+ if startIndex - 1 < 0:
+ break
+ if searchText[startIndex - 1] != " ":
+ foundChar = True
+ elif foundChar:
+ break
+ textControl.Remove(startIndex, curPos)
+ textControl.SetInsertionPoint(startIndex)
\ No newline at end of file
diff --git a/pyfa.spec b/pyfa.spec
new file mode 100644
index 000000000..d3156bb80
--- /dev/null
+++ b/pyfa.spec
@@ -0,0 +1,121 @@
+# -*- mode: python -*-
+
+import os
+from itertools import chain
+import subprocess
+import requests.certs
+import platform
+
+os_name = platform.system()
+block_cipher = None
+
+added_files = [
+ ('imgs/gui/*.png', 'imgs/gui'),
+ ('imgs/gui/*.gif', 'imgs/gui'),
+ ('imgs/icons/*.png', 'imgs/icons'),
+ ('imgs/renders/*.png', 'imgs/renders'),
+ ('service/jargon/*.yaml', 'service/jargon'),
+ (requests.certs.where(), '.'), # is this needed anymore?
+ ('eve.db', '.'),
+ ('README.md', '.'),
+ ('LICENSE', '.'),
+ ('version.yml', '.'),
+]
+
+icon = None
+pathex = []
+upx = True
+debug = False
+
+if os_name == 'Windows':
+ added_files.extend([
+ ('dist_assets/win/pyfa.ico', '.'),
+ ('dist_assets/win/pyfa.exe.manifest', '.'),
+ ('dist_assets/win/Microsoft.VC90.CRT.manifest', '.')
+ ])
+
+ icon = 'dist_assets/win/pyfa.ico'
+
+ pathex.extend([
+ # Need this, see https://github.com/pyinstaller/pyinstaller/issues/1566
+ # To get this, download and install windows 10 SDK
+ # If not building on Windows 10, this might be optional
+ r'C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x86'
+ ])
+
+if os_name == 'Darwin':
+ added_files.extend([
+ ('dist_assets/win/pyfa.ico', '.'), # osx only
+ ])
+
+ icon = 'dist_assets/mac/pyfa.icns'
+
+import_these = [
+ 'numpy.core._dtype_ctypes' # https://github.com/pyinstaller/pyinstaller/issues/3982
+]
+
+# Walk directories that do dynamic importing
+paths = ('eos/effects', 'eos/db/migrations', 'service/conversions')
+for root, folders, files in chain.from_iterable(os.walk(path) for path in paths):
+ for file_ in files:
+ if file_.endswith(".py") and not file_.startswith("_"):
+ mod_name = "{}.{}".format(
+ root.replace("/", "."),
+ file_.split(".py")[0],
+ )
+ import_these.append(mod_name)
+
+a = Analysis(['pyfa.py'],
+ pathex= pathex,
+ binaries=[],
+ datas=added_files,
+ hiddenimports=import_these,
+ hookspath=['dist_assets/pyinstaller_hooks'],
+ runtime_hooks=[],
+ excludes=['Tkinter'],
+ win_no_prefer_redirects=False,
+ win_private_assemblies=False,
+ cipher=block_cipher)
+
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ exclude_binaries=True,
+ name='pyfa',
+ debug=debug,
+ strip=False,
+ upx=upx,
+ icon= icon,
+ # version='win-version-info.txt',
+ console=False
+)
+
+coll = COLLECT(
+ exe,
+ a.binaries,
+ a.zipfiles,
+ a.datas,
+ strip=False,
+ upx=upx,
+ name='pyfa',
+)
+
+if platform.system() == 'Darwin':
+ info_plist = {
+ 'NSHighResolutionCapable': 'True',
+ 'NSPrincipalClass': 'NSApplication',
+ 'CFBundleName': 'pyfa',
+ 'CFBundleDisplayName': 'pyfa',
+ 'CFBundleIdentifier': 'org.pyfaorg.pyfa',
+ 'CFBundleVersion': '1.2.3',
+ 'CFBundleShortVersionString': '1.2.3',
+ }
+ app = BUNDLE(exe,
+ name='pyfa.app',
+ icon=icon,
+ bundle_identifier=None,
+ info_plist=info_plist
+ )
diff --git a/requirements.txt b/requirements.txt
index dc9c60768..0c7d59c65 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,14 +1,13 @@
-wxPython == 4.0.0b2
+wxPython == 4.0.4
logbook >= 1.0.0
matplotlib >= 2.0.0
python-dateutil
requests >= 2.0.0
sqlalchemy == 1.0.5
-cryptography
-diskcache
-markdown2
-packaging
-roman
-beautifulsoup4
-PyYAML
+cryptography>=2.3
+markdown2==2.3.5
+packaging==16.8
+roman==2.0.0
+beautifulsoup4==4.6.0
+pyyaml>=5.1b1
PyInstaller == 3.3
\ No newline at end of file
diff --git a/scripts/dump_version.py b/scripts/dump_version.py
new file mode 100644
index 000000000..c506bfdb9
--- /dev/null
+++ b/scripts/dump_version.py
@@ -0,0 +1,23 @@
+"""
+This script is solely used when generating builds. It generates a version number automatically using
+git tags as it's basis. Whenever a build is created, run this file beforehand and it should replace
+the old version number with the new one in VERSION.YML
+"""
+
+import yaml
+import subprocess
+import os
+
+
+with open("version.yml", 'r+') as file:
+ data = yaml.load(file, Loader=yaml.FullLoader)
+ file.seek(0)
+ file.truncate()
+ # todo: run Version() on the tag to ensure that it's of proper formatting - fail a test if not and prevent building
+ # python's versioning spec doesn't handle the same format git describe outputs, so convert it.
+ label = os.environ["PYFA_VERSION"].split('-') if "PYFA_VERSION" in os.environ else subprocess.check_output(["git", "describe", "--tags"]).strip().decode().split('-')
+ label = '-'.join(label[:-2])+'+'+'-'.join(label[-2:]) if len(label) > 1 else label[0]
+ print(label)
+ data['version'] = label
+ yaml.dump(data, file, default_flow_style=False)
+
diff --git a/scripts/iconIDs.yaml b/scripts/iconIDs.yaml
index 1fec32850..a2bdfc349 100644
--- a/scripts/iconIDs.yaml
+++ b/scripts/iconIDs.yaml
@@ -706,7 +706,7 @@
- tech2
iconFile: res:/ui/texture/icons/12_64_13.png
398:
- description: Corpse floating in space (male?). - Corpse
+ description: chemical item
iconFile: res:/ui/texture/icons/11_64_5.png
400:
description: Mineral - Pyerite
@@ -10432,6 +10432,66 @@
22076:
description: Gift Box with Ribbon
iconFile: res:/ui/texture/icons/76_64_3.png
+22077:
+ description: 49978_Male_outer_ExplorationSuit_M01_Types_ExplorationSuit_M01_W.png
+ iconFile: res:/UI/Asset/mannequin/outer/49978_Male_outer_ExplorationSuit_M01_Types_ExplorationSuit_M01_W.png
+22078:
+ description: 49980_Female_Outer_ExplorationSuit_F01_Types_ExplorationSuit_F01_W.png
+ iconFile: res:/UI/Asset/mannequin/outer/49980_Female_Outer_ExplorationSuit_F01_Types_ExplorationSuit_F01_W.png
+22079:
+ description: Preview for reward track augmentations
+ iconFile: res:/UI/Texture/Icons/RewardTrack/reward_Holiday2018_Augmentation1.png
+22080:
+ description: Preview for reward track augmentations
+ iconFile: res:/UI/Texture/Icons/RewardTrack/reward_Holiday2018_Augmentation2.png
+22081:
+ description: Holiday'18 Crate - Drake/Rupture Splash
+ iconFile: res:/UI/Texture/classes/ItemPacks/SplashImages/Holiday2018/splash_Holiday2018_DrakeRupture.png
+22082:
+ description: Holiday'18 Crate - ExpSuits
+ iconFile: res:/UI/Texture/classes/ItemPacks/SplashImages/Holiday2018/splash_Holiday2018_ExplorationSuits.png
+22084:
+ description: Holiday'18 Crate - Augmentation Set 1
+ iconFile: res:/UI/Texture/classes/ItemPacks/SplashImages/Holiday2018/splash_Holiday2018_FaceAugmentation1.png
+22085:
+ description: Holiday'18 Crate - Augmentation Set 2
+ iconFile: res:/UI/Texture/classes/ItemPacks/SplashImages/Holiday2018/splash_Holiday2018_FaceAugmentation2.png
+22086:
+ description: Holiday'18 Crate - Gnosis/Punisher Splash
+ iconFile: res:/UI/Texture/classes/ItemPacks/SplashImages/Holiday2018/splash_Holiday2018_GnosisPunisher.png
+22087:
+ description: 49978_Male_outer_ExplorationSuit_M01_Types_ExplorationSuit_M01_W.png
+ iconFile: res:/UI/Asset/mannequin/outer/49978_Male_outer_ExplorationSuit_M01_Types_ExplorationSuit_M01_W.png
+22088:
+ description: 49980_Female_Outer_ExplorationSuit_F01_Types_ExplorationSuit_F01_W.png
+ iconFile: res:/UI/Asset/mannequin/outer/49980_Female_Outer_ExplorationSuit_F01_Types_ExplorationSuit_F01_W.png
+22089:
+ description: 49984_Female_Makeup_Augmentations_Face_Paint_F01_Types_Face_Paint_F01_V0_Blue.png
+ iconFile: res:/UI/Asset/mannequin/makeup_augmentations/49984_Female_Makeup_Augmentations_Face_Paint_F01_Types_Face_Paint_F01_V0_Blue.png
+22090:
+ description: 49985_Female_Makeup_Augmentations_Face_Paint_F01_Types_Face_Paint_F01_V10_W.png
+ iconFile: res:/UI/Asset/mannequin/makeup_augmentations/49985_Female_Makeup_Augmentations_Face_Paint_F01_Types_Face_Paint_F01_V10_W.png
22091:
- description: Manually added mutadaptive RR icon path
- iconFile: res:/ui/texture/icons/modules/abyssalremoterepairer.png
+ backgrounds:
+ - blueprint
+ - blueprintCopy
+ description: Mutadaptive Remote Repairer
+ foregrounds:
+ - faction
+ - tech2
+ iconFile: res:/ui/texture/icons/modules/abyssalRemoteRepairer.png
+22092:
+ description: 49987_Male_Makeup_Augmentations_Face_Paint_M01_Types_Face_Paint_M01_V6_Blue.png
+ iconFile: res:/UI/Asset/mannequin/makeup_augmentations/49987_Male_Makeup_Augmentations_Face_Paint_M01_Types_Face_Paint_M01_V6_Blue.png
+22093:
+ description: Generic SKIN icon for crates containing multiple SKINs
+ iconFile: res:/UI/Texture/Icons/RewardTrack/crateSkinContainer.png
+22094:
+ description: Preview icon for exploration suit reward track winter'18
+ iconFile: res:/UI/Texture/Icons/RewardTrack/crateWinterExplorationSuit.png
+22095:
+ description: 49986_male_Makeup_Augmentations_Face_Paint_M01_Types_Face_Paint_M01_V10_W.png
+ iconFile: res:/UI/Asset/mannequin/makeup_augmentations/49986_male_Makeup_Augmentations_Face_Paint_M01_Types_Face_Paint_M01_V10_W.png
+22096:
+ description: Winter Login Campaign Banner
+ iconFile: res:/UI/Texture/LoginCampaigns/Winter_2018.png
diff --git a/scripts/jsonToSql.py b/scripts/jsonToSql.py
index 39e51d198..913b74b1c 100755
--- a/scripts/jsonToSql.py
+++ b/scripts/jsonToSql.py
@@ -28,6 +28,8 @@ sys.path.insert(0, os.path.realpath(os.path.join(path, '..')))
import json
import argparse
+import itertools
+
CATEGORIES_TO_REMOVE = [
30 # Apparel
@@ -174,6 +176,119 @@ def main(db, json_path):
newData.append(newRow)
return newData
+ def fillReplacements(tables):
+
+ def compareAttrs(attrs1, attrs2, attrHig):
+ """
+ Compares received attribute sets. Returns:
+ - 0 if sets are different
+ - 1 if sets are exactly the same
+ - 2 if first set is strictly better
+ - 3 if second set is strictly better
+ """
+ if set(attrs1) != set(attrs2):
+ return 0
+ if all(attrs1[aid] == attrs2[aid] for aid in attrs1):
+ return 1
+ if all(
+ (attrs1[aid] >= attrs2[aid] and attrHig[aid]) or
+ (attrs1[aid] <= attrs2[aid] and not attrHig[aid])
+ for aid in attrs1
+ ):
+ return 2
+ if all(
+ (attrs2[aid] >= attrs1[aid] and attrHig[aid]) or
+ (attrs2[aid] <= attrs1[aid] and not attrHig[aid])
+ for aid in attrs1
+ ):
+ return 3
+ return 0
+
+ skillReqAttribs = {
+ 182: 277,
+ 183: 278,
+ 184: 279,
+ 1285: 1286,
+ 1289: 1287,
+ 1290: 1288}
+ skillReqAttribsFlat = set(skillReqAttribs.keys()).union(skillReqAttribs.values())
+ # Get data on type groups
+ typesGroups = {}
+ for row in tables['evetypes']:
+ typesGroups[row['typeID']] = row['groupID']
+ # Get data on type attributes
+ typesNormalAttribs = {}
+ typesSkillAttribs = {}
+ for row in tables['dgmtypeattribs']:
+ attributeID = row['attributeID']
+ if attributeID in skillReqAttribsFlat:
+ typeSkillAttribs = typesSkillAttribs.setdefault(row['typeID'], {})
+ typeSkillAttribs[row['attributeID']] = row['value']
+ # Ignore these attributes for comparison purposes
+ elif attributeID in (
+ 422, # techLevel
+ 633, # metaLevel
+ 1692 # metaGroupID
+ ):
+ continue
+ else:
+ typeNormalAttribs = typesNormalAttribs.setdefault(row['typeID'], {})
+ typeNormalAttribs[row['attributeID']] = row['value']
+ # Get data on skill requirements
+ typesSkillReqs = {}
+ for typeID, typeAttribs in typesSkillAttribs.items():
+ typeSkillAttribs = typesSkillAttribs.get(typeID, {})
+ if not typeSkillAttribs:
+ continue
+ typeSkillReqs = typesSkillReqs.setdefault(typeID, {})
+ for skillreqTypeAttr, skillreqLevelAttr in skillReqAttribs.items():
+ try:
+ skillType = int(typeSkillAttribs[skillreqTypeAttr])
+ skillLevel = int(typeSkillAttribs[skillreqLevelAttr])
+ except (KeyError, ValueError):
+ continue
+ typeSkillReqs[skillType] = skillLevel
+ # Get data on attribute highIsGood flag
+ attrHig = {}
+ for row in tables['dgmattribs']:
+ attrHig[row['attributeID']] = bool(row['highIsGood'])
+ # As EVE affects various types mostly depending on their group or skill requirements,
+ # we're going to group various types up this way
+ groupedData = {}
+ for row in tables['evetypes']:
+ typeID = row['typeID']
+ typeAttribs = typesNormalAttribs.get(typeID, {})
+ # Ignore stuff w/o attributes
+ if not typeAttribs:
+ continue
+ # We need only skill types, not levels for keys
+ typeSkillreqs = frozenset(typesSkillReqs.get(typeID, {}))
+ typeGroup = typesGroups[typeID]
+ groupData = groupedData.setdefault((typeGroup, typeSkillreqs), [])
+ groupData.append((typeID, typeAttribs))
+ same = {}
+ better = {}
+ # Now, go through composed groups and for every item within it find items which are
+ # the same and which are better
+ for groupData in groupedData.values():
+ for type1, type2 in itertools.combinations(groupData, 2):
+ comparisonResult = compareAttrs(type1[1], type2[1], attrHig)
+ # Equal
+ if comparisonResult == 1:
+ same.setdefault(type1[0], set()).add(type2[0])
+ same.setdefault(type2[0], set()).add(type1[0])
+ # First is better
+ elif comparisonResult == 2:
+ better.setdefault(type2[0], set()).add(type1[0])
+ # Second is better
+ elif comparisonResult == 3:
+ better.setdefault(type1[0], set()).add(type2[0])
+ # Put this data into types table so that normal process hooks it up
+ for row in tables['evetypes']:
+ typeID = row['typeID']
+ row['replaceSame'] = ','.join('{}'.format(tid) for tid in sorted(same.get(typeID, ())))
+ row['replaceBetter'] = ','.join('{}'.format(tid) for tid in sorted(better.get(typeID, ())))
+
data = {}
# Dump all data to memory so we can easely cross check ignored rows
@@ -190,6 +305,8 @@ def main(db, json_path):
tableData = convertClones(tableData)
data[jsonName] = tableData
+ fillReplacements(data)
+
# Set with typeIDs which we will have in our database
# Sometimes CCP unpublishes some items we want to have published, we
# can do it here - just add them to initial set
diff --git a/scripts/package-osx.sh b/scripts/package-osx.sh
new file mode 100644
index 000000000..35a87a007
--- /dev/null
+++ b/scripts/package-osx.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+echo "${PYFA_VERSION}"
+
+cat version.yml
+python3 -m PyInstaller -y --clean --windowed dist_assets/mac/pyfa.spec
+cd dist
+zip -r "pyfa-$PYFA_VERSION-mac.zip" pyfa.app
+curl --upload-file "pyfa-$PYFA_VERSION-mac.zip" https://transfer.sh/
+echo -e "\n"
+md5 -r "pyfa-$PYFA_VERSION-mac.zip"
\ No newline at end of file
diff --git a/scripts/sdeReadIcons.py b/scripts/sdeReadIcons.py
index d099f6bd5..aba3866fe 100644
--- a/scripts/sdeReadIcons.py
+++ b/scripts/sdeReadIcons.py
@@ -10,7 +10,7 @@ import json
iconDict = {}
stream = open('iconIDs.yaml', 'r')
-docs = yaml.load_all(stream)
+docs = yaml.load_all(stream, Loader=yaml.FullLoader)
for doc in docs:
for k,v in list(doc.items()):
diff --git a/scripts/setup-osx.sh b/scripts/setup-osx.sh
new file mode 100644
index 000000000..97d477b50
--- /dev/null
+++ b/scripts/setup-osx.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+wget "https://www.python.org/ftp/python/${PYTHON}/python-${PYTHON}-macosx10.6.pkg"
+sudo installer -pkg python-${PYTHON}-macosx10.6.pkg -target /
+sudo python3 -m ensurepip
+# A manual check that the correct version of Python is running.
+python3 --version
+pip3 install -r requirements.txt
diff --git a/service/esi.py b/service/esi.py
index 7b5247bec..8162c6844 100644
--- a/service/esi.py
+++ b/service/esi.py
@@ -13,9 +13,11 @@ from eos.enum import Enum
from eos.saveddata.ssocharacter import SsoCharacter
from service.esiAccess import APIException, SsoMode
import gui.globalEvents as GE
+from gui.ssoLogin import SsoLogin, SsoLoginServer
from service.server import StoppableHTTPServer, AuthHandler
from service.settings import EsiSettings
from service.esiAccess import EsiAccess
+import gui.mainFrame
from requests import Session
@@ -104,19 +106,21 @@ class Esi(EsiAccess):
self.fittings_deleted.add(fittingID)
def login(self):
- serverAddr = None
# always start the local server if user is using client details. Otherwise, start only if they choose to do so.
if self.settings.get('ssoMode') == SsoMode.CUSTOM or self.settings.get('loginMode') == LoginMethod.SERVER:
- # random port, or if it's custom application, use a defined port
- serverAddr = self.startServer(6461 if self.settings.get('ssoMode') == SsoMode.CUSTOM else 0)
- uri = self.getLoginURI(serverAddr)
- webbrowser.open(uri)
- wx.PostEvent(self.mainFrame, GE.SsoLoggingIn(sso_mode=self.settings.get('ssoMode'), login_mode=self.settings.get('loginMode')))
+ dlg = gui.ssoLogin.SsoLoginServer(6461 if self.settings.get('ssoMode') == SsoMode.CUSTOM else 0)
+ dlg.ShowModal()
+ else:
+ dlg = gui.ssoLogin.SsoLogin()
+
+ if dlg.ShowModal() == wx.ID_OK:
+ self.handleLogin({'SSOInfo': [dlg.ssoInfoCtrl.Value.strip()]})
def stopServer(self):
pyfalog.debug("Stopping Server")
- self.httpd.stop()
- self.httpd = None
+ if self.httpd:
+ self.httpd.stop()
+ self.httpd = None
def startServer(self, port): # todo: break this out into two functions: starting the server, and getting the URI
pyfalog.debug("Starting server")
diff --git a/service/fit.py b/service/fit.py
index ad1e5c4dd..035bb50ee 100644
--- a/service/fit.py
+++ b/service/fit.py
@@ -89,7 +89,6 @@ class Fit(FitDeprecated):
"showTooltip": True,
"showMarketShortcuts": False,
"enableGaugeAnimation": True,
- "exportCharges": True,
"openFitInNew": False,
"priceSystem": "Jita",
"priceSource": "eve-marketdata.com",
diff --git a/service/jargon/loader.py b/service/jargon/loader.py
index be34a298e..541bd0a4b 100644
--- a/service/jargon/loader.py
+++ b/service/jargon/loader.py
@@ -43,9 +43,9 @@ class JargonLoader(object):
self.jargon_mtime != self._get_jargon_file_mtime())
def _load_jargon(self):
- jargondata = yaml.load(DEFAULT_DATA)
+ jargondata = yaml.load(DEFAULT_DATA, Loader=yaml.FullLoader)
with open(JARGON_PATH) as f:
- userdata = yaml.load(f)
+ userdata = yaml.load(f, Loader=yaml.FullLoader)
jargondata.update(userdata)
self.jargon_mtime = self._get_jargon_file_mtime()
self._jargon = Jargon(jargondata)
@@ -57,7 +57,7 @@ class JargonLoader(object):
@staticmethod
def init_user_jargon(jargon_path):
- values = yaml.load(DEFAULT_DATA)
+ values = yaml.load(DEFAULT_DATA, Loader=yaml.FullLoader)
# Disabled for issue/1533; do not overwrite existing user config
# if os.path.exists(jargon_path):
diff --git a/service/marketSources/evemarketdata.py b/service/marketSources/evemarketdata.py
index bd1f8f3af..15edc92ef 100644
--- a/service/marketSources/evemarketdata.py
+++ b/service/marketSources/evemarketdata.py
@@ -22,6 +22,7 @@ from xml.dom import minidom
from logbook import Logger
+from eos.saveddata.price import PriceStatus
from service.network import Network
from service.price import Price, TIMEOUT, VALIDITY
@@ -52,6 +53,7 @@ class EveMarketData(object):
price = float(type_.firstChild.data)
except (TypeError, ValueError):
pyfalog.warning("Failed to get price for: {0}", type_)
+ continue
# Fill price data
priceobj = priceMap[typeID]
@@ -61,11 +63,10 @@ class EveMarketData(object):
if price != 0:
priceobj.price = price
priceobj.time = time.time() + VALIDITY
+ priceobj.status = PriceStatus.success
else:
priceobj.time = time.time() + TIMEOUT
- priceobj.failed = None
-
# delete price from working dict
del priceMap[typeID]
diff --git a/service/marketSources/evemarketer.py b/service/marketSources/evemarketer.py
index a2728ddfb..478de4371 100644
--- a/service/marketSources/evemarketer.py
+++ b/service/marketSources/evemarketer.py
@@ -22,13 +22,14 @@ from xml.dom import minidom
from logbook import Logger
+from eos.saveddata.price import PriceStatus
from service.network import Network
from service.price import Price, VALIDITY
pyfalog = Logger(__name__)
-class EveCentral(object):
+class EveMarketer(object):
name = "evemarketer"
@@ -61,10 +62,10 @@ class EveCentral(object):
priceobj = priceMap[typeID]
priceobj.price = percprice
priceobj.time = time.time() + VALIDITY
- priceobj.failed = None
+ priceobj.status = PriceStatus.success
# delete price from working dict
del priceMap[typeID]
-Price.register(EveCentral)
+Price.register(EveMarketer)
diff --git a/service/network.py b/service/network.py
index 0c3bfb2b9..5554eadec 100644
--- a/service/network.py
+++ b/service/network.py
@@ -83,8 +83,7 @@ class Network(object):
raise Error("Access not enabled - please enable in Preferences > Network")
# Set up some things for the request
- versionString = "{0} {1} - {2} {3}".format(config.version, config.tag, config.expansionName,
- config.expansionVersion)
+ versionString = "{0}".format(config.version)
headers = {"User-Agent": "pyfa {0} (python-requests {1})".format(versionString, requests.__version__)}
# user-agent: pyfa 2.0.0b4 git -YC120.2 1.2 (python-requests 2.18.4)
diff --git a/service/port/dna.py b/service/port/dna.py
index bd2645ef8..bc64e9e99 100644
--- a/service/port/dna.py
+++ b/service/port/dna.py
@@ -138,7 +138,7 @@ def exportDna(fit):
mods[mod.itemID] = 0
mods[mod.itemID] += 1
- if mod.charge and sFit.serviceFittingOptions["exportCharges"]:
+ if mod.charge:
if mod.chargeID not in charges:
charges[mod.chargeID] = 0
# `or 1` because some charges (ie scripts) are without qty
diff --git a/service/port/efs.py b/service/port/efs.py
index 0e021902c..c93c7d87d 100755
--- a/service/port/efs.py
+++ b/service/port/efs.py
@@ -1,13 +1,8 @@
-import inspect
-import os
-import platform
-import re
-import sys
-import traceback
import json
import eos.db
from math import log
+from numbers import Number
from config import version as pyfaVersion
from service.fit import Fit
from service.market import Market
@@ -15,9 +10,11 @@ from eos.enum import Enum
from eos.saveddata.module import Hardpoint, Slot, Module, State
from eos.saveddata.drone import Drone
from eos.effectHandlerHelpers import HandledList
-from eos.db import gamedata_session, getItemsByCategory, getCategory, getAttributeInfo, getGroup
-from eos.gamedata import Category, Group, Item, Traits, Attribute, Effect, ItemEffect
+from eos.db import gamedata_session, getCategory, getAttributeInfo, getGroup
+from eos.gamedata import Attribute, Effect, Group, Item, ItemEffect
from eos.utils.spoolSupport import SpoolType, SpoolOptions
+from gui.fitCommands.calc.fitAddModule import FitAddModuleCommand
+from gui.fitCommands.calc.fitRemoveModule import FitRemoveModuleCommand
from logbook import Logger
pyfalog = Logger(__name__)
@@ -30,9 +27,9 @@ class RigSize(Enum):
CAPITAL = 4
-class EfsPort():
+class EfsPort:
wepTestSet = {}
- version = 0.02
+ version = 0.03
@staticmethod
def attrDirectMap(values, target, source):
@@ -72,12 +69,12 @@ class EfsPort():
if propID is None:
return None
- sFit.appendModule(fitID, propID)
+ FitAddModuleCommand(fitID, propID).Do()
sFit.recalc(fit)
fit = eos.db.getFit(fitID)
mwdPropSpeed = fit.maxSpeed
mwdPosition = list(filter(lambda mod: mod.item and mod.item.ID == propID, fit.modules))[0].position
- sFit.removeModule(fitID, mwdPosition)
+ FitRemoveModuleCommand(fitID, [mwdPosition]).Do()
sFit.recalc(fit)
fit = eos.db.getFit(fitID)
return mwdPropSpeed
@@ -112,9 +109,12 @@ class EfsPort():
"Burst Projectors", "Warp Disrupt Field Generator", "Armor Resistance Shift Hardener",
"Target Breaker", "Micro Jump Drive", "Ship Modifiers", "Stasis Grappler",
"Ancillary Remote Shield Booster", "Ancillary Remote Armor Repairer",
- "Titan Phenomena Generator", "Non-Repeating Hardeners"
+ "Titan Phenomena Generator", "Non-Repeating Hardeners", "Mutadaptive Remote Armor Repairer"
]
projectedMods = list(filter(lambda mod: mod.item and mod.item.group.name in modGroupNames, fit.modules))
+ # Sort projections to prevent the order needlessly changing as pyfa updates.
+ projectedMods.sort(key=lambda mod: mod.item.ID)
+ projectedMods.sort(key=lambda mod: mod.item.group.ID)
projections = []
for mod in projectedMods:
maxRangeDefault = 0
@@ -145,7 +145,9 @@ class EfsPort():
elif mod.item.group.name in ["Remote Shield Booster", "Ancillary Remote Shield Booster"]:
stats["type"] = "Remote Shield Booster"
EfsPort.attrDirectMap(["shieldBonus"], stats, mod)
- elif mod.item.group.name in ["Remote Armor Repairer", "Ancillary Remote Armor Repairer"]:
+ elif mod.item.group.name in [
+ "Remote Armor Repairer", "Ancillary Remote Armor Repairer", "Mutadaptive Remote Armor Repairer"
+ ]:
stats["type"] = "Remote Armor Repairer"
EfsPort.attrDirectMap(["armorDamageAmount"], stats, mod)
elif mod.item.group.name == "Warp Scrambler":
@@ -191,7 +193,7 @@ class EfsPort():
return projections
# Note that unless padTypeIDs is True all 0s will be removed from modTypeIDs in the return.
- # They always are added initally for the sake of brevity, as this option may not be retained long term.
+ # They always are added initially for the sake of brevity, as this option may not be retained long term.
@staticmethod
def getModuleInfo(fit, padTypeIDs=False):
moduleNames = []
@@ -303,9 +305,9 @@ class EfsPort():
def getWeaponSystemData(fit):
weaponSystems = []
groups = {}
- # TODO: fetch spoolup option
+ # Export at maximum spool for consistency, spoolup data is exported anyway.
defaultSpoolValue = 1
- spoolOptions = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)
+ spoolOptions = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, True)
for mod in fit.modules:
if mod.getDps(spoolOptions=spoolOptions).total > 0:
# Group weapon + ammo combinations that occur more than once
@@ -344,7 +346,7 @@ class EfsPort():
aoeFieldRange = stats.getModifiedItemAttr("empFieldRange")
# This also covers non-bomb weapons with dps values and no hardpoints, most notably targeted doomsdays.
typeing = "SmartBomb"
- # Targeted DDs are the only non drone/fighter weapon without an explict max range
+ # Targeted DDs are the only non drone/fighter weapon without an explicit max range
if stats.item.group.name == 'Super Weapon' and stats.maxRange is None:
maxRange = 300000
else:
@@ -515,7 +517,7 @@ class EfsPort():
# Since the effect modules are fairly opaque a mock test fit is used to test the impact of traits.
# standin class used to prevent . notation causing issues when used as an arg
- class standin():
+ class standin:
pass
tf = standin()
tf.modules = HandledList(turrets + launchers)
@@ -551,7 +553,7 @@ class EfsPort():
@staticmethod
def getShipSize(groupID):
- # Size groupings are somewhat arbitrary but allow for a more managable number of top level groupings in a tree structure.
+ # Size groupings are somewhat arbitrary but allow for a more manageable number of top level groupings in a tree structure.
frigateGroupNames = ["Frigate", "Shuttle", "Corvette", "Assault Frigate", "Covert Ops", "Interceptor",
"Stealth Bomber", "Electronic Attack Ship", "Expedition Frigate", "Logistics Frigate"]
destroyerGroupNames = ["Destroyer", "Interdictor", "Tactical Destroyer", "Command Destroyer"]
@@ -625,9 +627,29 @@ class EfsPort():
}
resonance = {"hull": hullResonance, "armor": armorResonance, "shield": shieldResonance}
shipSize = EfsPort.getShipSize(fit.ship.item.groupID)
- # TODO: fetch spoolup option
+ # Export at maximum spool for consistency, spoolup data is exported anyway.
defaultSpoolValue = 1
- spoolOptions = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)
+ spoolOptions = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, True)
+
+ def roundNumbers(data, digits):
+ if isinstance(data, str):
+ return
+ if isinstance(data, dict):
+ for key in data:
+ if isinstance(data[key], Number):
+ data[key] = round(data[key], digits)
+ else:
+ roundNumbers(data[key], digits)
+ if isinstance(data, list) or isinstance(data, tuple):
+ for val in data:
+ roundNumbers(val, digits)
+ if isinstance(data, Number):
+ rounded = round(data, digits)
+ if data != rounded:
+ pyfalog.error("Error rounding numbers for EFS export, export may be inconsistent."
+ "This suggests the format has been broken somewhere.")
+ return
+
try:
dataDict = {
"name": fitName, "ehp": fit.ehp, "droneDPS": fit.getDroneDps().total,
@@ -650,9 +672,12 @@ class EfsPort():
"modTypeIDs": modTypeIDs, "moduleNames": moduleNames,
"pyfaVersion": pyfaVersion, "efsExportVersion": EfsPort.version
}
- except TypeError:
+ # Recursively round any numbers in dicts to 6 decimal places.
+ # This prevents meaningless rounding errors from changing the output whenever pyfa changes.
+ roundNumbers(dataDict, 6)
+ except TypeError as e:
pyfalog.error("Error parsing fit:" + str(fit))
- pyfalog.error(TypeError)
+ pyfalog.error(e)
dataDict = {"name": fitName + "Fit could not be correctly parsed"}
export = json.dumps(dataDict, skipkeys=True)
return export
diff --git a/service/port/eft.py b/service/port/eft.py
index 1cc262e78..0d8a0a706 100644
--- a/service/port/eft.py
+++ b/service/port/eft.py
@@ -19,6 +19,7 @@
import re
+from enum import Enum
from logbook import Logger
@@ -36,7 +37,6 @@ from service.fit import Fit as svcFit
from service.market import Market
from service.port.muta import parseMutant, renderMutant
from service.port.shared import IPortUser, fetchItem, processing_notify
-from enum import Enum
pyfalog = Logger(__name__)
@@ -45,23 +45,20 @@ pyfalog = Logger(__name__)
class Options(Enum):
IMPLANTS = 1
MUTATIONS = 2
+ LOADED_CHARGES = 3
+
+
+EFT_OPTIONS = (
+ (Options.LOADED_CHARGES.value, 'Loaded Charges', 'Export charges loaded into modules', True),
+ (Options.MUTATIONS.value, 'Mutated Attributes', 'Export mutated modules\' stats', True),
+ (Options.IMPLANTS.value, 'Implants && Boosters', 'Export implants and boosters', True),
+)
MODULE_CATS = ('Module', 'Subsystem', 'Structure Module')
SLOT_ORDER = (Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE)
OFFLINE_SUFFIX = '/OFFLINE'
-EFT_OPTIONS = {
- Options.IMPLANTS.value: {
- "name": "Implants",
- "description": "Exports implants"
- },
- Options.MUTATIONS.value: {
- "name": "Mutated Attributes",
- "description": "Exports Abyssal stats"
- }
-}
-
def exportEft(fit, options):
# EFT formatted export is split in several sections, each section is
@@ -73,7 +70,6 @@ def exportEft(fit, options):
# Section 1: modules, rigs, subsystems, services
modsBySlotType = {}
- sFit = svcFit.getInstance()
for module in fit.modules:
modsBySlotType.setdefault(module.slot, []).append(module)
modSection = []
@@ -85,20 +81,19 @@ def exportEft(fit, options):
modules = modsBySlotType.get(slotType, ())
for module in modules:
if module.item:
- mutated = bool(module.mutators)
# if module was mutated, use base item name for export
- if mutated:
+ if module.isMutated:
modName = module.baseItem.name
else:
modName = module.item.name
- if mutated and options & Options.MUTATIONS.value:
+ if module.isMutated and options[Options.MUTATIONS.value]:
mutants[mutantReference] = module
mutationSuffix = ' [{}]'.format(mutantReference)
mutantReference += 1
else:
mutationSuffix = ''
modOfflineSuffix = ' {}'.format(OFFLINE_SUFFIX) if module.state == State.OFFLINE else ''
- if module.charge and sFit.serviceFittingOptions['exportCharges']:
+ if module.charge and options[Options.LOADED_CHARGES.value]:
rackLines.append('{}, {}{}{}'.format(
modName, module.charge.name, modOfflineSuffix, mutationSuffix))
else:
@@ -127,7 +122,7 @@ def exportEft(fit, options):
sections.append('\n\n'.join(minionSection))
# Section 3: implants, boosters
- if options & Options.IMPLANTS.value:
+ if options[Options.IMPLANTS.value]:
charSection = []
implantLines = []
for implant in fit.implants:
@@ -154,7 +149,7 @@ def exportEft(fit, options):
# Section 5: mutated modules' details
mutationLines = []
- if mutants and options & Options.MUTATIONS.value:
+ if mutants and options[Options.MUTATIONS.value]:
for mutantReference in sorted(mutants):
mutant = mutants[mutantReference]
mutationLines.append(renderMutant(mutant, firstPrefix='[{}] '.format(mutantReference), prefix=' '))
@@ -164,8 +159,8 @@ def exportEft(fit, options):
return '{}\n\n{}'.format(header, '\n\n\n'.join(sections))
-def importEft(eftString):
- lines = _importPrepareString(eftString)
+def importEft(lines):
+ lines = _importPrepare(lines)
try:
fit = _importCreateFit(lines)
except EftImportError:
@@ -293,7 +288,7 @@ def importEft(eftString):
return fit
-def importEftCfg(shipname, contents, iportuser):
+def importEftCfg(shipname, lines, iportuser):
"""Handle import from EFT config store file"""
# Check if we have such ship in database, bail if we don't
@@ -305,7 +300,6 @@ def importEftCfg(shipname, contents, iportuser):
fits = [] # List for fits
fitIndices = [] # List for starting line numbers for each fit
- lines = re.split('[\n\r]+', contents) # Separate string into lines
for line in lines:
# Detect fit header
@@ -486,8 +480,7 @@ def importEftCfg(shipname, contents, iportuser):
return fits
-def _importPrepareString(eftString):
- lines = eftString.splitlines()
+def _importPrepare(lines):
for i in range(len(lines)):
lines[i] = lines[i].strip()
while lines and not lines[0]:
diff --git a/service/port/esi.py b/service/port/esi.py
index f1e02d13a..a97480624 100644
--- a/service/port/esi.py
+++ b/service/port/esi.py
@@ -97,7 +97,7 @@ def exportESI(ofit):
item['type_id'] = module.item.ID
fit['items'].append(item)
- if module.charge and sFit.serviceFittingOptions["exportCharges"]:
+ if module.charge:
if module.chargeID not in charges:
charges[module.chargeID] = 0
# `or 1` because some charges (ie scripts) are without qty
@@ -137,11 +137,11 @@ def exportESI(ofit):
return json.dumps(fit)
-def importESI(str_):
+def importESI(string):
sMkt = Market.getInstance()
fitobj = Fit()
- refobj = json.loads(str_)
+ refobj = json.loads(string)
items = refobj['items']
# "<" and ">" is replace to "<", ">" by EVE client
fitobj.name = refobj['name']
diff --git a/service/port/multibuy.py b/service/port/multibuy.py
index f50750e76..1c6b78836 100644
--- a/service/port/multibuy.py
+++ b/service/port/multibuy.py
@@ -18,10 +18,23 @@
# =============================================================================
-from service.fit import Fit as svcFit
+from enum import Enum
-def exportMultiBuy(fit):
+class Options(Enum):
+ IMPLANTS = 1
+ CARGO = 2
+ LOADED_CHARGES = 3
+
+
+MULTIBUY_OPTIONS = (
+ (Options.LOADED_CHARGES.value, 'Loaded Charges', 'Export charges loaded into modules', True),
+ (Options.IMPLANTS.value, 'Implants && Boosters', 'Export implants and boosters', False),
+ (Options.CARGO.value, 'Cargo', 'Export cargo contents', True),
+)
+
+
+def exportMultiBuy(fit, options):
itemCounts = {}
def addItem(item, quantity=1):
@@ -29,11 +42,13 @@ def exportMultiBuy(fit):
itemCounts[item] = 0
itemCounts[item] += quantity
- exportCharges = svcFit.getInstance().serviceFittingOptions["exportCharges"]
for module in fit.modules:
if module.item:
+ # Mutated items are of no use for multibuy
+ if module.isMutated:
+ continue
addItem(module.item)
- if exportCharges and module.charge:
+ if module.charge and options[Options.LOADED_CHARGES.value]:
addItem(module.charge, module.numCharges)
for drone in fit.drones:
@@ -42,14 +57,16 @@ def exportMultiBuy(fit):
for fighter in fit.fighters:
addItem(fighter.item, fighter.amountActive)
- for cargo in fit.cargo:
- addItem(cargo.item, cargo.amount)
+ if options[Options.CARGO.value]:
+ for cargo in fit.cargo:
+ addItem(cargo.item, cargo.amount)
- for implant in fit.implants:
- addItem(implant.item)
+ if options[Options.IMPLANTS.value]:
+ for implant in fit.implants:
+ addItem(implant.item)
- for booster in fit.boosters:
- addItem(booster.item)
+ for booster in fit.boosters:
+ addItem(booster.item)
exportLines = []
exportLines.append(fit.ship.item.name)
diff --git a/service/port/port.py b/service/port/port.py
index cad3dfae2..8038cab8e 100644
--- a/service/port/port.py
+++ b/service/port/port.py
@@ -207,9 +207,14 @@ class Port(object):
@classmethod
def importAuto(cls, string, path=None, activeFit=None, iportuser=None):
# type: (Port, str, str, object, IPortUser) -> object
+ lines = string.splitlines()
# Get first line and strip space symbols of it to avoid possible detection errors
- firstLine = re.split("[\n\r]+", string.strip(), maxsplit=1)[0]
- firstLine = firstLine.strip()
+ firstLine = ''
+ for line in lines:
+ line = line.strip()
+ if line:
+ firstLine = line
+ break
# If XML-style start of tag encountered, detect as XML
if re.search(RE_XML_START, firstLine):
@@ -224,12 +229,12 @@ class Port(object):
if re.match("\[.*\]", firstLine) and path is not None:
filename = os.path.split(path)[1]
shipName = filename.rsplit('.')[0]
- return "EFT Config", cls.importEftCfg(shipName, string, iportuser)
+ return "EFT Config", cls.importEftCfg(shipName, lines, iportuser)
# If no file is specified and there's comma between brackets,
# consider that we have [ship, setup name] and detect like eft export format
if re.match("\[.*,.*\]", firstLine):
- return "EFT", (cls.importEft(string),)
+ return "EFT", (cls.importEft(lines),)
# Check if string is in DNA format
if re.match("\d+(:\d+(;\d+))*::", firstLine):
@@ -237,19 +242,19 @@ class Port(object):
# Assume that we import stand-alone abyssal module if all else fails
try:
- return "MutatedItem", (parseMutant(string.split("\n")),)
+ return "MutatedItem", (parseMutant(lines),)
except:
pass
# EFT-related methods
@staticmethod
- def importEft(eftString):
- return importEft(eftString)
+ def importEft(lines):
+ return importEft(lines)
@staticmethod
- def importEftCfg(shipname, contents, iportuser=None):
- return importEftCfg(shipname, contents, iportuser)
+ def importEftCfg(shipname, lines, iportuser=None):
+ return importEftCfg(shipname, lines, iportuser)
@classmethod
def exportEft(cls, fit, options):
@@ -284,5 +289,5 @@ class Port(object):
# Multibuy-related methods
@staticmethod
- def exportMultiBuy(fit):
- return exportMultiBuy(fit)
+ def exportMultiBuy(fit, options):
+ return exportMultiBuy(fit, options)
diff --git a/service/port/xml.py b/service/port/xml.py
index 54d5a98eb..432bbcdd8 100644
--- a/service/port/xml.py
+++ b/service/port/xml.py
@@ -283,7 +283,7 @@ def exportXml(iportuser, *fits):
hardware.setAttribute("slot", "%s slot %d" % (slotName, slotId))
fitting.appendChild(hardware)
- if module.charge and sFit.serviceFittingOptions["exportCharges"]:
+ if module.charge:
if module.charge.name not in charges:
charges[module.charge.name] = 0
# `or 1` because some charges (ie scripts) are without qty
diff --git a/service/price.py b/service/price.py
index 9a8485274..3028b74ad 100644
--- a/service/price.py
+++ b/service/price.py
@@ -26,6 +26,7 @@ import wx
from logbook import Logger
from eos import db
+from eos.saveddata.price import PriceStatus
from service.fit import Fit
from service.market import Market
from service.network import TimeoutError
@@ -85,13 +86,18 @@ class Price(object):
toRequest = set()
# Compose list of items we're going to request
- for typeID in priceMap:
+ for typeID in tuple(priceMap):
# Get item object
item = db.getItem(typeID)
# We're not going to request items only with market group, as eve-central
# doesn't provide any data for items not on the market
- if item is not None and item.marketGroupID:
- toRequest.add(typeID)
+ if item is None:
+ continue
+ if not item.marketGroupID:
+ priceMap[typeID].status = PriceStatus.notSupported
+ del priceMap[typeID]
+ continue
+ toRequest.add(typeID)
# Do not waste our time if all items are not on the market
if len(toRequest) == 0:
@@ -117,11 +123,10 @@ class Price(object):
except TimeoutError:
# Timeout error deserves special treatment
pyfalog.warning("Price fetch timout")
- for typeID in priceMap.keys():
+ for typeID in tuple(priceMap):
priceobj = priceMap[typeID]
priceobj.time = time.time() + TIMEOUT
- priceobj.failed = True
-
+ priceobj.status = PriceStatus.fail
del priceMap[typeID]
except Exception as ex:
# something happened, try another source
@@ -134,7 +139,7 @@ class Price(object):
for typeID in priceMap.keys():
priceobj = priceMap[typeID]
priceobj.time = time.time() + REREQUEST
- priceobj.failed = True
+ priceobj.status = PriceStatus.fail
@classmethod
def fitItemsList(cls, fit):
@@ -172,8 +177,8 @@ class Price(object):
def getPrices(self, objitems, callback, waitforthread=False):
"""Get prices for multiple typeIDs"""
requests = []
+ sMkt = Market.getInstance()
for objitem in objitems:
- sMkt = Market.getInstance()
item = sMkt.getItem(objitem)
requests.append(item.price)
@@ -197,6 +202,7 @@ class Price(object):
class PriceWorkerThread(threading.Thread):
+
def __init__(self):
threading.Thread.__init__(self)
self.name = "PriceWorker"
diff --git a/service/server.py b/service/server.py
index 09af2299e..31f235c04 100644
--- a/service/server.py
+++ b/service/server.py
@@ -117,13 +117,7 @@ class StoppableHTTPServer(socketserver.TCPServer):
# self.settings = CRESTSettings.getInstance()
- # Allow listening for x seconds
- sec = 120
- pyfalog.debug("Running server for {0} seconds", sec)
-
self.socket.settimeout(1)
- self.max_tries = sec / self.socket.gettimeout()
- self.tries = 0
self.run = True
def get_request(self):
@@ -140,13 +134,6 @@ class StoppableHTTPServer(socketserver.TCPServer):
pyfalog.warning("Setting pyfa server to stop.")
self.run = False
- def handle_timeout(self):
- pyfalog.debug("Number of tries: {0}", self.tries)
- self.tries += 1
- if self.tries == self.max_tries:
- pyfalog.debug("Server timed out waiting for connection")
- self.stop()
-
def serve(self, callback=None):
self.callback = callback
while self.run:
diff --git a/service/settings.py b/service/settings.py
index 39f8def9c..df5038d1f 100644
--- a/service/settings.py
+++ b/service/settings.py
@@ -525,6 +525,7 @@ class ContextMenuSettings(object):
"tacticalMode" : 1,
"targetResists" : 1,
"whProjector" : 1,
+ "moduleFill" : 1,
}
self.ContextMenuDefaultSettings = SettingsProvider.getInstance().getSettings("pyfaContextMenuSettings", ContextMenuDefaultSettings)
diff --git a/version.yml b/version.yml
new file mode 100644
index 000000000..e35fb355c
--- /dev/null
+++ b/version.yml
@@ -0,0 +1 @@
+version: v2.7.5