Merge remote-tracking branch 'origin/master' into spoolup

This commit is contained in:
Ryan Holmes
2019-03-02 20:57:29 -05:00
98 changed files with 1641 additions and 590 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

26
eos/const.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -17,17 +17,20 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
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,

View File

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

View File

@@ -1,7 +1,7 @@
# ammoInfluenceCapNeed
#
# Used by:
# Items from category: Charge (493 of 947)
# Items from category: Charge (493 of 949)
type = "passive"

View File

@@ -1,7 +1,7 @@
# ammoInfluenceRange
#
# Used by:
# Items from category: Charge (587 of 947)
# Items from category: Charge (587 of 949)
type = "passive"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
# skillBonusDroneDurability
#
# Used by:
# Implants from group: Cyber Drones (4 of 4)
# Skill: Drone Durability
type = "passive"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,24 +18,30 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
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):

BIN
eve.db

Binary file not shown.

View File

@@ -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.",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -58,7 +58,6 @@ class AttributeSlider(wx.Panel):
self.UserMinValue = minValue
self.UserMaxValue = maxValue
print(self.UserMinValue, self.UserMaxValue)
self.inverse = inverse

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -158,4 +158,5 @@ class PFListPane(wx.ScrolledWindow):
for widget in self._wList:
widget.Destroy()
self.Scroll(0, 0)
self._wList = []

View File

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

View File

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

View File

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

View File

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

View File

@@ -189,6 +189,7 @@ from gui.builtinContextMenus import ( # noqa: E402,F401
marketJump,
# droneSplit,
itemRemove,
fillWithModule,
droneRemoveStack,
ammoPattern,
project,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

121
pyfa.spec Normal file
View File

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

View File

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

23
scripts/dump_version.py Normal file
View File

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

View File

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

View File

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

11
scripts/package-osx.sh Normal file
View File

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

View File

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

7
scripts/setup-osx.sh Normal file
View File

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

View File

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

View File

@@ -89,7 +89,6 @@ class Fit(FitDeprecated):
"showTooltip": True,
"showMarketShortcuts": False,
"enableGaugeAnimation": True,
"exportCharges": True,
"openFitInNew": False,
"priceSystem": "Jita",
"priceSource": "eve-marketdata.com",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 "&lt;", "&gt;" by EVE client
fitobj.name = refobj['name']

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -525,6 +525,7 @@ class ContextMenuSettings(object):
"tacticalMode" : 1,
"targetResists" : 1,
"whProjector" : 1,
"moduleFill" : 1,
}
self.ContextMenuDefaultSettings = SettingsProvider.getInstance().getSettings("pyfaContextMenuSettings", ContextMenuDefaultSettings)

1
version.yml Normal file
View File

@@ -0,0 +1 @@
version: v2.7.5