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